diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a95e44e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,132 @@ +# CodeGraph - Agent Instructions + +Knowledge graph visualization tool for codebases. Python FastAPI backend + React/TypeScript frontend + FalkorDB graph database. + +## Architecture + +- **Backend** (`api/`): FastAPI, async-first. All routes in `api/index.py`. Graph ops in `api/graph.py`. LLM chat via GraphRAG in `api/llm.py`. +- **Frontend** (`app/`): React 18 + TypeScript + Vite. Tailwind CSS + Radix UI. 3D force-graph visualization (D3/Force-Graph). +- **Database**: FalkorDB (graph DB on Redis). Metadata in Redis key-value store. +- **Analyzers** (`api/analyzers/`): tree-sitter (Python), multilspy (Java, C#). Base class in `analyzer.py`, orchestrator in `source_analyzer.py`. + +### Data flow + +1. User submits repo URL or folder path -> backend clones/reads and analyzes via language-specific analyzers +2. Entities stored in FalkorDB (nodes: File, Class, Function; edges: DEFINES, CALLS, etc.) +3. Metadata (URL, commit) stored in Redis +4. Frontend fetches graph, renders interactive visualization +5. User can chat (GraphRAG), explore neighbors, find paths + +## Directory structure + +```text +api/ # Python backend + index.py # FastAPI app, routes, auth, SPA serving + graph.py # FalkorDB graph operations (sync + async) + llm.py # GraphRAG + LiteLLM chat + project.py # Repo cloning and analysis pipeline + info.py # Redis metadata operations + prompts.py # LLM prompt templates + auto_complete.py # Prefix search + analyzers/ # Language-specific code analyzers + entities/ # Graph entity models + git_utils/ # Git history graph construction +app/ # React frontend (Vite) + src/components/ # React components (ForceGraph, chat, code-graph, etc.) + src/lib/ # Utilities +tests/ # Pytest backend tests + endpoints/ # API endpoint integration tests +e2e/ # Playwright E2E tests + seed_test_data.py # Test data seeder +``` + +## Commands + +```bash +make install # Install all deps (uv sync + npm install) +make build-dev # Build frontend (dev mode) +make build-prod # Build frontend (production) +make run-dev # Build dev frontend + run API with reload +make run-prod # Build prod frontend + run API +make test # Run pytest suite +make lint # Ruff + TypeScript type-check +make lint-py # Ruff only +make lint-fe # TypeScript type-check only +make e2e # Run Playwright tests +make clean # Remove build artifacts +make docker-falkordb # Start FalkorDB container for testing +make docker-stop # Stop test containers +``` + +### Manual commands + +```bash +# Backend +uv run uvicorn api.index:app --host 127.0.0.1 --port 5000 --reload +uv run python -m pytest tests/ --verbose +uv run ruff check . + +# Frontend +npm --prefix ./app run dev # Vite dev server (port 3000, proxies /api to 5000) +npm --prefix ./app run build # Production build +npm --prefix ./app run lint # Type-check + +# E2E +npx playwright test +``` + +## Conventions + +### Python (backend) +- snake_case for functions/variables, PascalCase for classes +- Async-first: route handlers and most graph operations are async, though api/graph.py includes some synchronous helpers +- Auth: `public_or_auth` for read endpoints, `token_required` for mutating endpoints +- Graph labels: PascalCase (File, Class, Function). Relations: SCREAMING_SNAKE_CASE (DEFINES, CALLS) +- Linter: Ruff + +### TypeScript (frontend) +- camelCase for functions/variables, PascalCase for components +- Tailwind CSS for styling +- Radix UI for headless components +- Linter: tsc (type-check only) + +### General +- Python >=3.12,<3.14. Node 20+. +- Package managers: `uv` (Python), `npm` (frontend) +- Environment variables: SCREAMING_SNAKE_CASE. See `.env.template` for reference. + +## Environment variables + +Key variables (see `.env.template` for full list): + +| Variable | Default | Purpose | +|----------|---------|---------| +| `FALKORDB_HOST` | `localhost` | Graph DB host | +| `FALKORDB_PORT` | `6379` | Graph DB port | +| `SECRET_TOKEN` | empty | API auth token | +| `CODE_GRAPH_PUBLIC` | `0` | Skip auth on read endpoints when `1` | +| `MODEL_NAME` | `gemini/gemini-flash-lite-latest` | LiteLLM model for chat | +| `ALLOWED_ANALYSIS_DIR` | repo root | Root path for analyze_folder | + +## Testing + +- **Backend**: `make test` runs pytest against `tests/`. Endpoint tests in `tests/endpoints/`. +- **E2E**: `make e2e` runs Playwright (Chromium + Firefox). Test data seeded via `e2e/seed_test_data.py`. +- **CI**: GitHub Actions — `build.yml` (lint + build), `playwright.yml` (E2E, 2 shards), `release-image.yml` (Docker image). + +## API endpoints + +### Read (public_or_auth) +- `GET /api/list_repos` — List indexed repos +- `GET /api/graph_entities?repo=` — Fetch graph entities +- `POST /api/get_neighbors` — Neighboring nodes +- `POST /api/auto_complete` — Prefix search +- `POST /api/repo_info` — Repo stats/metadata +- `POST /api/find_paths` — Paths between nodes +- `POST /api/chat` — GraphRAG chat +- `POST /api/list_commits` — Git commit history + +### Mutating (token_required) +- `POST /api/analyze_folder` — Analyze local folder +- `POST /api/analyze_repo` — Clone and analyze repo +- `POST /api/switch_commit` — Switch to specific commit diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/app/index.html b/app/index.html index 9ec23a9..8df9869 100644 --- a/app/index.html +++ b/app/index.html @@ -4,7 +4,7 @@ - + Code Graph by FalkorDB diff --git a/app/src/App.tsx b/app/src/App.tsx index a9ea42f..156b131 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -17,6 +17,11 @@ import { cn, GraphRef, Message, Path, PathData, PathNode } from '@/lib/utils'; import type { GraphNode } from '@falkordb/canvas'; import { Toaster } from '@/components/ui/toaster'; import GTM from './GTM'; +import { Button } from '@/components/ui/button'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { ThemeToggle } from './components/theme-toggle'; +import Logo from './components/logo'; + const Chat = lazy(() => import('./components/chat').then(mod => ({ default: mod.Chat }))); const CodeGraph = lazy(() => import('./components/code-graph').then(mod => ({ default: mod.CodeGraph }))); @@ -378,108 +383,137 @@ export default function App() {
-
+
- - FalkorDB + + -

+

CODE GRAPH

-
    - - -

    Main Website

    -
    - - -

    Github

    -
    - - -

    Discord

    -
    - - - - - -
    - HOW TO USE THE PRODUCT - -
    - { - DESKTOP_TIPS.map((tip, index) => ( -
    -
    -

    {tip.title}

    -

    {tip.keyboardCommand}

    -
    -

    {tip.description}

    + +
      +
    • + + + + + Main Website + +
    • +
    • + + + + + GitHub + +
    • +
    • + + + + + Discord + +
    • +
    • + + + + + + + + Tips + + +
      + HOW TO USE THE PRODUCT +
      - )) - } -
      -
      - { - import.meta.env.VITE_LOCAL_MODE && - - - - - - - {!isSubmit ? "CREATE A NEW PROJECT" : "THANK YOU FOR A NEW REQUEST"} - - { - !isSubmit - ? "Please provide the URL of the project to connect and start querying data" - : "Processing your graph, this could take a while. We appreciate your patience" - } - - { - !isSubmit ? -
      - setCreateURL(e.target.value)} - placeholder="Type Project URL (File:// or https://)" - /> -
      - + DESKTOP_TIPS.map((tip, index) => ( +
      +
      +

      {tip.title}

      +

      {tip.keyboardCommand}

      +
      +

      {tip.description}

      - - : - } - -
      - } -
    + )) + } + + + +
  • + +
  • + { + import.meta.env.VITE_LOCAL_MODE && +
  • + + + + + + + {!isSubmit ? "CREATE A NEW PROJECT" : "THANK YOU FOR A NEW REQUEST"} + + + { + !isSubmit + ? "Please provide the URL of the project to connect and start querying data" + : "Processing your graph, this could take a while. We appreciate your patience" + } + + { + !isSubmit ? +
    + setCreateURL(e.target.value)} + placeholder="Type Project URL (File:// or https://)" + /> +
    + +
    +
    + : + } +
    +
    +
  • + } +
+
-
Loading...
}> @@ -514,7 +548,7 @@ export default function App() { setHasHiddenElements={setHasHiddenElements} /> - +
-
- - FalkorDB +
+ + + CODE GRAPH - +
+ + +
{menuOpen && ( -
+
  • -

    Github

    +

    GitHub

  • @@ -593,8 +631,8 @@ export default function App() {
    ))} @@ -640,11 +678,11 @@ export default function App() {
    - + - + @@ -673,11 +711,11 @@ export default function App() { - + - + @@ -689,7 +727,7 @@ export default function App() { cooldownTicks={cooldownTicks} /> setSearchNode(node)} icon={} diff --git a/app/src/components/Input.tsx b/app/src/components/Input.tsx index 1b2fa39..2073463 100644 --- a/app/src/components/Input.tsx +++ b/app/src/components/Input.tsx @@ -149,7 +149,7 @@ export default function Input({ onValueChange, handleSubmit, graph, icon, node, } }} onKeyDown={handleKeyDown} - className={cn("w-full border p-2 rounded-md pointer-events-auto", className)} + className={cn("w-full border p-2 rounded-md pointer-events-auto bg-background text-foreground focus:border-primary focus:ring-1 focus:ring-primary/50 focus-visible:outline-none transition-colors", className)} placeholder="Search for nodes in the graph" value={node?.name || ""} onChange={(e) => { @@ -175,7 +175,7 @@ export default function Input({ onValueChange, handleSubmit, graph, icon, node, open &&
    setSelectedOption(index)} onClick={() => { @@ -216,7 +216,7 @@ export default function Input({ onValueChange, handleSubmit, graph, icon, node,

    {name}

    -

    +

    {path}

    diff --git a/app/src/components/chat.tsx b/app/src/components/chat.tsx index 4d12ff1..0ecc49c 100644 --- a/app/src/components/chat.tsx +++ b/app/src/components/chat.tsx @@ -1,12 +1,13 @@ import { toast } from "@/components/ui/use-toast"; import { Dispatch, FormEvent, SetStateAction, useEffect, useRef, useState } from "react"; -import { AlignLeft, ArrowRight, ChevronDown, Lightbulb, Undo2 } from "lucide-react"; +import { AlignLeft, ArrowRight, ChevronDown, Lightbulb, Loader2, Undo2 } from "lucide-react"; import { Message, MessageTypes, Path, PathData, PATH_COLOR } from "@/lib/utils"; import Input from "./Input"; import { Graph, GraphData, Node } from "./model"; import { cn, GraphRef } from "@/lib/utils"; import { TypeAnimation } from "react-type-animation"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; const AUTH_HEADERS: HeadersInit = import.meta.env.VITE_SECRET_TOKEN ? { 'Authorization': `Bearer ${import.meta.env.VITE_SECRET_TOKEN}` } @@ -548,7 +549,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set value={path?.start?.name || ""} placeholder="Start typing starting point" type="text" - icon={} + icon={} node={path?.start} scrollToBottom={() => containerRef.current?.scrollTo(0, containerRef.current?.scrollHeight)} /> @@ -559,7 +560,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set onValueChange={({ name, id }) => setPath(prev => ({ end: { name, id }, start: prev?.start }))} placeholder="Start typing end point" type="text" - icon={} + icon={} node={path?.end} scrollToBottom={() => containerRef.current?.scrollTo(0, containerRef.current?.scrollHeight)} /> @@ -578,7 +579,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set p.nodes.length === selectedPath?.nodes.length && selectedPath?.nodes.every(node => p?.nodes.some((n) => n.id === node.id)) && "border-[#ffde21] bg-[#ffde2133]", - message.graphName !== graph.Id && "opacity-50 bg-gray-200" + message.graphName !== graph.Id && "opacity-50 bg-secondary" )} title={message.graphName !== graph.Id ? `Move to graph ${message.graphName} to use this path` : undefined} disabled={message.graphName !== graph.Id} @@ -615,7 +616,10 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set ) default: return (
    - Waiting for response +
    + + Thinking... +
    ) } @@ -642,7 +646,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set
    - @@ -650,11 +654,11 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set {getTip("!w-full")} -
    - - + + +
    diff --git a/app/src/components/code-graph.tsx b/app/src/components/code-graph.tsx index 117311f..c0b2a08 100644 --- a/app/src/components/code-graph.tsx +++ b/app/src/components/code-graph.tsx @@ -3,6 +3,7 @@ import { Graph, GraphData, Node, Link } from "./model"; import { Toolbar } from "./toolbar"; import { Labels } from "./labels"; import { Download, GitFork, Search, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; import ElementMenu from "./elementMenu"; import Combobox from "./combobox"; import { toast } from '@/components/ui/use-toast'; @@ -346,9 +347,9 @@ export function CodeGraph({ } return ( -
    +
    -
    +
    -
    +
    { graph.Id ?
    @@ -377,8 +378,10 @@ export function CodeGraph({
    { (isPathResponse || isPathResponse === undefined) && - + Reset Graph + } { hasHiddenElements && - + Unhide Nodes + }
    @@ -476,7 +481,7 @@ export function CodeGraph({ zoomedNodes={zoomedNodes} />
    -
    +

    {nodesCount} Nodes

    |

    {edgesCount} Edges

    @@ -484,7 +489,7 @@ export function CodeGraph({
    { commitIndex !== commits.length && -
    +
    - :
    - + :
    +

    Select a repo to show its graph here

    } diff --git a/app/src/components/combobox.tsx b/app/src/components/combobox.tsx index 0126636..93c6720 100644 --- a/app/src/components/combobox.tsx +++ b/app/src/components/combobox.tsx @@ -58,7 +58,7 @@ export default function Combobox({ options, setOptions, selectedValue, onSelecte return (