Skip to content

Commit 8f57bd1

Browse files
authored
Merge branch 'staging' into add-loading-states
2 parents 57cc3f9 + 7aa40c6 commit 8f57bd1

26 files changed

Lines changed: 1844 additions & 101 deletions

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
version: "latest"
5353

5454
- name: Install backend dependencies
55-
run: uv sync --extra test
55+
run: uv sync --all-extras
5656

5757
docker-build:
5858
runs-on: ubuntu-latest

.github/workflows/playwright.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
version: "latest"
4646

4747
- name: Install backend dependencies
48-
run: uv sync
48+
run: uv sync --all-extras
4949

5050
- name: Install frontend dependencies
5151
working-directory: ./app

.github/workflows/pypi-publish.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
tags: ["v*.*.*"]
6+
7+
jobs:
8+
publish:
9+
runs-on: ubuntu-latest
10+
environment: pypi
11+
permissions:
12+
id-token: write
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v6
16+
17+
- name: Install uv
18+
uses: astral-sh/setup-uv@v5
19+
20+
- name: Build package
21+
run: uv build
22+
23+
- name: Publish to PyPI
24+
uses: pypa/gh-action-pypi-publish@release/v1

AGENTS.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Knowledge graph visualization tool for codebases. Python FastAPI backend + React
2121

2222
```text
2323
api/ # Python backend
24+
cli.py # cgraph CLI tool (typer)
2425
index.py # FastAPI app, routes, auth, SPA serving
2526
graph.py # FalkorDB graph operations (sync + async)
2627
llm.py # GraphRAG + LiteLLM chat
@@ -34,6 +35,7 @@ api/ # Python backend
3435
app/ # React frontend (Vite)
3536
src/components/ # React components (ForceGraph, chat, code-graph, etc.)
3637
src/lib/ # Utilities
38+
skills/code-graph/ # Claude Code skill for code graph indexing/querying
3739
tests/ # Pytest backend tests
3840
endpoints/ # API endpoint integration tests
3941
e2e/ # Playwright E2E tests
@@ -44,6 +46,7 @@ e2e/ # Playwright E2E tests
4446

4547
```bash
4648
make install # Install all deps (uv sync + npm install)
49+
make install-cli # Install cgraph CLI entry point
4750
make build-dev # Build frontend (dev mode)
4851
make build-prod # Build frontend (production)
4952
make run-dev # Build dev frontend + run API with reload
@@ -130,3 +133,24 @@ Key variables (see `.env.template` for full list):
130133
- `POST /api/analyze_folder` — Analyze local folder
131134
- `POST /api/analyze_repo` — Clone and analyze repo
132135
- `POST /api/switch_commit` — Switch to specific commit
136+
137+
## CLI (`cgraph`)
138+
139+
Typer-based CLI wrapping the sync `Graph` and `Project` classes. Outputs JSON to stdout, status to stderr. Entry point: `api/cli.py`.
140+
141+
Install: `pipx install falkordb-code-graph` or `pip install falkordb-code-graph`
142+
143+
For development: `make install-cli` or `uv pip install -e .`
144+
145+
```bash
146+
cgraph ensure-db # Start FalkorDB if not running
147+
cgraph index . --ignore node_modules # Index local folder
148+
cgraph index-repo <url> # Clone + index a repo
149+
cgraph list # List indexed repos
150+
cgraph search <prefix> [--repo <name>] # Full-text prefix search
151+
cgraph neighbors <id>... [--repo <name>] [--rel <type>] [--label <label>] # Connected entities
152+
cgraph paths <src-id> <dest-id> [--repo <name>] # Call-chain paths
153+
cgraph info [--repo <name>] # Repo stats + metadata
154+
```
155+
156+
`--repo` defaults to the current directory name. Claude Code skill in `skills/code-graph/`.

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help install test e2e lint lint-py lint-fe clean build-dev build-prod run-dev run-prod docker-falkordb docker-stop
1+
.PHONY: help install install-cli test e2e lint lint-py lint-fe clean build-dev build-prod run-dev run-prod docker-falkordb docker-stop
22

33
help: ## Show this help message
44
@echo 'Usage: make [target]'
@@ -10,6 +10,9 @@ install: ## Install all dependencies (backend + frontend)
1010
uv sync --all-extras
1111
npm install --prefix ./app
1212

13+
install-cli: ## Install cgraph CLI entry point
14+
uv pip install -e .
15+
1316
build-dev: ## Build frontend for development
1417
npm --prefix ./app run build:dev
1518

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ code-graph/
2626
│ ├── project.py # Repository cloning and analysis orchestration
2727
│ ├── info.py # Repository metadata stored in Redis/FalkorDB
2828
│ ├── prompts.py # LLM system and prompt templates
29+
│ ├── cli.py # cgraph CLI tool (typer)
2930
│ ├── auto_complete.py # Prefix search helper
3031
│ ├── analyzers/ # Source analyzers (Python, Java, C#)
3132
│ ├── entities/ # Graph/entity models
@@ -37,6 +38,7 @@ code-graph/
3738
│ ├── package.json # Frontend dependencies and scripts
3839
│ ├── vite.config.ts # Vite config and /api proxy for dev mode
3940
│ └── tsconfig*.json # TypeScript config
41+
├── skills/code-graph/ # Claude Code skill for CLI-driven indexing/querying
4042
├── tests/ # Backend/unit and endpoint tests
4143
├── e2e/ # End-to-end helpers and Playwright assets
4244
├── Dockerfile # Unified container image
@@ -145,6 +147,7 @@ In this mode, the FastAPI app serves the built React SPA from `app/dist` on `htt
145147

146148
```bash
147149
make install # Install backend + frontend dependencies
150+
make install-cli # Install cgraph CLI entry point
148151
make build-dev # Build frontend in development mode
149152
make build-prod # Build frontend for production
150153
make run-dev # Build dev frontend + run Uvicorn with reload
@@ -157,6 +160,68 @@ make clean # Remove build/test artifacts
157160

158161
`make test` currently points at the right backend test entrypoint, but some legacy analyzer/git-history tests still need maintenance before the suite passes on a clean checkout.
159162

163+
## CLI Tool (`cgraph`)
164+
165+
CodeGraph includes a CLI tool for indexing codebases and querying the knowledge graph directly from the terminal. All output is JSON (to stdout), with status messages on stderr.
166+
167+
### Install
168+
169+
```bash
170+
# Install from PyPI (recommended for end users)
171+
pipx install falkordb-code-graph
172+
173+
# Or with pip
174+
pip install falkordb-code-graph
175+
```
176+
177+
For development (from a local clone):
178+
179+
```bash
180+
make install-cli
181+
# or
182+
uv pip install -e .
183+
```
184+
185+
### Usage
186+
187+
```bash
188+
# Ensure FalkorDB is running (auto-starts a Docker container if needed)
189+
cgraph ensure-db
190+
191+
# Index the current project
192+
cgraph index . --ignore node_modules --ignore .git --ignore venv --ignore __pycache__
193+
194+
# Index a remote repository
195+
cgraph index-repo https://github.com/user/repo --ignore node_modules
196+
197+
# List indexed repos
198+
cgraph list
199+
200+
# Search for entities by name prefix
201+
cgraph search parse_config
202+
203+
# Explore relationships (what does node 42 call?)
204+
cgraph neighbors 42 --rel CALLS
205+
206+
# Find call-chain paths between two nodes
207+
cgraph paths 42 99
208+
209+
# Show repo statistics
210+
cgraph info
211+
```
212+
213+
The `--repo` flag defaults to the current directory name. Run `cgraph --help` for full details.
214+
215+
### Claude Code Skill
216+
217+
A [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill is included in `skills/code-graph/`. Install it with:
218+
219+
```bash
220+
npx skills add FalkorDB/code-graph
221+
```
222+
223+
Then ask Claude things like *"what functions call analyze_sources?"* or *"find the dependency chain between parse_config and send_request"* — it will handle the indexing and querying automatically.
224+
160225
## Running with Docker
161226

162227
### Using Docker Compose

api/analyzers/analyzer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def resolve(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: P
5656
try:
5757
locations = lsp.request_definition(str(file_path), node.start_point.row, node.start_point.column)
5858
return [(files[Path(self.resolve_path(location['absolutePath'], path))], files[Path(self.resolve_path(location['absolutePath'], path))].tree.root_node.descendant_for_point_range(Point(location['range']['start']['line'], location['range']['start']['character']), Point(location['range']['end']['line'], location['range']['end']['character']))) for location in locations if location and Path(self.resolve_path(location['absolutePath'], path)) in files]
59-
except Exception as e:
59+
except Exception:
6060
return []
6161

6262
@abstractmethod
@@ -135,7 +135,7 @@ def add_symbols(self, entity: Entity) -> None:
135135
@abstractmethod
136136
def resolve_symbol(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, key: str, symbol: Node) -> list[Entity]:
137137
"""
138-
Resolve a symbol to an entity.
138+
Resolve a symbol to entities.
139139
140140
Args:
141141
lsp (SyncLanguageServer): The language server.

api/analyzers/javascript/__init__.py

Whitespace-only changes.
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
"""JavaScript analyzer using tree-sitter for code entity extraction."""
2+
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
from multilspy import SyncLanguageServer
7+
from ...entities.entity import Entity
8+
from ...entities.file import File
9+
from ..analyzer import AbstractAnalyzer
10+
11+
import tree_sitter_javascript as tsjs
12+
from tree_sitter import Language, Node
13+
14+
import logging
15+
logger = logging.getLogger('code_graph')
16+
17+
18+
class JavaScriptAnalyzer(AbstractAnalyzer):
19+
"""Analyzer for JavaScript source files using tree-sitter.
20+
21+
Extracts functions, classes, and methods from JavaScript code.
22+
Resolves class inheritance (extends) and function/method call references.
23+
"""
24+
25+
def __init__(self) -> None:
26+
"""Initialize the JavaScript analyzer with the tree-sitter JS grammar."""
27+
super().__init__(Language(tsjs.language()))
28+
29+
def add_dependencies(self, path: Path, files: list[Path]) -> None:
30+
"""Detect and register JavaScript project dependencies.
31+
32+
Currently a no-op; npm dependency resolution is not yet implemented.
33+
"""
34+
pass
35+
36+
def get_entity_label(self, node: Node) -> str:
37+
"""Return the graph label for a given AST node type.
38+
39+
Args:
40+
node: A tree-sitter AST node representing a JavaScript entity.
41+
42+
Returns:
43+
One of 'Function', 'Class', or 'Method'.
44+
45+
Raises:
46+
ValueError: If the node type is not a recognised entity.
47+
"""
48+
if node.type == 'function_declaration':
49+
return "Function"
50+
elif node.type == 'class_declaration':
51+
return "Class"
52+
elif node.type == 'method_definition':
53+
return "Method"
54+
raise ValueError(f"Unknown entity type: {node.type}")
55+
56+
def get_entity_name(self, node: Node) -> str:
57+
"""Extract the declared name from a JavaScript entity node.
58+
59+
Args:
60+
node: A tree-sitter AST node for a function, class, or method.
61+
62+
Returns:
63+
The entity name, or an empty string if no name node is found.
64+
65+
Raises:
66+
ValueError: If the node type is not a recognised entity.
67+
"""
68+
if node.type in ['function_declaration', 'class_declaration', 'method_definition']:
69+
name_node = node.child_by_field_name('name')
70+
if name_node is None:
71+
return ''
72+
return name_node.text.decode('utf-8')
73+
raise ValueError(f"Unknown entity type: {node.type}")
74+
75+
def get_entity_docstring(self, node: Node) -> Optional[str]:
76+
"""Extract a leading comment as a docstring for the entity.
77+
78+
Looks for a comment node immediately preceding the entity in the AST.
79+
80+
Args:
81+
node: A tree-sitter AST node for a function, class, or method.
82+
83+
Returns:
84+
The comment text, or None if no leading comment exists.
85+
86+
Raises:
87+
ValueError: If the node type is not a recognised entity.
88+
"""
89+
if node.type in ['function_declaration', 'class_declaration', 'method_definition']:
90+
if node.prev_sibling and node.prev_sibling.type == 'comment':
91+
return node.prev_sibling.text.decode('utf-8')
92+
return None
93+
raise ValueError(f"Unknown entity type: {node.type}")
94+
95+
def get_entity_types(self) -> list[str]:
96+
"""Return the tree-sitter node types recognised as JavaScript entities."""
97+
return ['function_declaration', 'class_declaration', 'method_definition']
98+
99+
def add_symbols(self, entity: Entity) -> None:
100+
"""Extract symbols (references) from a JavaScript entity.
101+
102+
For classes: extracts base-class identifiers from ``extends`` clauses.
103+
For functions/methods: extracts call-expression references.
104+
105+
Note:
106+
JavaScript parameters are untyped, so they are not captured as
107+
symbols — unlike typed languages (Java, Python) where parameter
108+
type annotations are meaningful for resolution.
109+
"""
110+
if entity.node.type == 'class_declaration':
111+
for child in entity.node.children:
112+
if child.type == 'class_heritage':
113+
for heritage_child in child.children:
114+
if heritage_child.type == 'identifier':
115+
entity.add_symbol("base_class", heritage_child)
116+
elif entity.node.type in ['function_declaration', 'method_definition']:
117+
captures = self._captures("(call_expression) @reference.call", entity.node)
118+
if 'reference.call' in captures:
119+
for caller in captures['reference.call']:
120+
entity.add_symbol("call", caller)
121+
122+
def is_dependency(self, file_path: str) -> bool:
123+
"""Check whether a file path belongs to an external dependency.
124+
125+
Uses path-segment matching so that directories merely containing
126+
'node_modules' in their name (e.g. ``node_modules_utils``) are not
127+
treated as dependencies.
128+
"""
129+
return "node_modules" in Path(file_path).parts
130+
131+
def resolve_path(self, file_path: str, path: Path) -> str:
132+
"""Resolve an import path relative to the project root."""
133+
return file_path
134+
135+
def resolve_type(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, node: Node) -> list[Entity]:
136+
"""Resolve a type reference to its class declaration entity."""
137+
res = []
138+
for file, resolved_node in self.resolve(files, lsp, file_path, path, node):
139+
type_dec = self.find_parent(resolved_node, ['class_declaration'])
140+
if type_dec in file.entities:
141+
res.append(file.entities[type_dec])
142+
return res
143+
144+
def resolve_method(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, node: Node) -> list[Entity]:
145+
"""Resolve a call expression to the target function or method entity."""
146+
res = []
147+
if node.type == 'call_expression':
148+
func_node = node.child_by_field_name('function')
149+
if func_node and func_node.type == 'member_expression':
150+
func_node = func_node.child_by_field_name('property')
151+
if func_node:
152+
node = func_node
153+
for file, resolved_node in self.resolve(files, lsp, file_path, path, node):
154+
method_dec = self.find_parent(resolved_node, ['function_declaration', 'method_definition', 'class_declaration'])
155+
if method_dec and method_dec.type == 'class_declaration':
156+
continue
157+
if method_dec in file.entities:
158+
res.append(file.entities[method_dec])
159+
return res
160+
161+
def resolve_symbol(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, key: str, symbol: Node) -> list[Entity]:
162+
"""Dispatch symbol resolution based on the symbol category.
163+
164+
Routes ``base_class`` symbols to type resolution and ``call`` symbols
165+
to method resolution.
166+
"""
167+
if key == "base_class":
168+
return self.resolve_type(files, lsp, file_path, path, symbol)
169+
elif key == "call":
170+
return self.resolve_method(files, lsp, file_path, path, symbol)
171+
else:
172+
raise ValueError(f"Unknown key {key}")

api/analyzers/kotlin/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)