ctxgraph-code 0.5.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/PKG-INFO +31 -9
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/README.md +30 -8
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/pyproject.toml +1 -1
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/cli.py +299 -4
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/hooks.py +2 -1
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/storage.py +36 -0
- ctxgraph_code-0.6.0/src/ctxgraph_code/render/__init__.py +20 -0
- ctxgraph_code-0.6.0/src/ctxgraph_code/render/mermaid.py +146 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/PKG-INFO +31 -9
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/SOURCES.txt +3 -1
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/setup.cfg +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/__init__.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/__main__.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/__init__.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/python/__init__.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/python/importer.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/python/semantic.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/python/symbols.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/__init__.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/analyzer.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/languages.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/__init__.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/build_status.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/global_paths.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/init.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/settings.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/exclude/__init__.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/exclude/patterns.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/__init__.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/builder.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/models.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/query.py +0 -0
- /ctxgraph_code-0.5.0/src/ctxgraph_code/render.py → /ctxgraph_code-0.6.0/src/ctxgraph_code/render/_text.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/view/__init__.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/view/visualizer.py +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/dependency_links.txt +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/entry_points.txt +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/requires.txt +0 -0
- {ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctxgraph-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Code knowledge graph for Claude Code. Build a relationship graph of your Python codebase and query it during coding sessions.
|
|
5
5
|
Author: ctxgraph-code contributors
|
|
6
6
|
License: MIT
|
|
@@ -30,6 +30,8 @@ Requires-Dist: pytest>=7.0; extra == "dev"
|
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
pip install ctxgraph-code
|
|
33
|
+
# For multi-language support (C, Go, Rust, JS, TS, Java, etc.):
|
|
34
|
+
pip install 'ctxgraph-code[full]'
|
|
33
35
|
cd my-project
|
|
34
36
|
ctxgraph-code setup
|
|
35
37
|
```
|
|
@@ -52,10 +54,10 @@ These questions require running multiple `grep` commands or reading dependency c
|
|
|
52
54
|
## Quick Start
|
|
53
55
|
|
|
54
56
|
```bash
|
|
55
|
-
# Install
|
|
56
|
-
pip install ctxgraph-code
|
|
57
|
+
# Install (add [full] for multi-language support)
|
|
58
|
+
pip install 'ctxgraph-code[full]'
|
|
57
59
|
|
|
58
|
-
# Navigate to your
|
|
60
|
+
# Navigate to your project
|
|
59
61
|
cd my-project
|
|
60
62
|
|
|
61
63
|
# One-command setup: init + build + configure Claude Code
|
|
@@ -80,7 +82,14 @@ Interactive walkthrough — prompts for:
|
|
|
80
82
|
Does everything in one step:
|
|
81
83
|
1. Creates `.ctxgraph/config.toml` with your chosen extensions and excludes
|
|
82
84
|
2. Installs the `/ctxgraph-code` slash command globally (works in every Claude Code session)
|
|
83
|
-
3. Builds the knowledge graph from all matching files
|
|
85
|
+
3. Builds the knowledge graph from all matching files — shows live per-graph progress:
|
|
86
|
+
```
|
|
87
|
+
Building 5 graphs with 8 workers...
|
|
88
|
+
✔ src/ (42 files, 156 nodes, 34 edges, 0.8s)
|
|
89
|
+
✔ api/ (18 files, 73 nodes, 12 edges, 0.4s)
|
|
90
|
+
✔ tests/ (31 files, 89 nodes, 0 edges, 0.6s)
|
|
91
|
+
Built all 5 graphs in 2.1s
|
|
92
|
+
```
|
|
84
93
|
|
|
85
94
|
Non-interactive mode:
|
|
86
95
|
```bash
|
|
@@ -122,6 +131,15 @@ Scans all matching files in the project, runs AST analysis. Extensions are read
|
|
|
122
131
|
- `--verbose` / `-v` — show per-file progress
|
|
123
132
|
- `--no-summary` — skip docstring extraction for faster builds
|
|
124
133
|
|
|
134
|
+
Shows live per-graph progress as each completes:
|
|
135
|
+
```
|
|
136
|
+
Building 5 graphs with 8 workers...
|
|
137
|
+
✔ src/ (42 files, 156 nodes, 34 edges, 0.8s)
|
|
138
|
+
✔ api/ (18 files, 73 nodes, 12 edges, 0.4s)
|
|
139
|
+
✔ tests/ (31 files, 89 nodes, 0 edges, 0.6s)
|
|
140
|
+
Built all 5 graphs in 2.1s
|
|
141
|
+
```
|
|
142
|
+
|
|
125
143
|
Stores graphs in `.ctxgraph/graphs/<dir>.db` (per-directory) or `.ctxgraph/graph.db` (combined).
|
|
126
144
|
|
|
127
145
|
> The graph is a **static snapshot**. If code changes, run `ctxgraph-code build` again to refresh. Use `--incremental` to only reprocess changed files.
|
|
@@ -225,10 +243,10 @@ ctxgraph-code probe "database connection pool"
|
|
|
225
243
|
ctxgraph-code probe "user authentication" --max 3
|
|
226
244
|
```
|
|
227
245
|
|
|
228
|
-
Searches the graph for relevant nodes **and reads the actual source code** inline. Claude gets paths + source in one command, saving 1–2 tool calls. Shows the first
|
|
246
|
+
Searches the graph for relevant nodes **and reads the actual source code** inline. Claude gets paths + source in one command, saving 1–2 tool calls. Shows the first N lines of matched files with automatic syntax highlighting per language.
|
|
229
247
|
|
|
230
248
|
- `--max` / `-m` — max files to probe (default: 5)
|
|
231
|
-
- `--context` / `-c` — lines
|
|
249
|
+
- `--context` / `-c` — lines to show per file (default: 40, use 0 for full file)
|
|
232
250
|
|
|
233
251
|
### `install-hooks`
|
|
234
252
|
|
|
@@ -317,7 +335,10 @@ This catches the common case where you edit a file after building the graph and
|
|
|
317
335
|
| **Multiprocessing** | Combined graphs split files across CPU cores via `multiprocessing.Pool` |
|
|
318
336
|
| **`--jobs`** | Control parallelism level (default: CPU count) |
|
|
319
337
|
| **Incremental builds** | `--incremental` caches file mtimes, only reprocesses changed files |
|
|
320
|
-
| **Trivial file skip** | `_quick_scan()` pre-checks
|
|
338
|
+
| **Trivial file skip** | `_quick_scan()` pre-checks all files (Python and non-Python) — skips full parse for files with no code |
|
|
339
|
+
| **`follow_symlinks` config** | Respects `follow_symlinks = false` setting to avoid duplicate/broken symlinks |
|
|
340
|
+
| **`max_file_size_mb` config** | Skips files exceeding the configured size limit before reading |
|
|
341
|
+
| **Live build progress** | Per-graph status + timing as each completes during parallel builds |
|
|
321
342
|
| **Cached excludes** | `lru_cache` on `should_exclude()` during `os.walk` |
|
|
322
343
|
| **Batch SQLite inserts** | `executemany` instead of per-row `INSERT` statements |
|
|
323
344
|
| **`--no-summary`** | Skips docstring extraction (fastest rebuilds) |
|
|
@@ -389,7 +410,7 @@ Built-in default exclusion patterns (always applied): `__pycache__`, `*.pyc`, `.
|
|
|
389
410
|
|
|
390
411
|
| Feature | ctxgraph | ctxgraph-code |
|
|
391
412
|
|---------|----------|---------------|
|
|
392
|
-
| CLI commands | 9+ |
|
|
413
|
+
| CLI commands | 9+ | 17 (init, build, query, deps, usedby, overview, symbols, context, setup, view, info, install-slash, build-status, probe, install-hooks, uninstall-hooks, version) |
|
|
393
414
|
| LLM integration | Built-in (Ollama, Claude, OpenAI, Azure) | None (delegates to Claude Code) |
|
|
394
415
|
| Chat sessions | Yes | No |
|
|
395
416
|
| Visualizer | D3.js HTML + SVG | D3.js HTML (`view` opens in browser, `--tree` for text) |
|
|
@@ -404,6 +425,7 @@ Built-in default exclusion patterns (always applied): `__pycache__`, `*.pyc`, `.
|
|
|
404
425
|
|
|
405
426
|
- Python 3.10+
|
|
406
427
|
- A Claude Code subscription (for the `/ctxgraph-code` slash command — the graph itself works standalone)
|
|
428
|
+
- For multi-language analysis (C, Go, Rust, JS, TS, Java, etc.): `pip install 'ctxgraph-code[full]'` to install tree-sitter support
|
|
407
429
|
|
|
408
430
|
---
|
|
409
431
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
pip install ctxgraph-code
|
|
7
|
+
# For multi-language support (C, Go, Rust, JS, TS, Java, etc.):
|
|
8
|
+
pip install 'ctxgraph-code[full]'
|
|
7
9
|
cd my-project
|
|
8
10
|
ctxgraph-code setup
|
|
9
11
|
```
|
|
@@ -26,10 +28,10 @@ These questions require running multiple `grep` commands or reading dependency c
|
|
|
26
28
|
## Quick Start
|
|
27
29
|
|
|
28
30
|
```bash
|
|
29
|
-
# Install
|
|
30
|
-
pip install ctxgraph-code
|
|
31
|
+
# Install (add [full] for multi-language support)
|
|
32
|
+
pip install 'ctxgraph-code[full]'
|
|
31
33
|
|
|
32
|
-
# Navigate to your
|
|
34
|
+
# Navigate to your project
|
|
33
35
|
cd my-project
|
|
34
36
|
|
|
35
37
|
# One-command setup: init + build + configure Claude Code
|
|
@@ -54,7 +56,14 @@ Interactive walkthrough — prompts for:
|
|
|
54
56
|
Does everything in one step:
|
|
55
57
|
1. Creates `.ctxgraph/config.toml` with your chosen extensions and excludes
|
|
56
58
|
2. Installs the `/ctxgraph-code` slash command globally (works in every Claude Code session)
|
|
57
|
-
3. Builds the knowledge graph from all matching files
|
|
59
|
+
3. Builds the knowledge graph from all matching files — shows live per-graph progress:
|
|
60
|
+
```
|
|
61
|
+
Building 5 graphs with 8 workers...
|
|
62
|
+
✔ src/ (42 files, 156 nodes, 34 edges, 0.8s)
|
|
63
|
+
✔ api/ (18 files, 73 nodes, 12 edges, 0.4s)
|
|
64
|
+
✔ tests/ (31 files, 89 nodes, 0 edges, 0.6s)
|
|
65
|
+
Built all 5 graphs in 2.1s
|
|
66
|
+
```
|
|
58
67
|
|
|
59
68
|
Non-interactive mode:
|
|
60
69
|
```bash
|
|
@@ -96,6 +105,15 @@ Scans all matching files in the project, runs AST analysis. Extensions are read
|
|
|
96
105
|
- `--verbose` / `-v` — show per-file progress
|
|
97
106
|
- `--no-summary` — skip docstring extraction for faster builds
|
|
98
107
|
|
|
108
|
+
Shows live per-graph progress as each completes:
|
|
109
|
+
```
|
|
110
|
+
Building 5 graphs with 8 workers...
|
|
111
|
+
✔ src/ (42 files, 156 nodes, 34 edges, 0.8s)
|
|
112
|
+
✔ api/ (18 files, 73 nodes, 12 edges, 0.4s)
|
|
113
|
+
✔ tests/ (31 files, 89 nodes, 0 edges, 0.6s)
|
|
114
|
+
Built all 5 graphs in 2.1s
|
|
115
|
+
```
|
|
116
|
+
|
|
99
117
|
Stores graphs in `.ctxgraph/graphs/<dir>.db` (per-directory) or `.ctxgraph/graph.db` (combined).
|
|
100
118
|
|
|
101
119
|
> The graph is a **static snapshot**. If code changes, run `ctxgraph-code build` again to refresh. Use `--incremental` to only reprocess changed files.
|
|
@@ -199,10 +217,10 @@ ctxgraph-code probe "database connection pool"
|
|
|
199
217
|
ctxgraph-code probe "user authentication" --max 3
|
|
200
218
|
```
|
|
201
219
|
|
|
202
|
-
Searches the graph for relevant nodes **and reads the actual source code** inline. Claude gets paths + source in one command, saving 1–2 tool calls. Shows the first
|
|
220
|
+
Searches the graph for relevant nodes **and reads the actual source code** inline. Claude gets paths + source in one command, saving 1–2 tool calls. Shows the first N lines of matched files with automatic syntax highlighting per language.
|
|
203
221
|
|
|
204
222
|
- `--max` / `-m` — max files to probe (default: 5)
|
|
205
|
-
- `--context` / `-c` — lines
|
|
223
|
+
- `--context` / `-c` — lines to show per file (default: 40, use 0 for full file)
|
|
206
224
|
|
|
207
225
|
### `install-hooks`
|
|
208
226
|
|
|
@@ -291,7 +309,10 @@ This catches the common case where you edit a file after building the graph and
|
|
|
291
309
|
| **Multiprocessing** | Combined graphs split files across CPU cores via `multiprocessing.Pool` |
|
|
292
310
|
| **`--jobs`** | Control parallelism level (default: CPU count) |
|
|
293
311
|
| **Incremental builds** | `--incremental` caches file mtimes, only reprocesses changed files |
|
|
294
|
-
| **Trivial file skip** | `_quick_scan()` pre-checks
|
|
312
|
+
| **Trivial file skip** | `_quick_scan()` pre-checks all files (Python and non-Python) — skips full parse for files with no code |
|
|
313
|
+
| **`follow_symlinks` config** | Respects `follow_symlinks = false` setting to avoid duplicate/broken symlinks |
|
|
314
|
+
| **`max_file_size_mb` config** | Skips files exceeding the configured size limit before reading |
|
|
315
|
+
| **Live build progress** | Per-graph status + timing as each completes during parallel builds |
|
|
295
316
|
| **Cached excludes** | `lru_cache` on `should_exclude()` during `os.walk` |
|
|
296
317
|
| **Batch SQLite inserts** | `executemany` instead of per-row `INSERT` statements |
|
|
297
318
|
| **`--no-summary`** | Skips docstring extraction (fastest rebuilds) |
|
|
@@ -363,7 +384,7 @@ Built-in default exclusion patterns (always applied): `__pycache__`, `*.pyc`, `.
|
|
|
363
384
|
|
|
364
385
|
| Feature | ctxgraph | ctxgraph-code |
|
|
365
386
|
|---------|----------|---------------|
|
|
366
|
-
| CLI commands | 9+ |
|
|
387
|
+
| CLI commands | 9+ | 17 (init, build, query, deps, usedby, overview, symbols, context, setup, view, info, install-slash, build-status, probe, install-hooks, uninstall-hooks, version) |
|
|
367
388
|
| LLM integration | Built-in (Ollama, Claude, OpenAI, Azure) | None (delegates to Claude Code) |
|
|
368
389
|
| Chat sessions | Yes | No |
|
|
369
390
|
| Visualizer | D3.js HTML + SVG | D3.js HTML (`view` opens in browser, `--tree` for text) |
|
|
@@ -378,6 +399,7 @@ Built-in default exclusion patterns (always applied): `__pycache__`, `*.pyc`, `.
|
|
|
378
399
|
|
|
379
400
|
- Python 3.10+
|
|
380
401
|
- A Claude Code subscription (for the `/ctxgraph-code` slash command — the graph itself works standalone)
|
|
402
|
+
- For multi-language analysis (C, Go, Rust, JS, TS, Java, etc.): `pip install 'ctxgraph-code[full]'` to install tree-sitter support
|
|
381
403
|
|
|
382
404
|
---
|
|
383
405
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ctxgraph-code"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Code knowledge graph for Claude Code. Build a relationship graph of your Python codebase and query it during coding sessions."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -268,7 +268,12 @@ SLASH_COMMAND_TEMPLATE = """# ctxgraph-code: Code Relationship Graph
|
|
|
268
268
|
- `ctxgraph-code overview --dir <name>` -- Show project structure for a specific graph
|
|
269
269
|
- `ctxgraph-code symbols <path>` -- List classes/functions defined in a file
|
|
270
270
|
- `ctxgraph-code context "task"` -- Generate a focused context summary
|
|
271
|
+
- `ctxgraph-code subgraph "task"` -- Extract a focused subgraph with inline source for a task
|
|
272
|
+
- `ctxgraph-code diff` -- Compare graph with filesystem to find new/removed/changed files
|
|
273
|
+
- `ctxgraph-code mermaid --type classDiagram` -- Export graph as Mermaid diagram
|
|
271
274
|
- `ctxgraph-code view --dir <name>` -- Visualize a graph interactively
|
|
275
|
+
- `ctxgraph-code build-status` -- Show last build status and timing
|
|
276
|
+
- `ctxgraph-code install-slash` -- (Re)install this slash command
|
|
272
277
|
|
|
273
278
|
**Tip:** Use `--dir <name>` to scope queries to a per-directory graph.
|
|
274
279
|
When passing a file path like `auth/login.py`, the correct graph is auto-detected.
|
|
@@ -386,13 +391,13 @@ def _build_dirs_parallel(path, exts, user_patterns, top_dirs, graphs_dir, jobs,
|
|
|
386
391
|
edges = stats.get("total_edges", 0)
|
|
387
392
|
t = stats.get("elapsed_seconds", 0)
|
|
388
393
|
console.print(
|
|
389
|
-
f" [green]
|
|
394
|
+
f" [green]OK[/green] {label}/ "
|
|
390
395
|
f"({files} files, {nodes} nodes, {edges} edges, {t}s)"
|
|
391
396
|
)
|
|
392
397
|
except Exception as e:
|
|
393
398
|
results.append((label, str(e)))
|
|
394
399
|
err_count += 1
|
|
395
|
-
console.print(f" [red]
|
|
400
|
+
console.print(f" [red]FAIL[/red] {label}/ ([red]{e}[/red])")
|
|
396
401
|
|
|
397
402
|
elapsed_total = time.time() - _build_progress_start
|
|
398
403
|
if err_count:
|
|
@@ -819,7 +824,7 @@ def setup(
|
|
|
819
824
|
)
|
|
820
825
|
|
|
821
826
|
|
|
822
|
-
@app.command(name="build-status")
|
|
827
|
+
@app.command(name="build-status")
|
|
823
828
|
def build_status(
|
|
824
829
|
repo_path: Optional[str] = typer.Option(
|
|
825
830
|
None, "--repo", "-r", help="Repository path"
|
|
@@ -1105,10 +1110,300 @@ def version():
|
|
|
1105
1110
|
try:
|
|
1106
1111
|
ver = _v("ctxgraph-code")
|
|
1107
1112
|
except Exception:
|
|
1108
|
-
ver = "0.
|
|
1113
|
+
ver = "0.6.0"
|
|
1109
1114
|
console.print(f"ctxgraph-code version [bold]{ver}[/bold]")
|
|
1110
1115
|
|
|
1111
1116
|
|
|
1117
|
+
@app.command()
|
|
1118
|
+
def mermaid(
|
|
1119
|
+
repo_path: Optional[str] = typer.Option(
|
|
1120
|
+
None, "--repo", "-r", help="Repository path"
|
|
1121
|
+
),
|
|
1122
|
+
output: Optional[str] = typer.Option(
|
|
1123
|
+
None, "--output", "-o", help="Save Mermaid output to file"
|
|
1124
|
+
),
|
|
1125
|
+
max_nodes: int = typer.Option(
|
|
1126
|
+
50, "--max-nodes", "-n", help="Maximum nodes in diagram"
|
|
1127
|
+
),
|
|
1128
|
+
dir_name: Optional[str] = typer.Option(
|
|
1129
|
+
None, "--dir", "-d", help="Directory graph to use"
|
|
1130
|
+
),
|
|
1131
|
+
diagram_type: str = typer.Argument(
|
|
1132
|
+
"classDiagram", help="Diagram type: classDiagram, flowchart, sequence"
|
|
1133
|
+
),
|
|
1134
|
+
):
|
|
1135
|
+
"""Export the graph as a Mermaid diagram.
|
|
1136
|
+
|
|
1137
|
+
Supported diagram types: classDiagram, flowchart, sequence.
|
|
1138
|
+
Outputs to console by default, or to a file with --output.
|
|
1139
|
+
"""
|
|
1140
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
1141
|
+
storage = _resolve_storage(path, dir_name=dir_name)
|
|
1142
|
+
|
|
1143
|
+
from ctxgraph_code.render.mermaid import MermaidError, render_mermaid
|
|
1144
|
+
|
|
1145
|
+
try:
|
|
1146
|
+
result = render_mermaid(
|
|
1147
|
+
storage,
|
|
1148
|
+
output_type=diagram_type,
|
|
1149
|
+
max_nodes=max_nodes,
|
|
1150
|
+
)
|
|
1151
|
+
except MermaidError as e:
|
|
1152
|
+
console.print(f"[red]{e}[/red]")
|
|
1153
|
+
raise typer.Exit(1)
|
|
1154
|
+
|
|
1155
|
+
if output:
|
|
1156
|
+
out_path = Path(output)
|
|
1157
|
+
out_path.write_text(result, encoding="utf-8")
|
|
1158
|
+
console.print(f"[green]Mermaid diagram saved to [bold]{out_path}[/bold][/green]")
|
|
1159
|
+
else:
|
|
1160
|
+
console.print(result)
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
@app.command()
|
|
1164
|
+
def subgraph(
|
|
1165
|
+
query: str = typer.Argument(..., help="Task description"),
|
|
1166
|
+
repo_path: Optional[str] = typer.Option(
|
|
1167
|
+
None, "--repo", "-r", help="Repository path"
|
|
1168
|
+
),
|
|
1169
|
+
max_nodes: int = typer.Option(
|
|
1170
|
+
10, "--max-nodes", "-n", help="Maximum nodes in subgraph"
|
|
1171
|
+
),
|
|
1172
|
+
dir_name: Optional[str] = typer.Option(
|
|
1173
|
+
None, "--dir", "-d", help="Directory graph to query"
|
|
1174
|
+
),
|
|
1175
|
+
):
|
|
1176
|
+
"""Extract a focused subgraph relevant to a task description.
|
|
1177
|
+
|
|
1178
|
+
Returns matching nodes, their relationships, and inline source code
|
|
1179
|
+
for a compact context window.
|
|
1180
|
+
"""
|
|
1181
|
+
import hashlib
|
|
1182
|
+
|
|
1183
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
1184
|
+
storage = _resolve_storage(path, dir_name=dir_name)
|
|
1185
|
+
|
|
1186
|
+
results = search_relevant_nodes(storage, query, max_nodes=max_nodes)
|
|
1187
|
+
if not results:
|
|
1188
|
+
console.print("[yellow]No relevant nodes found for subgraph.[/yellow]")
|
|
1189
|
+
raise typer.Exit()
|
|
1190
|
+
|
|
1191
|
+
matched_ids = {n.id for n, _ in results}
|
|
1192
|
+
edge_ids: set[str] = set()
|
|
1193
|
+
edges = storage.get_edges_for_nodes(matched_ids)
|
|
1194
|
+
for e in edges:
|
|
1195
|
+
if e.source_id in matched_ids:
|
|
1196
|
+
edge_ids.add(e.target_id)
|
|
1197
|
+
if e.target_id in matched_ids:
|
|
1198
|
+
edge_ids.add(e.source_id)
|
|
1199
|
+
|
|
1200
|
+
all_ids = matched_ids | edge_ids
|
|
1201
|
+
node_map: dict[str, object] = {}
|
|
1202
|
+
for nid in all_ids:
|
|
1203
|
+
n = storage.get_node(nid)
|
|
1204
|
+
if n:
|
|
1205
|
+
node_map[nid] = n
|
|
1206
|
+
|
|
1207
|
+
file_nodes_in_subgraph = {
|
|
1208
|
+
n for n in node_map.values()
|
|
1209
|
+
if hasattr(n, 'type') and n.type == "file"
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
lines = [
|
|
1213
|
+
f"Subgraph for: {query}",
|
|
1214
|
+
f"Nodes: {len(matched_ids)} seed + {len(edge_ids)} related = {len(all_ids)} total",
|
|
1215
|
+
"",
|
|
1216
|
+
]
|
|
1217
|
+
|
|
1218
|
+
# Group by file
|
|
1219
|
+
file_groups: dict[str, list] = {}
|
|
1220
|
+
for n in sorted(node_map.values(), key=lambda x: x.path or ""):
|
|
1221
|
+
fp = getattr(n, 'path', None) or ""
|
|
1222
|
+
file_groups.setdefault(fp, []).append(n)
|
|
1223
|
+
|
|
1224
|
+
for fp, nodes in sorted(file_groups.items()):
|
|
1225
|
+
file_node = next((n for n in nodes if n.type == "file"), None)
|
|
1226
|
+
if file_node:
|
|
1227
|
+
tag = "F"
|
|
1228
|
+
lines.append(f" [{tag}] [bold]{file_node.name}[/bold] [blue]{file_node.path or '-'}[/blue]")
|
|
1229
|
+
if file_node.summary:
|
|
1230
|
+
lines.append(f" {file_node.summary}")
|
|
1231
|
+
symbols = [n for n in nodes if n.type in ("class", "function")]
|
|
1232
|
+
if symbols:
|
|
1233
|
+
for s in symbols:
|
|
1234
|
+
tag = "C" if s.type == "class" else "M"
|
|
1235
|
+
lines.append(f" [{tag}] {s.name} (line {s.lineno})")
|
|
1236
|
+
if s.summary:
|
|
1237
|
+
lines.append(f" {s.summary}")
|
|
1238
|
+
|
|
1239
|
+
if edges:
|
|
1240
|
+
lines.append("")
|
|
1241
|
+
lines.append(" Relationships:")
|
|
1242
|
+
for e in edges[:15]:
|
|
1243
|
+
src = node_map.get(e.source_id)
|
|
1244
|
+
tgt = node_map.get(e.target_id)
|
|
1245
|
+
src_name = src.name if src else e.source_id
|
|
1246
|
+
tgt_name = tgt.name if tgt else e.target_id
|
|
1247
|
+
lines.append(f" {src_name} --[{e.relation}]--> {tgt_name}")
|
|
1248
|
+
|
|
1249
|
+
# Inline source for each file node
|
|
1250
|
+
lines.append("")
|
|
1251
|
+
for n in sorted(file_nodes_in_subgraph, key=lambda x: x.path or ""):
|
|
1252
|
+
fp = path / n.path if n.path else None
|
|
1253
|
+
if fp and fp.is_file():
|
|
1254
|
+
try:
|
|
1255
|
+
code = fp.read_text(encoding="utf-8", errors="replace")
|
|
1256
|
+
lines.append(f" === {n.path} ===")
|
|
1257
|
+
snippet = code.splitlines()[:60]
|
|
1258
|
+
lines.extend(snippet)
|
|
1259
|
+
if len(code.splitlines()) > 60:
|
|
1260
|
+
lines.append(f"... ({len(code.splitlines()) - 60} more lines)")
|
|
1261
|
+
lines.append("")
|
|
1262
|
+
except OSError:
|
|
1263
|
+
pass
|
|
1264
|
+
|
|
1265
|
+
console.print("\n".join(lines))
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
@app.command()
|
|
1269
|
+
def diff(
|
|
1270
|
+
repo_path: Optional[str] = typer.Option(
|
|
1271
|
+
None, "--repo", "-r", help="Repository path"
|
|
1272
|
+
),
|
|
1273
|
+
dir_name: Optional[str] = typer.Option(
|
|
1274
|
+
None, "--dir", "-d", help="Directory graph to compare"
|
|
1275
|
+
),
|
|
1276
|
+
ref_branch: Optional[str] = typer.Option(
|
|
1277
|
+
None, "--ref", help="Git reference/branch to diff against (requires git)"
|
|
1278
|
+
),
|
|
1279
|
+
):
|
|
1280
|
+
"""Compare the graph with the filesystem.
|
|
1281
|
+
|
|
1282
|
+
Shows files that have been added, removed, or changed since the
|
|
1283
|
+
graph was built. Use --ref for a git-aware diff against a branch.
|
|
1284
|
+
"""
|
|
1285
|
+
import hashlib
|
|
1286
|
+
import json as j
|
|
1287
|
+
|
|
1288
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
1289
|
+
storage = _resolve_storage(path, dir_name=dir_name)
|
|
1290
|
+
|
|
1291
|
+
new_files: list[str] = []
|
|
1292
|
+
removed_files: list[str] = []
|
|
1293
|
+
changed_files: list[str] = []
|
|
1294
|
+
|
|
1295
|
+
graph_paths = set(storage.get_file_paths())
|
|
1296
|
+
hashes_json = storage.get_metadata("content_hashes")
|
|
1297
|
+
stored_hashes: dict[str, str] = {}
|
|
1298
|
+
if hashes_json:
|
|
1299
|
+
try:
|
|
1300
|
+
stored_hashes = j.loads(hashes_json)
|
|
1301
|
+
except (j.JSONDecodeError, OSError):
|
|
1302
|
+
pass
|
|
1303
|
+
|
|
1304
|
+
if ref_branch:
|
|
1305
|
+
try:
|
|
1306
|
+
import subprocess
|
|
1307
|
+
result = subprocess.run(
|
|
1308
|
+
["git", "diff", "--name-only", ref_branch],
|
|
1309
|
+
capture_output=True, text=True, check=False, cwd=str(path),
|
|
1310
|
+
)
|
|
1311
|
+
if result.returncode == 0:
|
|
1312
|
+
git_files = set(result.stdout.strip().splitlines()) if result.stdout.strip() else set()
|
|
1313
|
+
for gf in git_files:
|
|
1314
|
+
if gf not in graph_paths:
|
|
1315
|
+
new_files.append(gf)
|
|
1316
|
+
for gp in graph_paths:
|
|
1317
|
+
if gp not in git_files:
|
|
1318
|
+
removed_files.append(gp)
|
|
1319
|
+
# Changed files = git diff --name-only from HEAD
|
|
1320
|
+
head_result = subprocess.run(
|
|
1321
|
+
["git", "diff", "--name-only", "HEAD"],
|
|
1322
|
+
capture_output=True, text=True, check=False, cwd=str(path),
|
|
1323
|
+
)
|
|
1324
|
+
if head_result.returncode == 0:
|
|
1325
|
+
changed_files = [f for f in head_result.stdout.strip().splitlines() if f in graph_paths]
|
|
1326
|
+
else:
|
|
1327
|
+
console.print(f"[red]git diff failed: {result.stderr.strip()}[/red]")
|
|
1328
|
+
console.print("[yellow]Falling back to filesystem comparison...[/yellow]")
|
|
1329
|
+
ref_branch = None
|
|
1330
|
+
except FileNotFoundError:
|
|
1331
|
+
console.print("[yellow]Git not found. Falling back to filesystem comparison...[/yellow]")
|
|
1332
|
+
ref_branch = None
|
|
1333
|
+
|
|
1334
|
+
if not ref_branch:
|
|
1335
|
+
for gp in graph_paths:
|
|
1336
|
+
fp = path / gp
|
|
1337
|
+
if not fp.is_file():
|
|
1338
|
+
removed_files.append(gp)
|
|
1339
|
+
|
|
1340
|
+
src_extensions = {".py", ".js", ".ts", ".tsx", ".go", ".rs", ".c", ".h", ".cpp", ".java", ".rb", ".kt", ".swift"}
|
|
1341
|
+
for root, _dirs, files in os.walk(path):
|
|
1342
|
+
_dirs[:] = [d for d in _dirs if not d.startswith(".") and d not in ("node_modules", "venv", ".venv", "env", "dist", "build", "__pycache__")]
|
|
1343
|
+
for f in files:
|
|
1344
|
+
ext = os.path.splitext(f)[1].lower()
|
|
1345
|
+
if ext not in src_extensions:
|
|
1346
|
+
continue
|
|
1347
|
+
rel_path = os.path.relpath(os.path.join(root, f), path).replace("\\", "/")
|
|
1348
|
+
if rel_path not in graph_paths:
|
|
1349
|
+
new_files.append(rel_path)
|
|
1350
|
+
|
|
1351
|
+
# Content hash comparison for changed detection
|
|
1352
|
+
if stored_hashes:
|
|
1353
|
+
for rel_path, stored_hash in stored_hashes.items():
|
|
1354
|
+
fp = path / rel_path
|
|
1355
|
+
if fp.is_file():
|
|
1356
|
+
try:
|
|
1357
|
+
actual = hashlib.sha256(
|
|
1358
|
+
fp.read_text(encoding="utf-8", errors="replace")
|
|
1359
|
+
.encode("utf-8")
|
|
1360
|
+
).hexdigest()
|
|
1361
|
+
if actual != stored_hash:
|
|
1362
|
+
changed_files.append(rel_path)
|
|
1363
|
+
except OSError:
|
|
1364
|
+
changed_files.append(rel_path)
|
|
1365
|
+
|
|
1366
|
+
lines = ["Graph vs Filesystem Diff", ""]
|
|
1367
|
+
if new_files:
|
|
1368
|
+
lines.append(f" [green]+ {len(new_files)} new file(s):[/green]")
|
|
1369
|
+
for f in sorted(new_files)[:10]:
|
|
1370
|
+
lines.append(f" + {f}")
|
|
1371
|
+
if len(new_files) > 10:
|
|
1372
|
+
lines.append(f" ... and {len(new_files) - 10} more")
|
|
1373
|
+
else:
|
|
1374
|
+
lines.append(" [green]+ No new files[/green]")
|
|
1375
|
+
|
|
1376
|
+
if removed_files:
|
|
1377
|
+
lines.append(f"")
|
|
1378
|
+
lines.append(f" [red]- {len(removed_files)} removed file(s):[/red]")
|
|
1379
|
+
for f in sorted(removed_files)[:10]:
|
|
1380
|
+
lines.append(f" - {f}")
|
|
1381
|
+
if len(removed_files) > 10:
|
|
1382
|
+
lines.append(f" ... and {len(removed_files) - 10} more")
|
|
1383
|
+
else:
|
|
1384
|
+
lines.append(f"")
|
|
1385
|
+
lines.append(" [red]- No removed files[/red]")
|
|
1386
|
+
|
|
1387
|
+
if changed_files:
|
|
1388
|
+
lines.append(f"")
|
|
1389
|
+
lines.append(f" [yellow]~ {len(changed_files)} changed file(s):[/yellow]")
|
|
1390
|
+
for f in sorted(changed_files)[:10]:
|
|
1391
|
+
lines.append(f" ~ {f}")
|
|
1392
|
+
if len(changed_files) > 10:
|
|
1393
|
+
lines.append(f" ... and {len(changed_files) - 10} more")
|
|
1394
|
+
else:
|
|
1395
|
+
lines.append(f"")
|
|
1396
|
+
lines.append(" [yellow]~ No changed files[/yellow]")
|
|
1397
|
+
|
|
1398
|
+
lines.append("")
|
|
1399
|
+
if new_files or removed_files or changed_files:
|
|
1400
|
+
lines.append("[yellow]Run [bold]ctxgraph-code build --incremental[/bold] to update the graph.[/yellow]")
|
|
1401
|
+
else:
|
|
1402
|
+
lines.append("[green]Graph is up to date with the filesystem.[/green]")
|
|
1403
|
+
|
|
1404
|
+
console.print("\n".join(lines))
|
|
1405
|
+
|
|
1406
|
+
|
|
1112
1407
|
if __name__ == "__main__":
|
|
1113
1408
|
app()
|
|
1114
1409
|
|
|
@@ -144,7 +144,8 @@ def compute_hint_summary(repo_path: Path) -> Optional[str]:
|
|
|
144
144
|
)
|
|
145
145
|
|
|
146
146
|
lines.append(
|
|
147
|
-
"Use `ctxgraph-code probe \"<question>\"
|
|
147
|
+
"Use `ctxgraph-code probe \"<question>\"`, "
|
|
148
|
+
"`ctxgraph-code subgraph \"<task>\"`, or "
|
|
148
149
|
"`/ctxgraph-code` for help."
|
|
149
150
|
)
|
|
150
151
|
|
|
@@ -224,6 +224,42 @@ class Storage:
|
|
|
224
224
|
)
|
|
225
225
|
self.conn.commit()
|
|
226
226
|
|
|
227
|
+
def get_nodes_by_file_path(self, file_path: str) -> list[Node]:
|
|
228
|
+
rows = self.conn.execute(
|
|
229
|
+
"SELECT * FROM nodes WHERE path = ?", (file_path,)
|
|
230
|
+
).fetchall()
|
|
231
|
+
return [
|
|
232
|
+
Node(
|
|
233
|
+
id=r["id"],
|
|
234
|
+
type=r["type"],
|
|
235
|
+
name=r["name"],
|
|
236
|
+
path=r["path"],
|
|
237
|
+
parent_id=r["parent_id"],
|
|
238
|
+
summary=r["summary"],
|
|
239
|
+
importance=r["importance"],
|
|
240
|
+
size_bytes=r["size_bytes"],
|
|
241
|
+
lineno=r["lineno"],
|
|
242
|
+
)
|
|
243
|
+
for r in rows
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
def get_file_paths(self) -> list[str]:
|
|
247
|
+
rows = self.conn.execute(
|
|
248
|
+
"SELECT DISTINCT path FROM nodes WHERE type = 'file' AND path IS NOT NULL"
|
|
249
|
+
).fetchall()
|
|
250
|
+
return [r[0] for r in rows]
|
|
251
|
+
|
|
252
|
+
def get_content_hash(self, file_path: str) -> Optional[str]:
|
|
253
|
+
hashes_json = self.get_metadata("content_hashes")
|
|
254
|
+
if not hashes_json:
|
|
255
|
+
return None
|
|
256
|
+
try:
|
|
257
|
+
import json
|
|
258
|
+
stored = json.loads(hashes_json)
|
|
259
|
+
return stored.get(file_path)
|
|
260
|
+
except (json.JSONDecodeError, OSError):
|
|
261
|
+
return None
|
|
262
|
+
|
|
227
263
|
def save_metadata(self, key: str, value: str):
|
|
228
264
|
self.conn.execute(
|
|
229
265
|
"INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from ctxgraph_code.render._text import (
|
|
2
|
+
render_context,
|
|
3
|
+
render_deps,
|
|
4
|
+
render_overview,
|
|
5
|
+
render_symbols,
|
|
6
|
+
render_treeview,
|
|
7
|
+
render_usedby,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from ctxgraph_code.render.mermaid import render_mermaid as render_mermaid
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"render_context",
|
|
14
|
+
"render_deps",
|
|
15
|
+
"render_overview",
|
|
16
|
+
"render_symbols",
|
|
17
|
+
"render_treeview",
|
|
18
|
+
"render_usedby",
|
|
19
|
+
"render_mermaid",
|
|
20
|
+
]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from ctxgraph_code.graph.storage import Storage
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MermaidError(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def render_mermaid(
|
|
13
|
+
storage: Storage,
|
|
14
|
+
output_type: str = "classDiagram",
|
|
15
|
+
max_nodes: int = 50,
|
|
16
|
+
) -> str:
|
|
17
|
+
output_type = output_type or "classDiagram"
|
|
18
|
+
supported = {"classDiagram", "flowchart", "sequence"}
|
|
19
|
+
if output_type not in supported:
|
|
20
|
+
raise MermaidError(
|
|
21
|
+
f"Unsupported diagram type '{output_type}'. "
|
|
22
|
+
f"Choose from: {', '.join(sorted(supported))}"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
all_nodes = storage.get_all_nodes()
|
|
26
|
+
all_edges = storage.get_all_edges()
|
|
27
|
+
|
|
28
|
+
if output_type == "classDiagram":
|
|
29
|
+
return _render_class_diagram(all_nodes, all_edges, max_nodes)
|
|
30
|
+
elif output_type == "flowchart":
|
|
31
|
+
return _render_flowchart(all_nodes, all_edges, max_nodes)
|
|
32
|
+
elif output_type == "sequence":
|
|
33
|
+
return _render_sequence(all_nodes, all_edges, max_nodes)
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _safe_id(name: str) -> str:
|
|
38
|
+
safe = "".join(c if c.isalnum() or c == "_" else "_" for c in name)
|
|
39
|
+
if safe and safe[0].isdigit():
|
|
40
|
+
safe = "n" + safe
|
|
41
|
+
return safe or "node"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _render_class_diagram(nodes: list, edges: list, max_nodes: int) -> str:
|
|
45
|
+
lines = ["classDiagram"]
|
|
46
|
+
added = 0
|
|
47
|
+
|
|
48
|
+
class_nodes = [n for n in nodes if n.type == "class"]
|
|
49
|
+
file_nodes = [n for n in nodes if n.type == "file"]
|
|
50
|
+
|
|
51
|
+
for cls in class_nodes[:max_nodes]:
|
|
52
|
+
nid = _safe_id(cls.name)
|
|
53
|
+
lines.append(f" class {nid} {{")
|
|
54
|
+
if cls.summary:
|
|
55
|
+
lines.append(f" +{cls.summary}")
|
|
56
|
+
lines.append(" }")
|
|
57
|
+
added += 1
|
|
58
|
+
if added >= max_nodes:
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
if added < max_nodes:
|
|
62
|
+
remaining = max_nodes - added
|
|
63
|
+
for f in file_nodes[:remaining]:
|
|
64
|
+
nid = _safe_id(f.name)
|
|
65
|
+
label = f.path or f.name
|
|
66
|
+
lines.append(f" class {nid} {{")
|
|
67
|
+
lines.append(f" +File: {label}")
|
|
68
|
+
if f.summary:
|
|
69
|
+
lines.append(f" +{f.summary}")
|
|
70
|
+
lines.append(" }")
|
|
71
|
+
|
|
72
|
+
inheritance_edges = [e for e in edges if e.relation == "inherits"]
|
|
73
|
+
for e in inheritance_edges:
|
|
74
|
+
src_name = _find_node_name(nodes, e.source_id)
|
|
75
|
+
tgt_name = _find_node_name(nodes, e.target_id)
|
|
76
|
+
if src_name and tgt_name:
|
|
77
|
+
lines.append(f" {_safe_id(src_name)} --|> {_safe_id(tgt_name)}")
|
|
78
|
+
|
|
79
|
+
lines.append("")
|
|
80
|
+
return "\n".join(lines)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _render_flowchart(nodes: list, edges: list, max_nodes: int) -> str:
|
|
84
|
+
lines = ["flowchart TD"]
|
|
85
|
+
node_map = {n.id: n for n in nodes}
|
|
86
|
+
shown: set[str] = set()
|
|
87
|
+
|
|
88
|
+
sorted_edges = sorted(edges, key=lambda e: e.weight or 1.0, reverse=True)
|
|
89
|
+
for e in sorted_edges[:max_nodes * 2]:
|
|
90
|
+
src = node_map.get(e.source_id)
|
|
91
|
+
tgt = node_map.get(e.target_id)
|
|
92
|
+
if not src or not tgt:
|
|
93
|
+
continue
|
|
94
|
+
for n in (src, tgt):
|
|
95
|
+
if n.id not in shown and len(shown) < max_nodes:
|
|
96
|
+
nid = _safe_id(n.name)
|
|
97
|
+
label = n.path or n.name
|
|
98
|
+
lines.append(f" {nid}[\"{label}\"]")
|
|
99
|
+
shown.add(n.id)
|
|
100
|
+
if src.id in shown and tgt.id in shown:
|
|
101
|
+
src_id = _safe_id(src.name)
|
|
102
|
+
tgt_id = _safe_id(tgt.name)
|
|
103
|
+
rel = e.relation or "link"
|
|
104
|
+
lines.append(f" {src_id} -->|{rel}| {tgt_id}")
|
|
105
|
+
|
|
106
|
+
if not shown and nodes:
|
|
107
|
+
for n in nodes[:max_nodes]:
|
|
108
|
+
nid = _safe_id(n.name)
|
|
109
|
+
label = n.path or n.name
|
|
110
|
+
lines.append(f" {nid}[\"{label}\"]")
|
|
111
|
+
|
|
112
|
+
lines.append("")
|
|
113
|
+
return "\n".join(lines)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _render_sequence(nodes: list, edges: list, max_nodes: int) -> str:
|
|
117
|
+
lines = ["sequenceDiagram"]
|
|
118
|
+
participants: set[str] = set()
|
|
119
|
+
node_map = {n.id: n for n in nodes}
|
|
120
|
+
|
|
121
|
+
sorted_edges = sorted(edges, key=lambda e: e.weight or 1.0, reverse=True)
|
|
122
|
+
for e in sorted_edges[:max_nodes * 2]:
|
|
123
|
+
src = node_map.get(e.source_id)
|
|
124
|
+
tgt = node_map.get(e.target_id)
|
|
125
|
+
if not src or not tgt:
|
|
126
|
+
continue
|
|
127
|
+
for label, n in [("", src), ("", tgt)]:
|
|
128
|
+
if n.id not in participants:
|
|
129
|
+
safe = _safe_id(n.name)
|
|
130
|
+
display = n.path or n.name
|
|
131
|
+
lines.append(f" participant {safe} as \"{display}\"")
|
|
132
|
+
participants.add(n.id)
|
|
133
|
+
src_safe = _safe_id(src.name)
|
|
134
|
+
tgt_safe = _safe_id(tgt.name)
|
|
135
|
+
rel = e.relation or "calls"
|
|
136
|
+
lines.append(f" {src_safe}->>+{tgt_safe}: {rel}")
|
|
137
|
+
|
|
138
|
+
lines.append("")
|
|
139
|
+
return "\n".join(lines)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _find_node_name(nodes: list, node_id: str) -> Optional[str]:
|
|
143
|
+
for n in nodes:
|
|
144
|
+
if n.id == node_id:
|
|
145
|
+
return n.name
|
|
146
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctxgraph-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Code knowledge graph for Claude Code. Build a relationship graph of your Python codebase and query it during coding sessions.
|
|
5
5
|
Author: ctxgraph-code contributors
|
|
6
6
|
License: MIT
|
|
@@ -30,6 +30,8 @@ Requires-Dist: pytest>=7.0; extra == "dev"
|
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
pip install ctxgraph-code
|
|
33
|
+
# For multi-language support (C, Go, Rust, JS, TS, Java, etc.):
|
|
34
|
+
pip install 'ctxgraph-code[full]'
|
|
33
35
|
cd my-project
|
|
34
36
|
ctxgraph-code setup
|
|
35
37
|
```
|
|
@@ -52,10 +54,10 @@ These questions require running multiple `grep` commands or reading dependency c
|
|
|
52
54
|
## Quick Start
|
|
53
55
|
|
|
54
56
|
```bash
|
|
55
|
-
# Install
|
|
56
|
-
pip install ctxgraph-code
|
|
57
|
+
# Install (add [full] for multi-language support)
|
|
58
|
+
pip install 'ctxgraph-code[full]'
|
|
57
59
|
|
|
58
|
-
# Navigate to your
|
|
60
|
+
# Navigate to your project
|
|
59
61
|
cd my-project
|
|
60
62
|
|
|
61
63
|
# One-command setup: init + build + configure Claude Code
|
|
@@ -80,7 +82,14 @@ Interactive walkthrough — prompts for:
|
|
|
80
82
|
Does everything in one step:
|
|
81
83
|
1. Creates `.ctxgraph/config.toml` with your chosen extensions and excludes
|
|
82
84
|
2. Installs the `/ctxgraph-code` slash command globally (works in every Claude Code session)
|
|
83
|
-
3. Builds the knowledge graph from all matching files
|
|
85
|
+
3. Builds the knowledge graph from all matching files — shows live per-graph progress:
|
|
86
|
+
```
|
|
87
|
+
Building 5 graphs with 8 workers...
|
|
88
|
+
✔ src/ (42 files, 156 nodes, 34 edges, 0.8s)
|
|
89
|
+
✔ api/ (18 files, 73 nodes, 12 edges, 0.4s)
|
|
90
|
+
✔ tests/ (31 files, 89 nodes, 0 edges, 0.6s)
|
|
91
|
+
Built all 5 graphs in 2.1s
|
|
92
|
+
```
|
|
84
93
|
|
|
85
94
|
Non-interactive mode:
|
|
86
95
|
```bash
|
|
@@ -122,6 +131,15 @@ Scans all matching files in the project, runs AST analysis. Extensions are read
|
|
|
122
131
|
- `--verbose` / `-v` — show per-file progress
|
|
123
132
|
- `--no-summary` — skip docstring extraction for faster builds
|
|
124
133
|
|
|
134
|
+
Shows live per-graph progress as each completes:
|
|
135
|
+
```
|
|
136
|
+
Building 5 graphs with 8 workers...
|
|
137
|
+
✔ src/ (42 files, 156 nodes, 34 edges, 0.8s)
|
|
138
|
+
✔ api/ (18 files, 73 nodes, 12 edges, 0.4s)
|
|
139
|
+
✔ tests/ (31 files, 89 nodes, 0 edges, 0.6s)
|
|
140
|
+
Built all 5 graphs in 2.1s
|
|
141
|
+
```
|
|
142
|
+
|
|
125
143
|
Stores graphs in `.ctxgraph/graphs/<dir>.db` (per-directory) or `.ctxgraph/graph.db` (combined).
|
|
126
144
|
|
|
127
145
|
> The graph is a **static snapshot**. If code changes, run `ctxgraph-code build` again to refresh. Use `--incremental` to only reprocess changed files.
|
|
@@ -225,10 +243,10 @@ ctxgraph-code probe "database connection pool"
|
|
|
225
243
|
ctxgraph-code probe "user authentication" --max 3
|
|
226
244
|
```
|
|
227
245
|
|
|
228
|
-
Searches the graph for relevant nodes **and reads the actual source code** inline. Claude gets paths + source in one command, saving 1–2 tool calls. Shows the first
|
|
246
|
+
Searches the graph for relevant nodes **and reads the actual source code** inline. Claude gets paths + source in one command, saving 1–2 tool calls. Shows the first N lines of matched files with automatic syntax highlighting per language.
|
|
229
247
|
|
|
230
248
|
- `--max` / `-m` — max files to probe (default: 5)
|
|
231
|
-
- `--context` / `-c` — lines
|
|
249
|
+
- `--context` / `-c` — lines to show per file (default: 40, use 0 for full file)
|
|
232
250
|
|
|
233
251
|
### `install-hooks`
|
|
234
252
|
|
|
@@ -317,7 +335,10 @@ This catches the common case where you edit a file after building the graph and
|
|
|
317
335
|
| **Multiprocessing** | Combined graphs split files across CPU cores via `multiprocessing.Pool` |
|
|
318
336
|
| **`--jobs`** | Control parallelism level (default: CPU count) |
|
|
319
337
|
| **Incremental builds** | `--incremental` caches file mtimes, only reprocesses changed files |
|
|
320
|
-
| **Trivial file skip** | `_quick_scan()` pre-checks
|
|
338
|
+
| **Trivial file skip** | `_quick_scan()` pre-checks all files (Python and non-Python) — skips full parse for files with no code |
|
|
339
|
+
| **`follow_symlinks` config** | Respects `follow_symlinks = false` setting to avoid duplicate/broken symlinks |
|
|
340
|
+
| **`max_file_size_mb` config** | Skips files exceeding the configured size limit before reading |
|
|
341
|
+
| **Live build progress** | Per-graph status + timing as each completes during parallel builds |
|
|
321
342
|
| **Cached excludes** | `lru_cache` on `should_exclude()` during `os.walk` |
|
|
322
343
|
| **Batch SQLite inserts** | `executemany` instead of per-row `INSERT` statements |
|
|
323
344
|
| **`--no-summary`** | Skips docstring extraction (fastest rebuilds) |
|
|
@@ -389,7 +410,7 @@ Built-in default exclusion patterns (always applied): `__pycache__`, `*.pyc`, `.
|
|
|
389
410
|
|
|
390
411
|
| Feature | ctxgraph | ctxgraph-code |
|
|
391
412
|
|---------|----------|---------------|
|
|
392
|
-
| CLI commands | 9+ |
|
|
413
|
+
| CLI commands | 9+ | 17 (init, build, query, deps, usedby, overview, symbols, context, setup, view, info, install-slash, build-status, probe, install-hooks, uninstall-hooks, version) |
|
|
393
414
|
| LLM integration | Built-in (Ollama, Claude, OpenAI, Azure) | None (delegates to Claude Code) |
|
|
394
415
|
| Chat sessions | Yes | No |
|
|
395
416
|
| Visualizer | D3.js HTML + SVG | D3.js HTML (`view` opens in browser, `--tree` for text) |
|
|
@@ -404,6 +425,7 @@ Built-in default exclusion patterns (always applied): `__pycache__`, `*.pyc`, `.
|
|
|
404
425
|
|
|
405
426
|
- Python 3.10+
|
|
406
427
|
- A Claude Code subscription (for the `/ctxgraph-code` slash command — the graph itself works standalone)
|
|
428
|
+
- For multi-language analysis (C, Go, Rust, JS, TS, Java, etc.): `pip install 'ctxgraph-code[full]'` to install tree-sitter support
|
|
407
429
|
|
|
408
430
|
---
|
|
409
431
|
|
|
@@ -3,7 +3,6 @@ pyproject.toml
|
|
|
3
3
|
src/ctxgraph_code/__init__.py
|
|
4
4
|
src/ctxgraph_code/__main__.py
|
|
5
5
|
src/ctxgraph_code/cli.py
|
|
6
|
-
src/ctxgraph_code/render.py
|
|
7
6
|
src/ctxgraph_code.egg-info/PKG-INFO
|
|
8
7
|
src/ctxgraph_code.egg-info/SOURCES.txt
|
|
9
8
|
src/ctxgraph_code.egg-info/dependency_links.txt
|
|
@@ -31,5 +30,8 @@ src/ctxgraph_code/graph/builder.py
|
|
|
31
30
|
src/ctxgraph_code/graph/models.py
|
|
32
31
|
src/ctxgraph_code/graph/query.py
|
|
33
32
|
src/ctxgraph_code/graph/storage.py
|
|
33
|
+
src/ctxgraph_code/render/__init__.py
|
|
34
|
+
src/ctxgraph_code/render/_text.py
|
|
35
|
+
src/ctxgraph_code/render/mermaid.py
|
|
34
36
|
src/ctxgraph_code/view/__init__.py
|
|
35
37
|
src/ctxgraph_code/view/visualizer.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/__init__.py
RENAMED
|
File without changes
|
{ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/analyzer.py
RENAMED
|
File without changes
|
{ctxgraph_code-0.5.0 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/languages.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|