ctxgraph-code 0.1.1__tar.gz → 0.1.3__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.1.1 → ctxgraph_code-0.1.3}/PKG-INFO +18 -3
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/README.md +17 -2
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/pyproject.toml +1 -1
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/cli.py +53 -1
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/render.py +101 -0
- ctxgraph_code-0.1.3/src/ctxgraph_code/view/__init__.py +0 -0
- ctxgraph_code-0.1.3/src/ctxgraph_code/view/visualizer.py +288 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code.egg-info/PKG-INFO +18 -3
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code.egg-info/SOURCES.txt +3 -1
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/setup.cfg +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/__init__.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/__main__.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/analyzers/__init__.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/analyzers/python/__init__.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/analyzers/python/importer.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/analyzers/python/semantic.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/analyzers/python/symbols.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/config/__init__.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/config/init.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/config/settings.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/exclude/__init__.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/exclude/patterns.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/graph/__init__.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/graph/builder.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/graph/models.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/graph/query.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/graph/storage.py +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code.egg-info/dependency_links.txt +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code.egg-info/entry_points.txt +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code.egg-info/requires.txt +0 -0
- {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/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.1.
|
|
3
|
+
Version: 0.1.3
|
|
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
|
|
@@ -162,6 +162,21 @@ ctxgraph-code context "add pagination to the users endpoint"
|
|
|
162
162
|
|
|
163
163
|
Generates a focused context summary: relevant files, their symbols, and dependency/call edges between them. This is the closest equivalent to `ctxgraph`'s capsule format.
|
|
164
164
|
|
|
165
|
+
### `view`
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
ctxgraph-code view # generates interactive D3.js HTML and opens browser
|
|
169
|
+
ctxgraph-code view --no-open # generate HTML without opening browser
|
|
170
|
+
ctxgraph-code view --tree # show text tree instead (useful in terminal)
|
|
171
|
+
ctxgraph-code view --output graph.html # save to custom path
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Opens an **interactive D3.js force-directed graph** in the browser. Drag nodes, zoom/pan, search by name, filter by type (File/Class/Function). Hover to highlight connected nodes and see summaries.
|
|
175
|
+
|
|
176
|
+
The HTML is self-contained (loads D3.js from CDN) and saved to `.ctxgraph/graph.html`.
|
|
177
|
+
|
|
178
|
+
Use `--tree` for a terminal-friendly text view of the directory hierarchy with symbols and edges.
|
|
179
|
+
|
|
165
180
|
### `info`
|
|
166
181
|
|
|
167
182
|
```bash
|
|
@@ -272,10 +287,10 @@ Built-in default exclusion patterns (always applied): `__pycache__`, `*.pyc`, `.
|
|
|
272
287
|
|
|
273
288
|
| Feature | ctxgraph | ctxgraph-code |
|
|
274
289
|
|---------|----------|---------------|
|
|
275
|
-
| CLI commands | 9 (build, capsule, query, view, serve, info, init, ask, chat, history, skill) |
|
|
290
|
+
| CLI commands | 9 (build, capsule, query, view, serve, info, init, ask, chat, history, skill) | 9 (init, build, query, deps, usedby, overview, symbols, context, setup, view, info) |
|
|
276
291
|
| LLM integration | Built-in (Ollama, Claude, OpenAI, Azure) | None (delegates to Claude Code) |
|
|
277
292
|
| Chat sessions | Yes | No |
|
|
278
|
-
| Visualizer | D3.js HTML + SVG |
|
|
293
|
+
| Visualizer | D3.js HTML + SVG | D3.js HTML (`view` opens in browser, `--tree` for text) |
|
|
279
294
|
| Skills system | Yes (customizable skill TOML files) | No |
|
|
280
295
|
| MCP server | Yes | No |
|
|
281
296
|
| Token savings | Yes (capsule DSL compression) | No |
|
|
@@ -139,6 +139,21 @@ ctxgraph-code context "add pagination to the users endpoint"
|
|
|
139
139
|
|
|
140
140
|
Generates a focused context summary: relevant files, their symbols, and dependency/call edges between them. This is the closest equivalent to `ctxgraph`'s capsule format.
|
|
141
141
|
|
|
142
|
+
### `view`
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
ctxgraph-code view # generates interactive D3.js HTML and opens browser
|
|
146
|
+
ctxgraph-code view --no-open # generate HTML without opening browser
|
|
147
|
+
ctxgraph-code view --tree # show text tree instead (useful in terminal)
|
|
148
|
+
ctxgraph-code view --output graph.html # save to custom path
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Opens an **interactive D3.js force-directed graph** in the browser. Drag nodes, zoom/pan, search by name, filter by type (File/Class/Function). Hover to highlight connected nodes and see summaries.
|
|
152
|
+
|
|
153
|
+
The HTML is self-contained (loads D3.js from CDN) and saved to `.ctxgraph/graph.html`.
|
|
154
|
+
|
|
155
|
+
Use `--tree` for a terminal-friendly text view of the directory hierarchy with symbols and edges.
|
|
156
|
+
|
|
142
157
|
### `info`
|
|
143
158
|
|
|
144
159
|
```bash
|
|
@@ -249,10 +264,10 @@ Built-in default exclusion patterns (always applied): `__pycache__`, `*.pyc`, `.
|
|
|
249
264
|
|
|
250
265
|
| Feature | ctxgraph | ctxgraph-code |
|
|
251
266
|
|---------|----------|---------------|
|
|
252
|
-
| CLI commands | 9 (build, capsule, query, view, serve, info, init, ask, chat, history, skill) |
|
|
267
|
+
| CLI commands | 9 (build, capsule, query, view, serve, info, init, ask, chat, history, skill) | 9 (init, build, query, deps, usedby, overview, symbols, context, setup, view, info) |
|
|
253
268
|
| LLM integration | Built-in (Ollama, Claude, OpenAI, Azure) | None (delegates to Claude Code) |
|
|
254
269
|
| Chat sessions | Yes | No |
|
|
255
|
-
| Visualizer | D3.js HTML + SVG |
|
|
270
|
+
| Visualizer | D3.js HTML + SVG | D3.js HTML (`view` opens in browser, `--tree` for text) |
|
|
256
271
|
| Skills system | Yes (customizable skill TOML files) | No |
|
|
257
272
|
| MCP server | Yes | No |
|
|
258
273
|
| Token savings | Yes (capsule DSL compression) | No |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ctxgraph-code"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
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"}
|
|
@@ -350,6 +350,58 @@ def setup(
|
|
|
350
350
|
console.print("Open Claude Code in this project and type [bold]/ctxgraph-code[/bold] to get started.")
|
|
351
351
|
|
|
352
352
|
|
|
353
|
+
@app.command()
|
|
354
|
+
def view(
|
|
355
|
+
repo_path: Optional[str] = typer.Option(
|
|
356
|
+
None, "--repo", "-r", help="Repository path"
|
|
357
|
+
),
|
|
358
|
+
output: Optional[str] = typer.Option(
|
|
359
|
+
None, "--output", "-o", help="Save graph HTML to file"
|
|
360
|
+
),
|
|
361
|
+
no_open: bool = typer.Option(
|
|
362
|
+
False, "--no-open", help="Generate HTML but don't open browser"
|
|
363
|
+
),
|
|
364
|
+
tree: bool = typer.Option(
|
|
365
|
+
False, "--tree", help="Show text tree instead of interactive graph"
|
|
366
|
+
),
|
|
367
|
+
):
|
|
368
|
+
"""Open an interactive D3.js graph in the browser."""
|
|
369
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
370
|
+
|
|
371
|
+
storage = get_storage(path)
|
|
372
|
+
if storage is None:
|
|
373
|
+
console.print("[red]No graph found. Run [bold]ctxgraph-code build[/bold] first.[/red]")
|
|
374
|
+
raise typer.Exit(1)
|
|
375
|
+
|
|
376
|
+
if tree:
|
|
377
|
+
from ctxgraph_code.render import render_treeview
|
|
378
|
+
text = render_treeview(storage)
|
|
379
|
+
if output:
|
|
380
|
+
out_path = Path(output)
|
|
381
|
+
out_path.write_text(text, encoding="utf-8")
|
|
382
|
+
console.print(f"Saved tree to [bold]{out_path}[/bold]")
|
|
383
|
+
else:
|
|
384
|
+
console.print(text)
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
from ctxgraph_code.view.visualizer import render_view
|
|
388
|
+
html = render_view(storage)
|
|
389
|
+
|
|
390
|
+
graph_dir = path / ".ctxgraph"
|
|
391
|
+
graph_dir.mkdir(parents=True, exist_ok=True)
|
|
392
|
+
out_path = Path(output) if output else (graph_dir / "graph.html")
|
|
393
|
+
out_path.write_text(html, encoding="utf-8")
|
|
394
|
+
|
|
395
|
+
console.print(f"Graph saved to [bold]{out_path}[/bold]")
|
|
396
|
+
|
|
397
|
+
if not no_open:
|
|
398
|
+
import webbrowser
|
|
399
|
+
webbrowser.open(str(out_path.resolve()))
|
|
400
|
+
console.print("[green]Opened in browser.[/green]")
|
|
401
|
+
else:
|
|
402
|
+
console.print(f"Open {out_path} in a browser to view.")
|
|
403
|
+
|
|
404
|
+
|
|
353
405
|
@app.command()
|
|
354
406
|
def info(
|
|
355
407
|
repo_path: Optional[str] = typer.Option(
|
|
@@ -395,7 +447,7 @@ def version():
|
|
|
395
447
|
try:
|
|
396
448
|
ver = _v("ctxgraph-code")
|
|
397
449
|
except Exception:
|
|
398
|
-
ver = "0.1.
|
|
450
|
+
ver = "0.1.3"
|
|
399
451
|
console.print(f"ctxgraph-code version [bold]{ver}[/bold]")
|
|
400
452
|
|
|
401
453
|
|
|
@@ -235,6 +235,107 @@ def render_context(storage: Storage, query: str, max_nodes: int = 15) -> str:
|
|
|
235
235
|
return "\n".join(lines)
|
|
236
236
|
|
|
237
237
|
|
|
238
|
+
def render_treeview(storage: Storage) -> str:
|
|
239
|
+
all_nodes = storage.get_all_nodes()
|
|
240
|
+
all_edges = storage.get_all_edges()
|
|
241
|
+
|
|
242
|
+
file_nodes = sorted([n for n in all_nodes if n.type == "file"], key=lambda n: n.path or "")
|
|
243
|
+
symbol_map: dict[str, list[Node]] = {}
|
|
244
|
+
for n in all_nodes:
|
|
245
|
+
if n.type != "file":
|
|
246
|
+
symbol_map.setdefault(n.parent_id or "", []).append(n)
|
|
247
|
+
|
|
248
|
+
dir_tree: dict[str, dict] = {}
|
|
249
|
+
for node in file_nodes:
|
|
250
|
+
parts = (node.path or node.name).split("/")
|
|
251
|
+
for i in range(len(parts)):
|
|
252
|
+
parent = "/".join(parts[:i]) if i > 0 else "."
|
|
253
|
+
child = parts[i]
|
|
254
|
+
if parent not in dir_tree:
|
|
255
|
+
dir_tree[parent] = {"dirs": set(), "files": []}
|
|
256
|
+
if i == len(parts) - 1:
|
|
257
|
+
dir_tree[parent]["files"].append(node)
|
|
258
|
+
else:
|
|
259
|
+
dir_tree[parent]["dirs"].add(child)
|
|
260
|
+
|
|
261
|
+
stats = storage.stats()
|
|
262
|
+
bt = storage.get_metadata("build_time")
|
|
263
|
+
build_label = ""
|
|
264
|
+
if bt:
|
|
265
|
+
try:
|
|
266
|
+
from datetime import datetime
|
|
267
|
+
build_label = f" (built {datetime.fromtimestamp(float(bt)).strftime('%Y-%m-%d %H:%M')})"
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
lines = [
|
|
272
|
+
f".ctxgraph/graph.db ({stats['nodes']} nodes, {stats['edges']} edges){build_label}",
|
|
273
|
+
"",
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
def _render_dir(parent: str, prefix: str = ""):
|
|
277
|
+
entry = dir_tree.get(parent, {"dirs": set(), "files": []})
|
|
278
|
+
items: list[tuple[str, object]] = []
|
|
279
|
+
for d in sorted(entry["dirs"]):
|
|
280
|
+
items.append(("dir", d))
|
|
281
|
+
for f in sorted(entry["files"], key=lambda x: x.path or x.name):
|
|
282
|
+
items.append(("file", f))
|
|
283
|
+
|
|
284
|
+
for idx, (kind, obj) in enumerate(items):
|
|
285
|
+
last = idx == len(items) - 1
|
|
286
|
+
connector = "\\-- " if last else "+-- "
|
|
287
|
+
ext = " " if last else "| "
|
|
288
|
+
|
|
289
|
+
if kind == "dir":
|
|
290
|
+
name = str(obj)
|
|
291
|
+
lines.append(f"{prefix}{connector}{name}/")
|
|
292
|
+
child = name if parent == "." else parent + "/" + name
|
|
293
|
+
_render_dir(child, prefix + ext)
|
|
294
|
+
else:
|
|
295
|
+
node = obj
|
|
296
|
+
basename = (node.path or node.name).split("/")[-1]
|
|
297
|
+
lines.append(f"{prefix}{connector}{basename}")
|
|
298
|
+
symbols = sorted(symbol_map.get(node.id, []), key=lambda s: s.lineno)
|
|
299
|
+
if symbols:
|
|
300
|
+
sym_lines = []
|
|
301
|
+
for s in symbols:
|
|
302
|
+
tag = "C" if s.type == "class" else "M"
|
|
303
|
+
summary = f" -- {s.summary}" if s.summary else ""
|
|
304
|
+
sym_lines.append(f" [{tag}] {s.name}{summary}")
|
|
305
|
+
for sidx, sl in enumerate(sym_lines):
|
|
306
|
+
slast = sidx == len(sym_lines) - 1
|
|
307
|
+
sconn = "\\-- " if slast else "+-- "
|
|
308
|
+
lines.append(f"{prefix}{ext}{sconn}{sl}")
|
|
309
|
+
|
|
310
|
+
_render_dir(".")
|
|
311
|
+
lines.append("")
|
|
312
|
+
|
|
313
|
+
import_edges = [(s, t) for e in all_edges for s, t, r in [(e.source_id, e.target_id, e.relation)] if r == "imports"]
|
|
314
|
+
call_edges = [(s, t) for e in all_edges for s, t, r in [(e.source_id, e.target_id, e.relation)] if r == "calls"]
|
|
315
|
+
|
|
316
|
+
if import_edges:
|
|
317
|
+
lines.append(f"Imports ({len(import_edges)}):")
|
|
318
|
+
node_map = {n.id: n for n in all_nodes}
|
|
319
|
+
for src, tgt in sorted(import_edges, key=lambda x: (x[0], x[1]))[:20]:
|
|
320
|
+
sn = _short_name(src, node_map) or src
|
|
321
|
+
tn = _short_name(tgt, node_map) or tgt
|
|
322
|
+
lines.append(f" {sn} -> {tn}")
|
|
323
|
+
if len(import_edges) > 20:
|
|
324
|
+
lines.append(f" ... and {len(import_edges) - 20} more")
|
|
325
|
+
|
|
326
|
+
if call_edges:
|
|
327
|
+
lines.append(f"\nCalls ({len(call_edges)}):")
|
|
328
|
+
node_map = {n.id: n for n in all_nodes}
|
|
329
|
+
for src, tgt in sorted(call_edges, key=lambda x: (x[0], x[1]))[:15]:
|
|
330
|
+
sn = _short_name(src, node_map) or src
|
|
331
|
+
tn = _short_name(tgt, node_map) or tgt
|
|
332
|
+
lines.append(f" {sn} -> {tn}")
|
|
333
|
+
if len(call_edges) > 15:
|
|
334
|
+
lines.append(f" ... and {len(call_edges) - 15} more")
|
|
335
|
+
|
|
336
|
+
return "\n".join(lines)
|
|
337
|
+
|
|
338
|
+
|
|
238
339
|
def _short_name(node_id: str, node_map: dict[str, Node]) -> Optional[str]:
|
|
239
340
|
if node_id in node_map:
|
|
240
341
|
n = node_map[node_id]
|
|
File without changes
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from ctxgraph_code.graph.storage import Storage
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def render_view(storage: Storage) -> str:
|
|
9
|
+
nodes = storage.get_all_nodes()
|
|
10
|
+
edges = storage.get_all_edges()
|
|
11
|
+
|
|
12
|
+
file_nodes = [n for n in nodes if n.type == "file"]
|
|
13
|
+
symbol_nodes = [n for n in nodes if n.type != "file"]
|
|
14
|
+
|
|
15
|
+
graph_data: dict = {
|
|
16
|
+
"nodes": [],
|
|
17
|
+
"links": [],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
file_node_ids = {n.id for n in file_nodes}
|
|
21
|
+
|
|
22
|
+
for node in file_nodes:
|
|
23
|
+
graph_data["nodes"].append(
|
|
24
|
+
{
|
|
25
|
+
"id": node.id,
|
|
26
|
+
"label": _short_path(node.path or node.name),
|
|
27
|
+
"type": "file",
|
|
28
|
+
"summary": (node.summary or "")[:100],
|
|
29
|
+
"importance": node.importance,
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
for node in symbol_nodes:
|
|
34
|
+
graph_data["nodes"].append(
|
|
35
|
+
{
|
|
36
|
+
"id": node.id,
|
|
37
|
+
"label": node.name,
|
|
38
|
+
"type": node.type,
|
|
39
|
+
"summary": (node.summary or "")[:100],
|
|
40
|
+
"importance": node.importance,
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
seen_links = set()
|
|
45
|
+
for edge in edges:
|
|
46
|
+
if edge.source_id in file_node_ids or edge.target_id in file_node_ids:
|
|
47
|
+
key = (edge.source_id, edge.target_id)
|
|
48
|
+
if key not in seen_links:
|
|
49
|
+
seen_links.add(key)
|
|
50
|
+
graph_data["links"].append(
|
|
51
|
+
{
|
|
52
|
+
"source": edge.source_id,
|
|
53
|
+
"target": edge.target_id,
|
|
54
|
+
"relation": edge.relation,
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
json_data = json.dumps(graph_data, indent=2)
|
|
59
|
+
template = _get_html_template()
|
|
60
|
+
return template.replace("/* GRAPH_DATA */", json_data)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _short_path(path: str) -> str:
|
|
64
|
+
parts = path.split("/")
|
|
65
|
+
if len(parts) > 3:
|
|
66
|
+
return "/".join(parts[:2] + ["..."] + parts[-1:])
|
|
67
|
+
return path
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _get_html_template() -> str:
|
|
71
|
+
return """<!DOCTYPE html>
|
|
72
|
+
<html lang="en">
|
|
73
|
+
<head>
|
|
74
|
+
<meta charset="UTF-8">
|
|
75
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
76
|
+
<title>ctxgraph-code - Knowledge Graph</title>
|
|
77
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
78
|
+
<style>
|
|
79
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
80
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; overflow: hidden; }
|
|
81
|
+
#container { width: 100vw; height: 100vh; position: relative; }
|
|
82
|
+
svg { width: 100%; height: 100%; }
|
|
83
|
+
#toolbar { position: absolute; top: 16px; left: 16px; z-index: 10; display: flex; gap: 8px; align-items: center; background: #161b22; padding: 12px 16px; border-radius: 8px; border: 1px solid #30363d; }
|
|
84
|
+
#toolbar input { background: #0d1117; border: 1px solid #30363d; color: #c9d1d9; padding: 6px 12px; border-radius: 4px; width: 220px; font-size: 13px; }
|
|
85
|
+
#toolbar select { background: #0d1117; border: 1px solid #30363d; color: #c9d1d9; padding: 6px 8px; border-radius: 4px; font-size: 13px; }
|
|
86
|
+
#toolbar label { font-size: 13px; color: #8b949e; }
|
|
87
|
+
#legend { position: absolute; bottom: 16px; left: 16px; z-index: 10; background: #161b22; padding: 12px; border-radius: 8px; border: 1px solid #30363d; font-size: 12px; display: flex; gap: 16px; }
|
|
88
|
+
.legend-item { display: flex; align-items: center; gap: 6px; }
|
|
89
|
+
.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
|
|
90
|
+
#tooltip { position: absolute; z-index: 20; background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px; font-size: 13px; max-width: 400px; pointer-events: none; display: none; box-shadow: 0 4px 12px rgba(0,0,0,0.4); }
|
|
91
|
+
#tooltip .tt-name { font-weight: 600; color: #58a6ff; margin-bottom: 4px; }
|
|
92
|
+
#tooltip .tt-type { color: #8b949e; font-size: 11px; margin-bottom: 4px; }
|
|
93
|
+
#tooltip .tt-summary { color: #c9d1d9; font-size: 12px; }
|
|
94
|
+
#stats { position: absolute; bottom: 16px; right: 16px; z-index: 10; background: #161b22; padding: 8px 12px; border-radius: 8px; border: 1px solid #30363d; font-size: 11px; color: #8b949e; }
|
|
95
|
+
.link { stroke-opacity: 0.4; }
|
|
96
|
+
.link.imports { stroke: #58a6ff; }
|
|
97
|
+
.link.calls { stroke: #3fb950; }
|
|
98
|
+
.link.defines { stroke: #d29922; }
|
|
99
|
+
.link.extends { stroke: #bc8cff; }
|
|
100
|
+
.node { cursor: pointer; transition: opacity 0.2s; }
|
|
101
|
+
.node:hover { opacity: 0.8; }
|
|
102
|
+
.node-label { font-size: 11px; fill: #8b949e; pointer-events: none; text-shadow: 0 1px 2px #0d1117, 0 -1px 2px #0d1117, 1px 0 2px #0d1117, -1px 0 2px #0d1117; }
|
|
103
|
+
.node.highlighted .node-label { fill: #c9d1d9; font-weight: 600; }
|
|
104
|
+
.link.highlighted { stroke-opacity: 0.8; }
|
|
105
|
+
</style>
|
|
106
|
+
</head>
|
|
107
|
+
<body>
|
|
108
|
+
<div id="container">
|
|
109
|
+
<div id="toolbar">
|
|
110
|
+
<label>Search:</label>
|
|
111
|
+
<input type="text" id="search" placeholder="Search nodes..." oninput="filterGraph(this.value)">
|
|
112
|
+
<label>Filter:</label>
|
|
113
|
+
<select id="filterType" onchange="filterByType(this.value)">
|
|
114
|
+
<option value="all">All</option>
|
|
115
|
+
<option value="file">Files</option>
|
|
116
|
+
<option value="class">Classes</option>
|
|
117
|
+
<option value="function">Functions</option>
|
|
118
|
+
</select>
|
|
119
|
+
</div>
|
|
120
|
+
<div id="legend">
|
|
121
|
+
<div class="legend-item"><div class="legend-dot" style="background:#58a6ff"></div> File</div>
|
|
122
|
+
<div class="legend-item"><div class="legend-dot" style="background:#d29922"></div> Class</div>
|
|
123
|
+
<div class="legend-item"><div class="legend-dot" style="background:#3fb950"></div> Function</div>
|
|
124
|
+
<div class="legend-item"><svg width="20" height="2"><line x1="0" y1="1" x2="20" y2="1" stroke="#58a6ff" stroke-width="1.5" stroke-dasharray="4,2"/></svg> Import</div>
|
|
125
|
+
<div class="legend-item"><svg width="20" height="2"><line x1="0" y1="1" x2="20" y2="1" stroke="#3fb950" stroke-width="1.5" stroke-dasharray="2,2"/></svg> Call</div>
|
|
126
|
+
</div>
|
|
127
|
+
<div id="stats"></div>
|
|
128
|
+
<div id="tooltip">
|
|
129
|
+
<div class="tt-name"></div>
|
|
130
|
+
<div class="tt-type"></div>
|
|
131
|
+
<div class="tt-summary"></div>
|
|
132
|
+
</div>
|
|
133
|
+
<svg width="100%" height="100%"></svg>
|
|
134
|
+
</div>
|
|
135
|
+
<script>
|
|
136
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
137
|
+
const graphData = /* GRAPH_DATA */;
|
|
138
|
+
const width = window.innerWidth;
|
|
139
|
+
const height = window.innerHeight;
|
|
140
|
+
|
|
141
|
+
const svg = d3.select("svg");
|
|
142
|
+
svg.attr("width", width).attr("height", height);
|
|
143
|
+
svg.attr("viewBox", `0 0 ${width} ${height}`);
|
|
144
|
+
|
|
145
|
+
const g = svg.append("g");
|
|
146
|
+
|
|
147
|
+
const zoom = d3.zoom()
|
|
148
|
+
.scaleExtent([0.1, 4])
|
|
149
|
+
.on("zoom", (event) => g.attr("transform", event.transform));
|
|
150
|
+
|
|
151
|
+
svg.call(zoom);
|
|
152
|
+
|
|
153
|
+
const colorMap = { file: "#58a6ff", class: "#d29922", function: "#3fb950", module: "#bc8cff" };
|
|
154
|
+
|
|
155
|
+
const simulation = d3.forceSimulation(graphData.nodes)
|
|
156
|
+
.force("link", d3.forceLink(graphData.links).id(d => d.id).distance(d => {
|
|
157
|
+
return d.relation === "imports" ? 100 : 80;
|
|
158
|
+
}))
|
|
159
|
+
.force("charge", d3.forceManyBody().strength(-200))
|
|
160
|
+
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
161
|
+
.force("collision", d3.forceCollide(20));
|
|
162
|
+
|
|
163
|
+
const link = g.append("g")
|
|
164
|
+
.selectAll("line")
|
|
165
|
+
.data(graphData.links)
|
|
166
|
+
.join("line")
|
|
167
|
+
.attr("class", d => `link ${d.relation}`)
|
|
168
|
+
.attr("stroke-width", 1.5);
|
|
169
|
+
|
|
170
|
+
const node = g.append("g")
|
|
171
|
+
.selectAll("g")
|
|
172
|
+
.data(graphData.nodes)
|
|
173
|
+
.join("g")
|
|
174
|
+
.attr("class", "node")
|
|
175
|
+
.call(d3.drag()
|
|
176
|
+
.on("start", (event, d) => {
|
|
177
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
178
|
+
d.fx = d.x;
|
|
179
|
+
d.fy = d.y;
|
|
180
|
+
})
|
|
181
|
+
.on("drag", (event, d) => {
|
|
182
|
+
d.fx = event.x;
|
|
183
|
+
d.fy = event.y;
|
|
184
|
+
})
|
|
185
|
+
.on("end", (event, d) => {
|
|
186
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
187
|
+
d.fx = null;
|
|
188
|
+
d.fy = null;
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
node.append("circle")
|
|
193
|
+
.attr("r", d => 5 + (d.importance || 0.5) * 8)
|
|
194
|
+
.attr("fill", d => colorMap[d.type] || "#8b949e")
|
|
195
|
+
.attr("stroke", "#161b22")
|
|
196
|
+
.attr("stroke-width", 1.5);
|
|
197
|
+
|
|
198
|
+
node.append("text")
|
|
199
|
+
.attr("class", "node-label")
|
|
200
|
+
.text(d => d.label)
|
|
201
|
+
.attr("dx", d => 10 + (d.importance || 0.5) * 5)
|
|
202
|
+
.attr("dy", 4);
|
|
203
|
+
|
|
204
|
+
node.on("mouseover", function(event, d) {
|
|
205
|
+
const tt = d3.select("#tooltip");
|
|
206
|
+
tt.style("display", "block");
|
|
207
|
+
tt.select(".tt-name").text(d.label);
|
|
208
|
+
tt.select(".tt-type").text(`Type: ${d.type}`);
|
|
209
|
+
tt.select(".tt-summary").text(d.summary || "");
|
|
210
|
+
|
|
211
|
+
const connected = new Set();
|
|
212
|
+
graphData.links.forEach(l => {
|
|
213
|
+
const sid = typeof l.source === 'object' ? l.source.id : l.source;
|
|
214
|
+
const tid = typeof l.target === 'object' ? l.target.id : l.target;
|
|
215
|
+
if (sid === d.id) connected.add(tid);
|
|
216
|
+
if (tid === d.id) connected.add(sid);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
d3.selectAll(".node").each(function(n) {
|
|
220
|
+
if (n.id === d.id || connected.has(n.id)) {
|
|
221
|
+
d3.select(this).classed("highlighted", true);
|
|
222
|
+
d3.select(this).select("circle").attr("opacity", 1);
|
|
223
|
+
d3.select(this).select("text").attr("opacity", 1);
|
|
224
|
+
} else {
|
|
225
|
+
d3.select(this).select("circle").attr("opacity", 0.15);
|
|
226
|
+
d3.select(this).select("text").attr("opacity", 0.15);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
d3.selectAll(".link").each(function(l) {
|
|
231
|
+
const sid = typeof l.source === 'object' ? l.source.id : l.source;
|
|
232
|
+
const tid = typeof l.target === 'object' ? l.target.id : l.target;
|
|
233
|
+
if (sid === d.id || tid === d.id) {
|
|
234
|
+
d3.select(this).classed("highlighted", true);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
})
|
|
238
|
+
.on("mousemove", function(event) {
|
|
239
|
+
d3.select("#tooltip")
|
|
240
|
+
.style("left", (event.pageX + 12) + "px")
|
|
241
|
+
.style("top", (event.pageY - 10) + "px");
|
|
242
|
+
})
|
|
243
|
+
.on("mouseout", function() {
|
|
244
|
+
d3.select("#tooltip").style("display", "none");
|
|
245
|
+
d3.selectAll(".node").each(function(n) {
|
|
246
|
+
d3.select(this).classed("highlighted", false);
|
|
247
|
+
d3.select(this).select("circle").attr("opacity", 1);
|
|
248
|
+
d3.select(this).select("text").attr("opacity", 1);
|
|
249
|
+
});
|
|
250
|
+
d3.selectAll(".link").classed("highlighted", false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
simulation.on("tick", () => {
|
|
254
|
+
link
|
|
255
|
+
.attr("x1", d => d.source.x)
|
|
256
|
+
.attr("y1", d => d.source.y)
|
|
257
|
+
.attr("x2", d => d.target.x)
|
|
258
|
+
.attr("y2", d => d.target.y);
|
|
259
|
+
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
d3.select("#stats").text(`Nodes: ${graphData.nodes.length} | Edges: ${graphData.links.length}`);
|
|
263
|
+
|
|
264
|
+
window.filterGraph = function(query) {
|
|
265
|
+
const q = query.toLowerCase();
|
|
266
|
+
d3.selectAll(".node").each(function(d) {
|
|
267
|
+
const match = !q || d.label.toLowerCase().includes(q) || (d.summary && d.summary.toLowerCase().includes(q));
|
|
268
|
+
d3.select(this).style("display", match ? null : "none");
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
window.filterByType = function(type) {
|
|
273
|
+
d3.selectAll(".node").each(function(d) {
|
|
274
|
+
const match = type === "all" || d.type === type;
|
|
275
|
+
d3.select(this).style("display", match ? null : "none");
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
window.addEventListener("resize", () => {
|
|
280
|
+
const w = window.innerWidth;
|
|
281
|
+
const h = window.innerHeight;
|
|
282
|
+
svg.attr("width", w).attr("height", h);
|
|
283
|
+
svg.attr("viewBox", `0 0 ${w} ${h}`);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
</script>
|
|
287
|
+
</body>
|
|
288
|
+
</html>"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctxgraph-code
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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
|
|
@@ -162,6 +162,21 @@ ctxgraph-code context "add pagination to the users endpoint"
|
|
|
162
162
|
|
|
163
163
|
Generates a focused context summary: relevant files, their symbols, and dependency/call edges between them. This is the closest equivalent to `ctxgraph`'s capsule format.
|
|
164
164
|
|
|
165
|
+
### `view`
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
ctxgraph-code view # generates interactive D3.js HTML and opens browser
|
|
169
|
+
ctxgraph-code view --no-open # generate HTML without opening browser
|
|
170
|
+
ctxgraph-code view --tree # show text tree instead (useful in terminal)
|
|
171
|
+
ctxgraph-code view --output graph.html # save to custom path
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Opens an **interactive D3.js force-directed graph** in the browser. Drag nodes, zoom/pan, search by name, filter by type (File/Class/Function). Hover to highlight connected nodes and see summaries.
|
|
175
|
+
|
|
176
|
+
The HTML is self-contained (loads D3.js from CDN) and saved to `.ctxgraph/graph.html`.
|
|
177
|
+
|
|
178
|
+
Use `--tree` for a terminal-friendly text view of the directory hierarchy with symbols and edges.
|
|
179
|
+
|
|
165
180
|
### `info`
|
|
166
181
|
|
|
167
182
|
```bash
|
|
@@ -272,10 +287,10 @@ Built-in default exclusion patterns (always applied): `__pycache__`, `*.pyc`, `.
|
|
|
272
287
|
|
|
273
288
|
| Feature | ctxgraph | ctxgraph-code |
|
|
274
289
|
|---------|----------|---------------|
|
|
275
|
-
| CLI commands | 9 (build, capsule, query, view, serve, info, init, ask, chat, history, skill) |
|
|
290
|
+
| CLI commands | 9 (build, capsule, query, view, serve, info, init, ask, chat, history, skill) | 9 (init, build, query, deps, usedby, overview, symbols, context, setup, view, info) |
|
|
276
291
|
| LLM integration | Built-in (Ollama, Claude, OpenAI, Azure) | None (delegates to Claude Code) |
|
|
277
292
|
| Chat sessions | Yes | No |
|
|
278
|
-
| Visualizer | D3.js HTML + SVG |
|
|
293
|
+
| Visualizer | D3.js HTML + SVG | D3.js HTML (`view` opens in browser, `--tree` for text) |
|
|
279
294
|
| Skills system | Yes (customizable skill TOML files) | No |
|
|
280
295
|
| MCP server | Yes | No |
|
|
281
296
|
| Token savings | Yes (capsule DSL compression) | No |
|
|
@@ -24,4 +24,6 @@ src/ctxgraph_code/graph/__init__.py
|
|
|
24
24
|
src/ctxgraph_code/graph/builder.py
|
|
25
25
|
src/ctxgraph_code/graph/models.py
|
|
26
26
|
src/ctxgraph_code/graph/query.py
|
|
27
|
-
src/ctxgraph_code/graph/storage.py
|
|
27
|
+
src/ctxgraph_code/graph/storage.py
|
|
28
|
+
src/ctxgraph_code/view/__init__.py
|
|
29
|
+
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
|
|
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
|