codegraph-cli-ai 0.1.8__tar.gz → 0.1.9__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.
- {codegraph_cli_ai-0.1.8/codegraph_cli_ai.egg-info → codegraph_cli_ai-0.1.9}/PKG-INFO +4 -1
- codegraph_cli_ai-0.1.9/codegraph/cli.py +279 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9/codegraph_cli_ai.egg-info}/PKG-INFO +4 -1
- codegraph_cli_ai-0.1.9/codegraph_cli_ai.egg-info/requires.txt +6 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/pyproject.toml +5 -2
- codegraph_cli_ai-0.1.8/codegraph/cli.py +0 -564
- codegraph_cli_ai-0.1.8/codegraph_cli_ai.egg-info/requires.txt +0 -3
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/MANIFEST.in +0 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/README.md +0 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/codegraph/graph/builder.py +0 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/codegraph/parsers/database_parser.py +0 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/codegraph/parsers/image_parser.py +0 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/codegraph/parsers/multimodal_parser.py +0 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/codegraph/parsers/pdf_parser.py +0 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/codegraph/parsers/python_parser.py +0 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/codegraph_cli_ai.egg-info/SOURCES.txt +0 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/codegraph_cli_ai.egg-info/dependency_links.txt +0 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/codegraph_cli_ai.egg-info/entry_points.txt +0 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/codegraph_cli_ai.egg-info/top_level.txt +0 -0
- {codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-cli-ai
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: CLI tool to analyze codebases and visualize knowledge graphs using AST
|
|
5
5
|
Author: Aditya Jogdand
|
|
6
6
|
License: MIT
|
|
@@ -15,6 +15,9 @@ Description-Content-Type: text/markdown
|
|
|
15
15
|
Requires-Dist: typer>=0.9.0
|
|
16
16
|
Requires-Dist: networkx>=3.0
|
|
17
17
|
Requires-Dist: pyvis>=0.3.2
|
|
18
|
+
Requires-Dist: pypdf>=3.0.0
|
|
19
|
+
Requires-Dist: pytesseract>=0.3.10
|
|
20
|
+
Requires-Dist: Pillow>=9.0.0
|
|
18
21
|
|
|
19
22
|
# CodeGraph AI
|
|
20
23
|
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeGraph AI - CLI Entry Point
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import typer
|
|
7
|
+
import webbrowser
|
|
8
|
+
import networkx as nx
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, List, Set, Dict
|
|
12
|
+
from collections import deque
|
|
13
|
+
from codegraph.parsers.python_parser import PythonParser
|
|
14
|
+
from codegraph.parsers.multimodal_parser import MultiModalParser
|
|
15
|
+
from codegraph.graph.builder import GraphBuilder
|
|
16
|
+
|
|
17
|
+
app = typer.Typer()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.callback()
|
|
21
|
+
def main():
|
|
22
|
+
"""CodeGraph AI - Understand your codebase using graphs and AI."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Dynamic Ignore Defaults
|
|
27
|
+
IGNORE_FILES = {"graph.json", ".DS_Store"}
|
|
28
|
+
IGNORE_EXTENSIONS = {".pyc", ".log", ".pyo", ".pyd"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_virtualenv(path: Path) -> bool:
|
|
32
|
+
"""Detect if a directory is a virtual environment based on its structure."""
|
|
33
|
+
if not path.is_dir():
|
|
34
|
+
return False
|
|
35
|
+
return (
|
|
36
|
+
(path / "pyvenv.cfg").exists()
|
|
37
|
+
or (path / "bin" / "python").exists()
|
|
38
|
+
or (path / "Scripts" / "python.exe").exists()
|
|
39
|
+
or (path / "bin" / "pip").exists()
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def should_ignore_dir(name: str, path: Path, root: Path) -> bool:
|
|
44
|
+
"""Heuristic-based dynamic directory ignoring."""
|
|
45
|
+
if name.startswith(".") and path != root:
|
|
46
|
+
return True
|
|
47
|
+
if name.startswith("__"):
|
|
48
|
+
return True
|
|
49
|
+
if is_virtualenv(path):
|
|
50
|
+
return True
|
|
51
|
+
if name in {"node_modules", "bin", "Scripts", "lib", "obj", "target", "build", "dist"}:
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command()
|
|
57
|
+
def index(
|
|
58
|
+
path: str = typer.Argument(".", help="Path to the repo or folder to index")
|
|
59
|
+
):
|
|
60
|
+
"""Scan a directory, parse all Python files, and save the knowledge graph."""
|
|
61
|
+
root = Path(path).resolve()
|
|
62
|
+
|
|
63
|
+
if not root.exists():
|
|
64
|
+
typer.echo(f"[error] Path does not exist: {root}", err=True)
|
|
65
|
+
raise typer.Exit(code=1)
|
|
66
|
+
|
|
67
|
+
typer.echo(f"Indexing: {root}\n")
|
|
68
|
+
|
|
69
|
+
all_files = []
|
|
70
|
+
|
|
71
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
72
|
+
dpath = Path(dirpath)
|
|
73
|
+
dirnames[:] = [d for d in dirnames if not should_ignore_dir(d, dpath / d, root)]
|
|
74
|
+
|
|
75
|
+
for f in filenames:
|
|
76
|
+
if f in IGNORE_FILES:
|
|
77
|
+
continue
|
|
78
|
+
p = dpath / f
|
|
79
|
+
if p.suffix.lower() in IGNORE_EXTENSIONS:
|
|
80
|
+
continue
|
|
81
|
+
all_files.append(p)
|
|
82
|
+
|
|
83
|
+
py_files = [f for f in all_files if f.suffix == ".py"]
|
|
84
|
+
asset_exts = {".csv", ".json", ".db", ".sqlite", ".pdf", ".png", ".jpg", ".jpeg"}
|
|
85
|
+
asset_files = [f for f in all_files if f.suffix.lower() in asset_exts]
|
|
86
|
+
|
|
87
|
+
if not py_files and not asset_files:
|
|
88
|
+
typer.echo("No supported files found (everything might be ignored).")
|
|
89
|
+
raise typer.Exit()
|
|
90
|
+
|
|
91
|
+
typer.echo(f"Found {len(py_files)} Python file(s) and {len(asset_files)} asset(s)\n")
|
|
92
|
+
|
|
93
|
+
py_parser = PythonParser()
|
|
94
|
+
mm_parser = MultiModalParser()
|
|
95
|
+
parsed_files = []
|
|
96
|
+
parsed_assets = []
|
|
97
|
+
failed_files = []
|
|
98
|
+
|
|
99
|
+
for filepath in py_files:
|
|
100
|
+
result = py_parser.parse_file(str(filepath))
|
|
101
|
+
if result.errors:
|
|
102
|
+
failed_files.append((str(filepath), result.errors))
|
|
103
|
+
else:
|
|
104
|
+
typer.echo(f" ✔ [code] {filepath.relative_to(root)}")
|
|
105
|
+
parsed_files.append(result)
|
|
106
|
+
|
|
107
|
+
for filepath in asset_files:
|
|
108
|
+
try:
|
|
109
|
+
asset = mm_parser.parse(str(filepath))
|
|
110
|
+
typer.echo(f" ✔ [asset] {filepath.relative_to(root)}")
|
|
111
|
+
parsed_assets.append(asset)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
failed_files.append((str(filepath), [str(e)]))
|
|
114
|
+
|
|
115
|
+
typer.echo("\nBuilding graph...")
|
|
116
|
+
builder = GraphBuilder()
|
|
117
|
+
builder.build(parsed_files, parsed_assets)
|
|
118
|
+
summary = builder.summary()
|
|
119
|
+
|
|
120
|
+
output_dir = root / ".codegraph"
|
|
121
|
+
output_dir.mkdir(exist_ok=True)
|
|
122
|
+
output_file = output_dir / "graph.json"
|
|
123
|
+
with output_file.open("w", encoding="utf-8") as fp:
|
|
124
|
+
json.dump(builder.to_dict(), fp, indent=2)
|
|
125
|
+
|
|
126
|
+
typer.echo("\n" + "=" * 50)
|
|
127
|
+
typer.echo("Index complete")
|
|
128
|
+
typer.echo(f" Graph saved : {output_file}")
|
|
129
|
+
typer.echo(f" Graph nodes : {summary['total_nodes']}")
|
|
130
|
+
typer.echo(f" Graph edges : {summary['total_edges']}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command()
|
|
134
|
+
def plot(
|
|
135
|
+
hide_external: bool = typer.Option(False, "--hide-external", help="Hide external/stdlib nodes"),
|
|
136
|
+
level: Optional[str] = typer.Option(None, "--level", help="Show only: file, function, class, method"),
|
|
137
|
+
focus: Optional[str] = typer.Option(None, "--focus", help="Focus on a specific file"),
|
|
138
|
+
edge_type: Optional[str] = typer.Option(None, "--edge-type", help="Filter edges"),
|
|
139
|
+
):
|
|
140
|
+
"""Visualize the knowledge graph as a premium interactive HTML file."""
|
|
141
|
+
root = Path(".").resolve()
|
|
142
|
+
graph_file = root / ".codegraph" / "graph.json"
|
|
143
|
+
|
|
144
|
+
if not graph_file.exists():
|
|
145
|
+
typer.echo("[error] No graph found. Run 'codegraph index' first.", err=True)
|
|
146
|
+
raise typer.Exit(code=1)
|
|
147
|
+
|
|
148
|
+
typer.echo(f"Loading graph from {graph_file}...")
|
|
149
|
+
with graph_file.open("r", encoding="utf-8") as f:
|
|
150
|
+
data = json.load(f)
|
|
151
|
+
|
|
152
|
+
G_orig = nx.DiGraph()
|
|
153
|
+
for node in data.get("nodes", []):
|
|
154
|
+
G_orig.add_node(node["id"], **{k: v for k, v in node.items() if k != "id"})
|
|
155
|
+
for edge in data.get("edges", []):
|
|
156
|
+
G_orig.add_edge(edge["source"], edge["target"], **{k: v for k, v in edge.items() if k not in ["source", "target"]})
|
|
157
|
+
|
|
158
|
+
G = G_orig.copy()
|
|
159
|
+
|
|
160
|
+
if hide_external:
|
|
161
|
+
remove = [n for n, d in G.nodes(data=True) if d.get("external", False)]
|
|
162
|
+
G.remove_nodes_from(remove)
|
|
163
|
+
|
|
164
|
+
if level:
|
|
165
|
+
level_map = {
|
|
166
|
+
"file": {"file", "dataset", "database", "document", "image"},
|
|
167
|
+
"function": {"function", "method"},
|
|
168
|
+
"class": {"class"}
|
|
169
|
+
}
|
|
170
|
+
requested_levels = level.split(",")
|
|
171
|
+
allowed_kinds = set()
|
|
172
|
+
for r in requested_levels:
|
|
173
|
+
if r in level_map:
|
|
174
|
+
allowed_kinds.update(level_map[r])
|
|
175
|
+
else:
|
|
176
|
+
allowed_kinds.add(r)
|
|
177
|
+
|
|
178
|
+
if ("file" in requested_levels or "document" in allowed_kinds) and len(requested_levels) < 3:
|
|
179
|
+
G = _collapse_to_level(G, allowed_kinds)
|
|
180
|
+
else:
|
|
181
|
+
remove = [n for n, d in G.nodes(data=True) if d.get("kind") not in allowed_kinds]
|
|
182
|
+
G.remove_nodes_from(remove)
|
|
183
|
+
|
|
184
|
+
if focus:
|
|
185
|
+
focus_id = f"file:{focus}"
|
|
186
|
+
if focus_id in G:
|
|
187
|
+
keep = {focus_id} | set(G.successors(focus_id)) | set(G.predecessors(focus_id))
|
|
188
|
+
remove = [n for n in G.nodes if n not in keep]
|
|
189
|
+
G.remove_nodes_from(remove)
|
|
190
|
+
|
|
191
|
+
if edge_type and edge_type != "all":
|
|
192
|
+
allowed_edges = set(edge_type.split(","))
|
|
193
|
+
remove_edges = [(s, t) for s, t, d in G.edges(data=True) if d.get("relation") not in allowed_edges]
|
|
194
|
+
G.remove_edges_from(remove_edges)
|
|
195
|
+
G.remove_nodes_from(list(nx.isolates(G)))
|
|
196
|
+
|
|
197
|
+
typer.echo(f"Rendering {G.number_of_nodes()} nodes, {G.number_of_edges()} edges...")
|
|
198
|
+
|
|
199
|
+
html = _build_premium_html(G)
|
|
200
|
+
output_path = root / "graph.html"
|
|
201
|
+
output_path.write_text(html, encoding="utf-8")
|
|
202
|
+
typer.echo(f"Saved to: {output_path}")
|
|
203
|
+
webbrowser.open(f"file://{output_path.resolve()}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _collapse_to_level(G: nx.DiGraph, allowed_kinds: set) -> nx.DiGraph:
|
|
207
|
+
"""Contract the graph using depth-limited BFS for high-performance connectivity."""
|
|
208
|
+
new_G = nx.DiGraph()
|
|
209
|
+
keep_nodes = [n for n, d in G.nodes(data=True) if d.get("kind") in allowed_kinds]
|
|
210
|
+
|
|
211
|
+
# Pre-populate all nodes
|
|
212
|
+
for n in keep_nodes:
|
|
213
|
+
new_G.add_node(n, **G.nodes[n])
|
|
214
|
+
|
|
215
|
+
# For each source node, find reachable target nodes within 5 steps
|
|
216
|
+
for start_node in keep_nodes:
|
|
217
|
+
# Standard BFS with depth tracking
|
|
218
|
+
queue = deque([(start_node, 0)])
|
|
219
|
+
visited = {start_node}
|
|
220
|
+
|
|
221
|
+
while queue:
|
|
222
|
+
current, depth = queue.popleft()
|
|
223
|
+
if depth >= 5: continue
|
|
224
|
+
|
|
225
|
+
for neighbor in G.successors(current):
|
|
226
|
+
if neighbor in visited: continue
|
|
227
|
+
visited.add(neighbor)
|
|
228
|
+
|
|
229
|
+
if neighbor in new_G:
|
|
230
|
+
# Found a connection to another keep_node!
|
|
231
|
+
# Extract relation: if it's a multi-hop, use a representative one
|
|
232
|
+
edge_data = G.get_edge_data(current, neighbor)
|
|
233
|
+
# For simplicity, we just use the first edge's relation or a default
|
|
234
|
+
rel = edge_data.get("relation", "calls")
|
|
235
|
+
new_G.add_edge(start_node, neighbor, relation=rel)
|
|
236
|
+
# Stop searching down this branch once we hit a keep_node
|
|
237
|
+
continue
|
|
238
|
+
else:
|
|
239
|
+
# Keep searching through non-keep nodes
|
|
240
|
+
queue.append((neighbor, depth + 1))
|
|
241
|
+
|
|
242
|
+
return new_G
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _build_premium_html(G: nx.DiGraph) -> str:
|
|
246
|
+
"""Generate a self-contained premium HTML visualization."""
|
|
247
|
+
nodes_js = []
|
|
248
|
+
for n, d in G.nodes(data=True):
|
|
249
|
+
nodes_js.append(f"{{id: {json.dumps(n)}, label: {json.dumps(d.get('label', n))}}}")
|
|
250
|
+
edges_js = []
|
|
251
|
+
for s, t, d in G.edges(data=True):
|
|
252
|
+
edges_js.append(f"{{from: {json.dumps(s)}, to: {json.dumps(t)}, label: {json.dumps(d.get('relation', ''))}, arrows: 'to'}}")
|
|
253
|
+
|
|
254
|
+
nodes_str = ','.join(nodes_js)
|
|
255
|
+
edges_str = ','.join(edges_js)
|
|
256
|
+
|
|
257
|
+
return f"""<!DOCTYPE html>
|
|
258
|
+
<html>
|
|
259
|
+
<head>
|
|
260
|
+
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
|
261
|
+
<style>body{{background:#0F1117;color:#fff;margin:0;overflow:hidden;}}#graph{{height:100vh;}}</style>
|
|
262
|
+
</head>
|
|
263
|
+
<body>
|
|
264
|
+
<div id="graph"></div>
|
|
265
|
+
<script>
|
|
266
|
+
const nodes = new vis.DataSet([{nodes_str}]);
|
|
267
|
+
const edges = new vis.DataSet([{edges_str}]);
|
|
268
|
+
new vis.Network(document.getElementById('graph'), {{nodes, edges}}, {{physics:{{solver:'forceAtlas2Based'}} }});
|
|
269
|
+
</script>
|
|
270
|
+
</body>
|
|
271
|
+
</html>"""
|
|
272
|
+
|
|
273
|
+
@app.command()
|
|
274
|
+
def ask(query: str = typer.Argument(..., help="Question about your codebase")):
|
|
275
|
+
typer.echo("ask: coming soon")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
app()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-cli-ai
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: CLI tool to analyze codebases and visualize knowledge graphs using AST
|
|
5
5
|
Author: Aditya Jogdand
|
|
6
6
|
License: MIT
|
|
@@ -15,6 +15,9 @@ Description-Content-Type: text/markdown
|
|
|
15
15
|
Requires-Dist: typer>=0.9.0
|
|
16
16
|
Requires-Dist: networkx>=3.0
|
|
17
17
|
Requires-Dist: pyvis>=0.3.2
|
|
18
|
+
Requires-Dist: pypdf>=3.0.0
|
|
19
|
+
Requires-Dist: pytesseract>=0.3.10
|
|
20
|
+
Requires-Dist: Pillow>=9.0.0
|
|
18
21
|
|
|
19
22
|
# CodeGraph AI
|
|
20
23
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codegraph-cli-ai"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.9"
|
|
8
8
|
description = "CLI tool to analyze codebases and visualize knowledge graphs using AST"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -26,7 +26,10 @@ classifiers = [
|
|
|
26
26
|
dependencies = [
|
|
27
27
|
"typer>=0.9.0",
|
|
28
28
|
"networkx>=3.0",
|
|
29
|
-
"pyvis>=0.3.2"
|
|
29
|
+
"pyvis>=0.3.2",
|
|
30
|
+
"pypdf>=3.0.0",
|
|
31
|
+
"pytesseract>=0.3.10",
|
|
32
|
+
"Pillow>=9.0.0"
|
|
30
33
|
]
|
|
31
34
|
|
|
32
35
|
[project.scripts]
|
|
@@ -1,564 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
CodeGraph AI - CLI Entry Point
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import typer
|
|
7
|
-
import webbrowser
|
|
8
|
-
import networkx as nx
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Optional
|
|
11
|
-
from codegraph.parsers.python_parser import PythonParser
|
|
12
|
-
from codegraph.parsers.multimodal_parser import MultiModalParser
|
|
13
|
-
from codegraph.graph.builder import GraphBuilder
|
|
14
|
-
|
|
15
|
-
app = typer.Typer()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@app.callback()
|
|
19
|
-
def main():
|
|
20
|
-
"""CodeGraph AI - Understand your codebase using graphs and AI."""
|
|
21
|
-
pass
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
IGNORE_DIRS = {
|
|
25
|
-
"venv", ".venv", "env", "bin", "Scripts",
|
|
26
|
-
".git", "__pycache__", "node_modules", ".codegraph"
|
|
27
|
-
}
|
|
28
|
-
IGNORE_FILES = {"graph.json", ".DS_Store"}
|
|
29
|
-
IGNORE_EXTENSIONS = {".pyc", ".log", ".pyo", ".pyd"}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def is_virtualenv(path: Path) -> bool:
|
|
33
|
-
"""Detect if a directory is a virtual environment based on its structure."""
|
|
34
|
-
return (
|
|
35
|
-
(path / "pyvenv.cfg").exists()
|
|
36
|
-
or (path / "bin" / "python").exists()
|
|
37
|
-
or (path / "Scripts" / "python.exe").exists()
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@app.command()
|
|
42
|
-
def index(
|
|
43
|
-
path: str = typer.Argument(".", help="Path to the repo or folder to index")
|
|
44
|
-
):
|
|
45
|
-
"""Scan a directory, parse all Python files, and save the knowledge graph."""
|
|
46
|
-
root = Path(path).resolve()
|
|
47
|
-
|
|
48
|
-
if not root.exists():
|
|
49
|
-
typer.echo(f"[error] Path does not exist: {root}", err=True)
|
|
50
|
-
raise typer.Exit(code=1)
|
|
51
|
-
|
|
52
|
-
typer.echo(f"Indexing: {root}\n")
|
|
53
|
-
|
|
54
|
-
# Phase A: Identify top-level custom virtualenvs
|
|
55
|
-
venv_dirs = set()
|
|
56
|
-
for item in root.iterdir():
|
|
57
|
-
if item.is_dir() and is_virtualenv(item):
|
|
58
|
-
venv_dirs.add(item)
|
|
59
|
-
|
|
60
|
-
# Phase B: Filtered Scan
|
|
61
|
-
all_files = []
|
|
62
|
-
for p in root.rglob("*"):
|
|
63
|
-
if not p.is_file():
|
|
64
|
-
continue
|
|
65
|
-
|
|
66
|
-
# 1. Skip if any parent directory is in IGNORE_DIRS
|
|
67
|
-
if any(part in IGNORE_DIRS for part in p.parts):
|
|
68
|
-
continue
|
|
69
|
-
|
|
70
|
-
# 2. Skip if inside a detected custom venv
|
|
71
|
-
if any(v_dir in p.parents for v_dir in venv_dirs):
|
|
72
|
-
continue
|
|
73
|
-
|
|
74
|
-
# 3. Skip if filename is ignored
|
|
75
|
-
if p.name in IGNORE_FILES:
|
|
76
|
-
continue
|
|
77
|
-
|
|
78
|
-
# 4. Skip if extension is ignored
|
|
79
|
-
if p.suffix.lower() in IGNORE_EXTENSIONS:
|
|
80
|
-
continue
|
|
81
|
-
|
|
82
|
-
all_files.append(p)
|
|
83
|
-
|
|
84
|
-
py_files = [f for f in all_files if f.suffix == ".py"]
|
|
85
|
-
asset_exts = {
|
|
86
|
-
".csv", ".json", ".db", ".sqlite",
|
|
87
|
-
".pdf",
|
|
88
|
-
".png", ".jpg", ".jpeg"
|
|
89
|
-
}
|
|
90
|
-
asset_files = [f for f in all_files if f.suffix.lower() in asset_exts]
|
|
91
|
-
|
|
92
|
-
if not py_files and not asset_files:
|
|
93
|
-
typer.echo("No supported files found (everything might be ignored).")
|
|
94
|
-
raise typer.Exit()
|
|
95
|
-
|
|
96
|
-
typer.echo(f"Found {len(py_files)} Python file(s) and {len(asset_files)} asset(s)\n")
|
|
97
|
-
|
|
98
|
-
# Step 1 — Parse
|
|
99
|
-
py_parser = PythonParser()
|
|
100
|
-
mm_parser = MultiModalParser()
|
|
101
|
-
|
|
102
|
-
parsed_files = []
|
|
103
|
-
parsed_assets = []
|
|
104
|
-
failed_files = []
|
|
105
|
-
|
|
106
|
-
# Parse Python files
|
|
107
|
-
for filepath in py_files:
|
|
108
|
-
result = py_parser.parse_file(str(filepath))
|
|
109
|
-
if result.errors:
|
|
110
|
-
failed_files.append((str(filepath), result.errors))
|
|
111
|
-
else:
|
|
112
|
-
typer.echo(f" ✔ [code] {filepath.relative_to(root)}")
|
|
113
|
-
parsed_files.append(result)
|
|
114
|
-
|
|
115
|
-
# Parse assets
|
|
116
|
-
for filepath in asset_files:
|
|
117
|
-
try:
|
|
118
|
-
asset = mm_parser.parse(str(filepath))
|
|
119
|
-
typer.echo(f" ✔ [asset] {filepath.relative_to(root)}")
|
|
120
|
-
parsed_assets.append(asset)
|
|
121
|
-
except Exception as e:
|
|
122
|
-
failed_files.append((str(filepath), [str(e)]))
|
|
123
|
-
|
|
124
|
-
# Step 2 — Build graph
|
|
125
|
-
typer.echo("\nBuilding graph...")
|
|
126
|
-
builder = GraphBuilder()
|
|
127
|
-
builder.build(parsed_files, parsed_assets)
|
|
128
|
-
summary = builder.summary()
|
|
129
|
-
|
|
130
|
-
# Step 3 — Save to .codegraph/graph.json
|
|
131
|
-
output_dir = root / ".codegraph"
|
|
132
|
-
output_dir.mkdir(exist_ok=True)
|
|
133
|
-
output_file = output_dir / "graph.json"
|
|
134
|
-
with output_file.open("w", encoding="utf-8") as fp:
|
|
135
|
-
json.dump(builder.to_dict(), fp, indent=2)
|
|
136
|
-
|
|
137
|
-
# Step 4 — Summary
|
|
138
|
-
typer.echo("\n" + "=" * 50)
|
|
139
|
-
typer.echo("Index complete")
|
|
140
|
-
typer.echo(f" Graph saved : {output_file}")
|
|
141
|
-
typer.echo(f" Files parsed : {len(parsed_files)}")
|
|
142
|
-
typer.echo(f" Graph nodes : {summary['total_nodes']}")
|
|
143
|
-
typer.echo(f" Graph edges : {summary['total_edges']}")
|
|
144
|
-
typer.echo("\n Node breakdown:")
|
|
145
|
-
for kind, count in summary["nodes_by_kind"].items():
|
|
146
|
-
typer.echo(f" {kind:<12}: {count}")
|
|
147
|
-
typer.echo("\n Edge breakdown:")
|
|
148
|
-
for rel, count in summary["edges_by_relation"].items():
|
|
149
|
-
typer.echo(f" {rel:<12}: {count}")
|
|
150
|
-
|
|
151
|
-
if failed_files:
|
|
152
|
-
typer.echo(f"\n Failed files : {len(failed_files)}")
|
|
153
|
-
for fp, errors in failed_files:
|
|
154
|
-
typer.echo(f" {fp}: {errors}")
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
@app.command()
|
|
158
|
-
def plot(
|
|
159
|
-
hide_external: bool = typer.Option(False, "--hide-external", help="Hide external/stdlib nodes"),
|
|
160
|
-
level: Optional[str] = typer.Option(None, "--level", help="Show only: file, function, class, method"),
|
|
161
|
-
focus: Optional[str] = typer.Option(None, "--focus", help="Focus on a specific file (e.g. cli.py)"),
|
|
162
|
-
edge_type: Optional[str] = typer.Option(None, "--edge-type", help="Filter edges: calls, imports, contains, all"),
|
|
163
|
-
):
|
|
164
|
-
"""Visualize the knowledge graph as a premium interactive HTML file."""
|
|
165
|
-
root = Path(".").resolve()
|
|
166
|
-
graph_file = root / ".codegraph" / "graph.json"
|
|
167
|
-
|
|
168
|
-
if not graph_file.exists():
|
|
169
|
-
typer.echo("[error] No graph found. Run 'codegraph index' first.", err=True)
|
|
170
|
-
raise typer.Exit(code=1)
|
|
171
|
-
|
|
172
|
-
typer.echo(f"Loading graph from {graph_file}...")
|
|
173
|
-
with graph_file.open("r", encoding="utf-8") as f:
|
|
174
|
-
data = json.load(f)
|
|
175
|
-
|
|
176
|
-
# Build NetworkX graph
|
|
177
|
-
G = nx.DiGraph()
|
|
178
|
-
for node in data.get("nodes", []):
|
|
179
|
-
G.add_node(node["id"], **{k: v for k, v in node.items() if k != "id"})
|
|
180
|
-
for edge in data.get("edges", []):
|
|
181
|
-
G.add_edge(edge["source"], edge["target"], **{k: v for k, v in edge.items() if k not in ["source", "target"]})
|
|
182
|
-
|
|
183
|
-
# --- Apply filters ---
|
|
184
|
-
|
|
185
|
-
# 1. Hide external nodes
|
|
186
|
-
if hide_external:
|
|
187
|
-
remove = [n for n, d in G.nodes(data=True) if d.get("external", False)]
|
|
188
|
-
G.remove_nodes_from(remove)
|
|
189
|
-
|
|
190
|
-
# 2. Filter by level (node kind)
|
|
191
|
-
if level:
|
|
192
|
-
allowed = set(level.split(","))
|
|
193
|
-
remove = [n for n, d in G.nodes(data=True) if d.get("kind") not in allowed]
|
|
194
|
-
G.remove_nodes_from(remove)
|
|
195
|
-
|
|
196
|
-
# 3. Focus on a specific file — keep only that file + its direct neighbors
|
|
197
|
-
if focus:
|
|
198
|
-
focus_id = f"file:{focus}"
|
|
199
|
-
if focus_id in G:
|
|
200
|
-
keep = {focus_id} | set(G.successors(focus_id)) | set(G.predecessors(focus_id))
|
|
201
|
-
remove = [n for n in G.nodes if n not in keep]
|
|
202
|
-
G.remove_nodes_from(remove)
|
|
203
|
-
else:
|
|
204
|
-
typer.echo(f"[warn] File '{focus}' not found in graph. Showing full graph.")
|
|
205
|
-
|
|
206
|
-
# 4. Filter edge types
|
|
207
|
-
if edge_type and edge_type != "all":
|
|
208
|
-
allowed_edges = set(edge_type.split(","))
|
|
209
|
-
remove_edges = [(s, t) for s, t, d in G.edges(data=True) if d.get("relation") not in allowed_edges]
|
|
210
|
-
G.remove_edges_from(remove_edges)
|
|
211
|
-
# Remove orphan nodes after edge removal
|
|
212
|
-
G.remove_nodes_from(list(nx.isolates(G)))
|
|
213
|
-
|
|
214
|
-
typer.echo(f"Rendering {G.number_of_nodes()} nodes, {G.number_of_edges()} edges...")
|
|
215
|
-
|
|
216
|
-
# --- Build HTML visualization ---
|
|
217
|
-
html = _build_premium_html(G)
|
|
218
|
-
|
|
219
|
-
output_path = root / "graph.html"
|
|
220
|
-
output_path.write_text(html, encoding="utf-8")
|
|
221
|
-
|
|
222
|
-
typer.echo(f"Saved to: {output_path}")
|
|
223
|
-
typer.echo("Opening in browser...")
|
|
224
|
-
webbrowser.open(f"file://{output_path.resolve()}")
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def _build_premium_html(G: nx.DiGraph) -> str:
|
|
228
|
-
"""Generate a self-contained premium HTML visualization using vis.js."""
|
|
229
|
-
|
|
230
|
-
# --- Node styling ---
|
|
231
|
-
STYLES = {
|
|
232
|
-
"file": {"color": "#4A90E2", "shape": "diamond", "size": 28, "font_color": "#ffffff"},
|
|
233
|
-
"class": {"color": "#F5A623", "shape": "hexagon", "size": 24, "font_color": "#ffffff"},
|
|
234
|
-
"function": {"color": "#50C878", "shape": "dot", "size": 16, "font_color": "#ffffff"},
|
|
235
|
-
"method": {"color": "#7ED6A8", "shape": "dot", "size": 14, "font_color": "#ffffff"},
|
|
236
|
-
"module": {"color": "#B0BEC5", "shape": "box", "size": 14, "font_color": "#ffffff"},
|
|
237
|
-
"dataset": {"color": "#FFD700", "shape": "diamond", "size": 22, "font_color": "#ffffff"},
|
|
238
|
-
"database": {"color": "#FF69B4", "shape": "database", "size": 24, "font_color": "#ffffff"},
|
|
239
|
-
"document": {"color": "#9B59B6", "shape": "diamond", "size": 24, "font_color": "#ffffff"},
|
|
240
|
-
"image": {"color": "#5DADE2", "shape": "diamond", "size": 24, "font_color": "#ffffff"},
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
EDGE_COLORS = {
|
|
244
|
-
"contains": "#4A90E2",
|
|
245
|
-
"calls": "#50C878",
|
|
246
|
-
"imports": "#B0BEC5",
|
|
247
|
-
"defined_in": "#E8E8E8",
|
|
248
|
-
"uses": "#FFD700",
|
|
249
|
-
"references": "#9B59B6",
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
nodes_js = []
|
|
253
|
-
for node_id, attrs in G.nodes(data=True):
|
|
254
|
-
kind = attrs.get("kind", "function")
|
|
255
|
-
label = attrs.get("label", node_id)
|
|
256
|
-
external = attrs.get("external", False)
|
|
257
|
-
style = STYLES.get(kind, STYLES["function"])
|
|
258
|
-
|
|
259
|
-
color = "#CFD8DC" if external else style["color"]
|
|
260
|
-
font_color = "#B0BEC5" if external else style["font_color"]
|
|
261
|
-
size = max(style["size"] - 4, 10) if external else style["size"]
|
|
262
|
-
|
|
263
|
-
# Tooltip
|
|
264
|
-
tooltip_parts = [f"<b>{label}</b>", f"Kind: {kind}"]
|
|
265
|
-
if attrs.get("filename"):
|
|
266
|
-
tooltip_parts.append(f"File: {attrs['filename']}")
|
|
267
|
-
elif attrs.get("file"):
|
|
268
|
-
tooltip_parts.append(f"File: {attrs['file'].replace('file:', '')}")
|
|
269
|
-
if attrs.get("cls"):
|
|
270
|
-
tooltip_parts.append(f"Class: {attrs['cls']}")
|
|
271
|
-
|
|
272
|
-
# MultiModal Metadata for tooltips
|
|
273
|
-
metadata = attrs.get("metadata", {})
|
|
274
|
-
if kind == "dataset":
|
|
275
|
-
if "columns" in metadata: tooltip_parts.append(f"Columns: {', '.join(metadata['columns'])}")
|
|
276
|
-
if "keys" in metadata: tooltip_parts.append(f"Keys: {', '.join(metadata['keys'])}")
|
|
277
|
-
elif kind == "database":
|
|
278
|
-
if "tables" in metadata: tooltip_parts.append(f"Tables: {', '.join(metadata['tables'])}")
|
|
279
|
-
elif kind == "document":
|
|
280
|
-
if "num_pages" in metadata: tooltip_parts.append(f"Pages: {metadata['num_pages']}")
|
|
281
|
-
if "text_preview" in metadata: tooltip_parts.append(f"<br>Preview: <i>{metadata['text_preview']}</i>")
|
|
282
|
-
elif kind == "image":
|
|
283
|
-
if "text" in metadata and metadata["text"]: tooltip_parts.append(f"<br>OCR Text: <i>{metadata['text']}</i>")
|
|
284
|
-
|
|
285
|
-
# Add metadata for datasets/databases
|
|
286
|
-
metadata = attrs.get("metadata", {})
|
|
287
|
-
if metadata:
|
|
288
|
-
if "columns" in metadata:
|
|
289
|
-
cols = ", ".join(metadata["columns"][:5])
|
|
290
|
-
if len(metadata["columns"]) > 5: cols += "..."
|
|
291
|
-
tooltip_parts.append(f"Columns: {cols}")
|
|
292
|
-
if "tables" in metadata:
|
|
293
|
-
tbls = ", ".join(metadata["tables"])
|
|
294
|
-
tooltip_parts.append(f"Tables: {tbls}")
|
|
295
|
-
if "keys" in metadata:
|
|
296
|
-
keys = ", ".join(metadata["keys"][:5])
|
|
297
|
-
if len(metadata["keys"]) > 5: keys += "..."
|
|
298
|
-
tooltip_parts.append(f"Keys: {keys}")
|
|
299
|
-
|
|
300
|
-
if external:
|
|
301
|
-
tooltip_parts.append("<i>external</i>")
|
|
302
|
-
tooltip = "<br>".join(tooltip_parts)
|
|
303
|
-
|
|
304
|
-
nodes_js.append(f"""{{
|
|
305
|
-
id: {json.dumps(node_id)},
|
|
306
|
-
label: {json.dumps(label)},
|
|
307
|
-
title: {json.dumps(tooltip)},
|
|
308
|
-
color: {{
|
|
309
|
-
background: {json.dumps(color)},
|
|
310
|
-
border: {json.dumps(_darken(color))},
|
|
311
|
-
highlight: {{ background: "#FFE082", border: "#FFA000" }},
|
|
312
|
-
hover: {{ background: "#E3F2FD", border: "#1E88E5" }}
|
|
313
|
-
}},
|
|
314
|
-
shape: {json.dumps(style["shape"])},
|
|
315
|
-
size: {size},
|
|
316
|
-
font: {{ color: {json.dumps(font_color)}, size: 13, face: "Inter, system-ui, sans-serif" }},
|
|
317
|
-
borderWidth: 1.5,
|
|
318
|
-
shadow: {{ enabled: true, color: "rgba(0,0,0,0.12)", size: 8, x: 2, y: 2 }}
|
|
319
|
-
}}""")
|
|
320
|
-
|
|
321
|
-
edges_js = []
|
|
322
|
-
for src, dst, attrs in G.edges(data=True):
|
|
323
|
-
relation = attrs.get("relation", "")
|
|
324
|
-
color = EDGE_COLORS.get(relation, "#BDBDBD")
|
|
325
|
-
dashed = relation in ("defined_in", "imports")
|
|
326
|
-
|
|
327
|
-
edges_js.append(f"""{{
|
|
328
|
-
from: {json.dumps(src)},
|
|
329
|
-
to: {json.dumps(dst)},
|
|
330
|
-
label: {json.dumps(relation)},
|
|
331
|
-
color: {{ color: {json.dumps(color)}, opacity: 0.7 }},
|
|
332
|
-
dashes: {"true" if dashed else "false"},
|
|
333
|
-
width: {"1" if dashed else "1.5"},
|
|
334
|
-
font: {{ color: "#9E9E9E", size: 10, face: "Inter, system-ui, sans-serif", align: "middle" }},
|
|
335
|
-
arrows: {{ to: {{ enabled: true, scaleFactor: 0.6 }} }},
|
|
336
|
-
smooth: {{ type: "curvedCW", roundness: 0.2 }}
|
|
337
|
-
}}""")
|
|
338
|
-
|
|
339
|
-
nodes_json = "[\n" + ",\n".join(nodes_js) + "\n]"
|
|
340
|
-
edges_json = "[\n" + ",\n".join(edges_js) + "\n]"
|
|
341
|
-
|
|
342
|
-
return f"""<!DOCTYPE html>
|
|
343
|
-
<html lang="en">
|
|
344
|
-
<head>
|
|
345
|
-
<meta charset="UTF-8">
|
|
346
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
347
|
-
<title>CodeGraph AI</title>
|
|
348
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
349
|
-
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
|
350
|
-
<style>
|
|
351
|
-
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
352
|
-
body {{
|
|
353
|
-
font-family: 'Inter', system-ui, sans-serif;
|
|
354
|
-
background: #0F1117;
|
|
355
|
-
color: #E0E0E0;
|
|
356
|
-
height: 100vh;
|
|
357
|
-
display: flex;
|
|
358
|
-
flex-direction: column;
|
|
359
|
-
overflow: hidden;
|
|
360
|
-
}}
|
|
361
|
-
|
|
362
|
-
/* Background effect */
|
|
363
|
-
#graph-container::before {{
|
|
364
|
-
content: "";
|
|
365
|
-
position: absolute;
|
|
366
|
-
top: 0; left: 0; right: 0; bottom: 0;
|
|
367
|
-
background-image: radial-gradient(circle at 2px 2px, rgba(255,255,255,0.03) 1px, transparent 0);
|
|
368
|
-
background-size: 32px 32px;
|
|
369
|
-
pointer-events: none;
|
|
370
|
-
z-index: 0;
|
|
371
|
-
}}
|
|
372
|
-
|
|
373
|
-
#graph-container::after {{
|
|
374
|
-
content: "";
|
|
375
|
-
position: absolute;
|
|
376
|
-
top: 0; left: 0; right: 0; bottom: 0;
|
|
377
|
-
background: radial-gradient(circle at center, transparent 0%, rgba(15,17,23,0.4) 100%);
|
|
378
|
-
pointer-events: none;
|
|
379
|
-
z-index: 1;
|
|
380
|
-
}}
|
|
381
|
-
|
|
382
|
-
/* Header */
|
|
383
|
-
#header {{
|
|
384
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
385
|
-
padding: 16px 24px;
|
|
386
|
-
background: rgba(26, 29, 39, 0.8);
|
|
387
|
-
backdrop-filter: blur(10px);
|
|
388
|
-
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
389
|
-
flex-shrink: 0;
|
|
390
|
-
z-index: 10;
|
|
391
|
-
}}
|
|
392
|
-
#header h1 {{ font-size: 18px; font-weight: 600; color: #fff; letter-spacing: -0.01em; }}
|
|
393
|
-
#header h1 span {{ color: #4A90E2; }}
|
|
394
|
-
#stats {{ display: flex; gap: 20px; font-size: 13px; color: #78909C; }}
|
|
395
|
-
#stats b {{ color: #ffffff; }}
|
|
396
|
-
|
|
397
|
-
/* Legend */
|
|
398
|
-
#legend {{
|
|
399
|
-
display: flex; gap: 16px; align-items: center;
|
|
400
|
-
padding: 10px 24px;
|
|
401
|
-
background: rgba(26, 29, 39, 0.6);
|
|
402
|
-
backdrop-filter: blur(8px);
|
|
403
|
-
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
404
|
-
flex-shrink: 0;
|
|
405
|
-
flex-wrap: wrap;
|
|
406
|
-
z-index: 10;
|
|
407
|
-
}}
|
|
408
|
-
.legend-item {{ display: flex; align-items: center; gap: 8px; font-size: 12px; color: #B0BEC5; font-weight: 500; }}
|
|
409
|
-
.legend-dot {{ width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }}
|
|
410
|
-
|
|
411
|
-
/* Graph */
|
|
412
|
-
#graph-container {{ flex: 1; position: relative; background: #0F1117; }}
|
|
413
|
-
#graph {{ position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 2; }}
|
|
414
|
-
|
|
415
|
-
/* Tooltip override */
|
|
416
|
-
.vis-tooltip {{
|
|
417
|
-
background: #1E2130 !important;
|
|
418
|
-
border: 1px solid #2A2D3A !important;
|
|
419
|
-
color: #E0E0E0 !important;
|
|
420
|
-
font-size: 13px !important;
|
|
421
|
-
border-radius: 8px !important;
|
|
422
|
-
padding: 10px 14px !important;
|
|
423
|
-
box-shadow: 0 8px 24px rgba(0,0,0,0.5) !important;
|
|
424
|
-
backdrop-filter: blur(4px);
|
|
425
|
-
}}
|
|
426
|
-
</style>
|
|
427
|
-
</head>
|
|
428
|
-
<body>
|
|
429
|
-
|
|
430
|
-
<div id="header">
|
|
431
|
-
<h1>Code<span>Graph</span> AI</h1>
|
|
432
|
-
<div id="stats">
|
|
433
|
-
<span>Nodes: <b id="node-count">-</b></span>
|
|
434
|
-
<span>Edges: <b id="edge-count">-</b></span>
|
|
435
|
-
</div>
|
|
436
|
-
</div>
|
|
437
|
-
|
|
438
|
-
<div id="legend">
|
|
439
|
-
<span style="font-size:11px; color:#546E7A; font-weight: 700; margin-right:4px; letter-spacing: 0.05em;">NODES</span>
|
|
440
|
-
<div class="legend-item"><div class="legend-dot" style="background:#4A90E2;border-radius:2px;transform:rotate(45deg)"></div>File</div>
|
|
441
|
-
<div class="legend-item"><div class="legend-dot" style="background:#F5A623"></div>Class</div>
|
|
442
|
-
<div class="legend-item"><div class="legend-dot" style="background:#50C878"></div>Function</div>
|
|
443
|
-
<div class="legend-item"><div class="legend-dot" style="background:#7ED6A8"></div>Method</div>
|
|
444
|
-
<div class="legend-item"><div class="legend-dot" style="background:#B0BEC5;border-radius:2px"></div>Module</div>
|
|
445
|
-
<div class="legend-item"><div class="legend-dot" style="background:#FFD700;clip-path:polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)"></div>Dataset</div>
|
|
446
|
-
<div class="legend-item"><div class="legend-dot" style="background:#FF69B4;border-radius:2px"></div>Database</div>
|
|
447
|
-
<div class="legend-item"><div class="legend-dot" style="background:#9B59B6;clip-path:polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)"></div>Document</div>
|
|
448
|
-
<div class="legend-item"><div class="legend-dot" style="background:#5DADE2;clip-path:polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)"></div>Image</div>
|
|
449
|
-
<span style="font-size:11px; color:#546E7A; font-weight: 700; margin-left:12px; margin-right:4px; letter-spacing: 0.05em;">EDGES</span>
|
|
450
|
-
<div class="legend-item"><div style="width:20px;height:2px;background:#4A90E2;opacity:0.6"></div>contains</div>
|
|
451
|
-
<div class="legend-item"><div style="width:20px;height:2px;background:#50C878;opacity:0.6"></div>calls</div>
|
|
452
|
-
<div class="legend-item"><div style="width:20px;height:1px;background:#B0BEC5;border-top:1px dashed #B0BEC5"></div>imports</div>
|
|
453
|
-
<div class="legend-item"><div style="width:20px;height:2px;background:#FFD700;opacity:0.6"></div>uses</div>
|
|
454
|
-
</div>
|
|
455
|
-
|
|
456
|
-
<div id="graph-container">
|
|
457
|
-
<div id="graph"></div>
|
|
458
|
-
</div>
|
|
459
|
-
|
|
460
|
-
<script>
|
|
461
|
-
const nodesRaw = {nodes_json};
|
|
462
|
-
nodesRaw.forEach(node => {{
|
|
463
|
-
if (node.title) {{
|
|
464
|
-
const div = document.createElement("div");
|
|
465
|
-
div.innerHTML = node.title;
|
|
466
|
-
node.title = div;
|
|
467
|
-
}}
|
|
468
|
-
}});
|
|
469
|
-
|
|
470
|
-
const nodes = new vis.DataSet(nodesRaw);
|
|
471
|
-
const edges = new vis.DataSet({edges_json});
|
|
472
|
-
|
|
473
|
-
document.getElementById("node-count").textContent = nodes.length;
|
|
474
|
-
document.getElementById("edge-count").textContent = edges.length;
|
|
475
|
-
|
|
476
|
-
const container = document.getElementById("graph");
|
|
477
|
-
const options = {{
|
|
478
|
-
physics: {{
|
|
479
|
-
enabled: true,
|
|
480
|
-
solver: "forceAtlas2Based",
|
|
481
|
-
forceAtlas2Based: {{
|
|
482
|
-
gravitationalConstant: -60,
|
|
483
|
-
centralGravity: 0.005,
|
|
484
|
-
springLength: 180,
|
|
485
|
-
springConstant: 0.04,
|
|
486
|
-
damping: 0.4,
|
|
487
|
-
avoidOverlap: 0.8
|
|
488
|
-
}},
|
|
489
|
-
maxVelocity: 50,
|
|
490
|
-
minVelocity: 0.5,
|
|
491
|
-
timestep: 0.3,
|
|
492
|
-
stabilization: {{ iterations: 200, updateInterval: 25 }}
|
|
493
|
-
}},
|
|
494
|
-
interaction: {{
|
|
495
|
-
hover: true,
|
|
496
|
-
tooltipDelay: 150,
|
|
497
|
-
navigationButtons: true,
|
|
498
|
-
keyboard: true,
|
|
499
|
-
zoomView: true,
|
|
500
|
-
hideEdgesOnDrag: false,
|
|
501
|
-
hideNodesOnDrag: false
|
|
502
|
-
}},
|
|
503
|
-
edges: {{
|
|
504
|
-
smooth: {{ type: "curvedCW", roundness: 0.15 }},
|
|
505
|
-
font: {{ strokeWidth: 2, strokeColor: "#0F1117", size: 11 }}
|
|
506
|
-
}},
|
|
507
|
-
nodes: {{
|
|
508
|
-
font: {{
|
|
509
|
-
size: 14,
|
|
510
|
-
face: "'Inter', system-ui, sans-serif",
|
|
511
|
-
strokeWidth: 3,
|
|
512
|
-
strokeColor: "#0F1117",
|
|
513
|
-
color: "#ffffff"
|
|
514
|
-
}},
|
|
515
|
-
shadow: {{
|
|
516
|
-
enabled: true,
|
|
517
|
-
color: "rgba(0,0,0,0.3)",
|
|
518
|
-
size: 10,
|
|
519
|
-
x: 0,
|
|
520
|
-
y: 0
|
|
521
|
-
}}
|
|
522
|
-
}}
|
|
523
|
-
}};
|
|
524
|
-
|
|
525
|
-
const network = new vis.Network(container, {{ nodes, edges }}, options);
|
|
526
|
-
network.once("stabilized", () => {{
|
|
527
|
-
network.setOptions({{ physics: {{ enabled: false }} }});
|
|
528
|
-
}});
|
|
529
|
-
</script>
|
|
530
|
-
</body>
|
|
531
|
-
</html>"""
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
def _darken(hex_color: str) -> str:
|
|
535
|
-
"""Darken a hex color by ~15% for border contrast."""
|
|
536
|
-
try:
|
|
537
|
-
h = hex_color.lstrip("#")
|
|
538
|
-
r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
|
539
|
-
r, g, b = max(0, r - 35), max(0, g - 35), max(0, b - 35)
|
|
540
|
-
return f"#{r:02x}{g:02x}{b:02x}"
|
|
541
|
-
except Exception:
|
|
542
|
-
return hex_color
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
@app.command()
|
|
546
|
-
def ask(query: str = typer.Argument(..., help="Question about your codebase")):
|
|
547
|
-
"""Ask a question about your codebase (coming soon)."""
|
|
548
|
-
typer.echo(f"ask: coming soon — query was: {query}")
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
@app.command()
|
|
552
|
-
def explain(file: str = typer.Argument(..., help="File to explain")):
|
|
553
|
-
"""Get an AI-generated explanation of a file (coming soon)."""
|
|
554
|
-
typer.echo(f"explain: coming soon — file was: {file}")
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
@app.command()
|
|
558
|
-
def audit():
|
|
559
|
-
"""Audit the codebase for missing docs, unused code, hidden deps (coming soon)."""
|
|
560
|
-
typer.echo("audit: coming soon")
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if __name__ == "__main__":
|
|
564
|
-
app()
|
|
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
|
{codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/codegraph_cli_ai.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{codegraph_cli_ai-0.1.8 → codegraph_cli_ai-0.1.9}/codegraph_cli_ai.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|