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.
Files changed (31) hide show
  1. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/PKG-INFO +18 -3
  2. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/README.md +17 -2
  3. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/pyproject.toml +1 -1
  4. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/cli.py +53 -1
  5. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/render.py +101 -0
  6. ctxgraph_code-0.1.3/src/ctxgraph_code/view/__init__.py +0 -0
  7. ctxgraph_code-0.1.3/src/ctxgraph_code/view/visualizer.py +288 -0
  8. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code.egg-info/PKG-INFO +18 -3
  9. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code.egg-info/SOURCES.txt +3 -1
  10. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/setup.cfg +0 -0
  11. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/__init__.py +0 -0
  12. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/__main__.py +0 -0
  13. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/analyzers/__init__.py +0 -0
  14. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/analyzers/python/__init__.py +0 -0
  15. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/analyzers/python/importer.py +0 -0
  16. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/analyzers/python/semantic.py +0 -0
  17. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/analyzers/python/symbols.py +0 -0
  18. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/config/__init__.py +0 -0
  19. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/config/init.py +0 -0
  20. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/config/settings.py +0 -0
  21. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/exclude/__init__.py +0 -0
  22. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/exclude/patterns.py +0 -0
  23. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/graph/__init__.py +0 -0
  24. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/graph/builder.py +0 -0
  25. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/graph/models.py +0 -0
  26. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/graph/query.py +0 -0
  27. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code/graph/storage.py +0 -0
  28. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code.egg-info/dependency_links.txt +0 -0
  29. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code.egg-info/entry_points.txt +0 -0
  30. {ctxgraph_code-0.1.1 → ctxgraph_code-0.1.3}/src/ctxgraph_code.egg-info/requires.txt +0 -0
  31. {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.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) | 8 (init, build, query, deps, usedby, overview, symbols, context, setup, info) |
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 | No |
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) | 8 (init, build, query, deps, usedby, overview, symbols, context, setup, info) |
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 | No |
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.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.0"
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.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) | 8 (init, build, query, deps, usedby, overview, symbols, context, setup, info) |
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 | No |
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