codegraph-cli-ai 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraph_cli_ai-0.1.0/PKG-INFO +16 -0
- codegraph_cli_ai-0.1.0/codegraph/cli.py +454 -0
- codegraph_cli_ai-0.1.0/codegraph/graph/builder.py +110 -0
- codegraph_cli_ai-0.1.0/codegraph/parsers/python_parser.py +125 -0
- codegraph_cli_ai-0.1.0/codegraph_cli_ai.egg-info/PKG-INFO +16 -0
- codegraph_cli_ai-0.1.0/codegraph_cli_ai.egg-info/SOURCES.txt +10 -0
- codegraph_cli_ai-0.1.0/codegraph_cli_ai.egg-info/dependency_links.txt +1 -0
- codegraph_cli_ai-0.1.0/codegraph_cli_ai.egg-info/entry_points.txt +2 -0
- codegraph_cli_ai-0.1.0/codegraph_cli_ai.egg-info/requires.txt +3 -0
- codegraph_cli_ai-0.1.0/codegraph_cli_ai.egg-info/top_level.txt +1 -0
- codegraph_cli_ai-0.1.0/pyproject.toml +29 -0
- codegraph_cli_ai-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codegraph-cli-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tool to analyze codebases and visualize knowledge graphs using AST
|
|
5
|
+
Author: Aditya Jogdand
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: code-analysis,ast,graph,cli,developer-tools
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: typer>=0.9.0
|
|
15
|
+
Requires-Dist: networkx>=3.0
|
|
16
|
+
Requires-Dist: pyvis>=0.3.2
|
|
@@ -0,0 +1,454 @@
|
|
|
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.graph.builder import GraphBuilder
|
|
13
|
+
|
|
14
|
+
app = typer.Typer()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.callback()
|
|
18
|
+
def main():
|
|
19
|
+
"""CodeGraph AI - Understand your codebase using graphs and AI."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command()
|
|
24
|
+
def index(
|
|
25
|
+
path: str = typer.Argument(".", help="Path to the repo or folder to index")
|
|
26
|
+
):
|
|
27
|
+
"""Scan a directory, parse all Python files, and save the knowledge graph."""
|
|
28
|
+
root = Path(path).resolve()
|
|
29
|
+
|
|
30
|
+
if not root.exists():
|
|
31
|
+
typer.echo(f"[error] Path does not exist: {root}", err=True)
|
|
32
|
+
raise typer.Exit(code=1)
|
|
33
|
+
|
|
34
|
+
typer.echo(f"Indexing: {root}\n")
|
|
35
|
+
py_files = list(root.rglob("*.py"))
|
|
36
|
+
|
|
37
|
+
if not py_files:
|
|
38
|
+
typer.echo("No Python files found.")
|
|
39
|
+
raise typer.Exit()
|
|
40
|
+
|
|
41
|
+
typer.echo(f"Found {len(py_files)} Python file(s)\n")
|
|
42
|
+
|
|
43
|
+
# Step 1 — Parse
|
|
44
|
+
parser = PythonParser()
|
|
45
|
+
parsed_files = []
|
|
46
|
+
failed_files = []
|
|
47
|
+
|
|
48
|
+
for filepath in py_files:
|
|
49
|
+
result = parser.parse_file(str(filepath))
|
|
50
|
+
if result.errors:
|
|
51
|
+
failed_files.append((str(filepath), result.errors))
|
|
52
|
+
else:
|
|
53
|
+
typer.echo(f" ✔ {filepath.relative_to(root)}")
|
|
54
|
+
parsed_files.append(result)
|
|
55
|
+
|
|
56
|
+
# Step 2 — Build graph
|
|
57
|
+
typer.echo("\nBuilding graph...")
|
|
58
|
+
builder = GraphBuilder()
|
|
59
|
+
builder.build(parsed_files)
|
|
60
|
+
summary = builder.summary()
|
|
61
|
+
|
|
62
|
+
# Step 3 — Save to .codegraph/graph.json
|
|
63
|
+
output_dir = root / ".codegraph"
|
|
64
|
+
output_dir.mkdir(exist_ok=True)
|
|
65
|
+
output_file = output_dir / "graph.json"
|
|
66
|
+
with output_file.open("w", encoding="utf-8") as fp:
|
|
67
|
+
json.dump(builder.to_dict(), fp, indent=2)
|
|
68
|
+
|
|
69
|
+
# Step 4 — Summary
|
|
70
|
+
typer.echo("\n" + "=" * 50)
|
|
71
|
+
typer.echo("Index complete")
|
|
72
|
+
typer.echo(f" Graph saved : {output_file}")
|
|
73
|
+
typer.echo(f" Files parsed : {len(parsed_files)}")
|
|
74
|
+
typer.echo(f" Graph nodes : {summary['total_nodes']}")
|
|
75
|
+
typer.echo(f" Graph edges : {summary['total_edges']}")
|
|
76
|
+
typer.echo("\n Node breakdown:")
|
|
77
|
+
for kind, count in summary["nodes_by_kind"].items():
|
|
78
|
+
typer.echo(f" {kind:<12}: {count}")
|
|
79
|
+
typer.echo("\n Edge breakdown:")
|
|
80
|
+
for rel, count in summary["edges_by_relation"].items():
|
|
81
|
+
typer.echo(f" {rel:<12}: {count}")
|
|
82
|
+
|
|
83
|
+
if failed_files:
|
|
84
|
+
typer.echo(f"\n Failed files : {len(failed_files)}")
|
|
85
|
+
for fp, errors in failed_files:
|
|
86
|
+
typer.echo(f" {fp}: {errors}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command()
|
|
90
|
+
def plot(
|
|
91
|
+
hide_external: bool = typer.Option(False, "--hide-external", help="Hide external/stdlib nodes"),
|
|
92
|
+
level: Optional[str] = typer.Option(None, "--level", help="Show only: file, function, class, method"),
|
|
93
|
+
focus: Optional[str] = typer.Option(None, "--focus", help="Focus on a specific file (e.g. cli.py)"),
|
|
94
|
+
edge_type: Optional[str] = typer.Option(None, "--edge-type", help="Filter edges: calls, imports, contains, all"),
|
|
95
|
+
):
|
|
96
|
+
"""Visualize the knowledge graph as a premium interactive HTML file."""
|
|
97
|
+
root = Path(".").resolve()
|
|
98
|
+
graph_file = root / ".codegraph" / "graph.json"
|
|
99
|
+
|
|
100
|
+
if not graph_file.exists():
|
|
101
|
+
typer.echo("[error] No graph found. Run 'codegraph index' first.", err=True)
|
|
102
|
+
raise typer.Exit(code=1)
|
|
103
|
+
|
|
104
|
+
typer.echo(f"Loading graph from {graph_file}...")
|
|
105
|
+
with graph_file.open("r", encoding="utf-8") as f:
|
|
106
|
+
data = json.load(f)
|
|
107
|
+
|
|
108
|
+
# Build NetworkX graph
|
|
109
|
+
G = nx.DiGraph()
|
|
110
|
+
for node in data.get("nodes", []):
|
|
111
|
+
G.add_node(node["id"], **{k: v for k, v in node.items() if k != "id"})
|
|
112
|
+
for edge in data.get("edges", []):
|
|
113
|
+
G.add_edge(edge["source"], edge["target"], **{k: v for k, v in edge.items() if k not in ["source", "target"]})
|
|
114
|
+
|
|
115
|
+
# --- Apply filters ---
|
|
116
|
+
|
|
117
|
+
# 1. Hide external nodes
|
|
118
|
+
if hide_external:
|
|
119
|
+
remove = [n for n, d in G.nodes(data=True) if d.get("external", False)]
|
|
120
|
+
G.remove_nodes_from(remove)
|
|
121
|
+
|
|
122
|
+
# 2. Filter by level (node kind)
|
|
123
|
+
if level:
|
|
124
|
+
allowed = set(level.split(","))
|
|
125
|
+
remove = [n for n, d in G.nodes(data=True) if d.get("kind") not in allowed]
|
|
126
|
+
G.remove_nodes_from(remove)
|
|
127
|
+
|
|
128
|
+
# 3. Focus on a specific file — keep only that file + its direct neighbors
|
|
129
|
+
if focus:
|
|
130
|
+
focus_id = f"file:{focus}"
|
|
131
|
+
if focus_id in G:
|
|
132
|
+
keep = {focus_id} | set(G.successors(focus_id)) | set(G.predecessors(focus_id))
|
|
133
|
+
remove = [n for n in G.nodes if n not in keep]
|
|
134
|
+
G.remove_nodes_from(remove)
|
|
135
|
+
else:
|
|
136
|
+
typer.echo(f"[warn] File '{focus}' not found in graph. Showing full graph.")
|
|
137
|
+
|
|
138
|
+
# 4. Filter edge types
|
|
139
|
+
if edge_type and edge_type != "all":
|
|
140
|
+
allowed_edges = set(edge_type.split(","))
|
|
141
|
+
remove_edges = [(s, t) for s, t, d in G.edges(data=True) if d.get("relation") not in allowed_edges]
|
|
142
|
+
G.remove_edges_from(remove_edges)
|
|
143
|
+
# Remove orphan nodes after edge removal
|
|
144
|
+
G.remove_nodes_from(list(nx.isolates(G)))
|
|
145
|
+
|
|
146
|
+
typer.echo(f"Rendering {G.number_of_nodes()} nodes, {G.number_of_edges()} edges...")
|
|
147
|
+
|
|
148
|
+
# --- Build HTML visualization ---
|
|
149
|
+
html = _build_premium_html(G)
|
|
150
|
+
|
|
151
|
+
output_path = root / "graph.html"
|
|
152
|
+
output_path.write_text(html, encoding="utf-8")
|
|
153
|
+
|
|
154
|
+
typer.echo(f"Saved to: {output_path}")
|
|
155
|
+
typer.echo("Opening in browser...")
|
|
156
|
+
webbrowser.open(f"file://{output_path.resolve()}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _build_premium_html(G: nx.DiGraph) -> str:
|
|
160
|
+
"""Generate a self-contained premium HTML visualization using vis.js."""
|
|
161
|
+
|
|
162
|
+
# --- Node styling ---
|
|
163
|
+
STYLES = {
|
|
164
|
+
"file": {"color": "#4A90E2", "shape": "diamond", "size": 28, "font_color": "#ffffff"},
|
|
165
|
+
"class": {"color": "#F5A623", "shape": "hexagon", "size": 24, "font_color": "#ffffff"},
|
|
166
|
+
"function": {"color": "#50C878", "shape": "dot", "size": 16, "font_color": "#ffffff"},
|
|
167
|
+
"method": {"color": "#7ED6A8", "shape": "dot", "size": 14, "font_color": "#ffffff"},
|
|
168
|
+
"module": {"color": "#B0BEC5", "shape": "box", "size": 14, "font_color": "#ffffff"},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
EDGE_COLORS = {
|
|
172
|
+
"contains": "#4A90E2",
|
|
173
|
+
"calls": "#50C878",
|
|
174
|
+
"imports": "#B0BEC5",
|
|
175
|
+
"defined_in": "#E8E8E8",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
nodes_js = []
|
|
179
|
+
for node_id, attrs in G.nodes(data=True):
|
|
180
|
+
kind = attrs.get("kind", "function")
|
|
181
|
+
label = attrs.get("label", node_id)
|
|
182
|
+
external = attrs.get("external", False)
|
|
183
|
+
style = STYLES.get(kind, STYLES["function"])
|
|
184
|
+
|
|
185
|
+
color = "#CFD8DC" if external else style["color"]
|
|
186
|
+
font_color = "#B0BEC5" if external else style["font_color"]
|
|
187
|
+
size = max(style["size"] - 4, 10) if external else style["size"]
|
|
188
|
+
|
|
189
|
+
# Tooltip
|
|
190
|
+
tooltip_parts = [f"<b>{label}</b>", f"Kind: {kind}"]
|
|
191
|
+
if attrs.get("file"):
|
|
192
|
+
tooltip_parts.append(f"File: {attrs['file'].replace('file:', '')}")
|
|
193
|
+
if attrs.get("cls"):
|
|
194
|
+
tooltip_parts.append(f"Class: {attrs['cls']}")
|
|
195
|
+
if external:
|
|
196
|
+
tooltip_parts.append("<i>external</i>")
|
|
197
|
+
tooltip = "<br>".join(tooltip_parts)
|
|
198
|
+
|
|
199
|
+
nodes_js.append(f"""{{
|
|
200
|
+
id: {json.dumps(node_id)},
|
|
201
|
+
label: {json.dumps(label)},
|
|
202
|
+
title: {json.dumps(tooltip)},
|
|
203
|
+
color: {{
|
|
204
|
+
background: {json.dumps(color)},
|
|
205
|
+
border: {json.dumps(_darken(color))},
|
|
206
|
+
highlight: {{ background: "#FFE082", border: "#FFA000" }},
|
|
207
|
+
hover: {{ background: "#E3F2FD", border: "#1E88E5" }}
|
|
208
|
+
}},
|
|
209
|
+
shape: {json.dumps(style["shape"])},
|
|
210
|
+
size: {size},
|
|
211
|
+
font: {{ color: {json.dumps(font_color)}, size: 13, face: "Inter, system-ui, sans-serif" }},
|
|
212
|
+
borderWidth: 1.5,
|
|
213
|
+
shadow: {{ enabled: true, color: "rgba(0,0,0,0.12)", size: 8, x: 2, y: 2 }}
|
|
214
|
+
}}""")
|
|
215
|
+
|
|
216
|
+
edges_js = []
|
|
217
|
+
for src, dst, attrs in G.edges(data=True):
|
|
218
|
+
relation = attrs.get("relation", "")
|
|
219
|
+
color = EDGE_COLORS.get(relation, "#BDBDBD")
|
|
220
|
+
dashed = relation in ("defined_in", "imports")
|
|
221
|
+
|
|
222
|
+
edges_js.append(f"""{{
|
|
223
|
+
from: {json.dumps(src)},
|
|
224
|
+
to: {json.dumps(dst)},
|
|
225
|
+
label: {json.dumps(relation)},
|
|
226
|
+
color: {{ color: {json.dumps(color)}, opacity: 0.7 }},
|
|
227
|
+
dashes: {"true" if dashed else "false"},
|
|
228
|
+
width: {"1" if dashed else "1.5"},
|
|
229
|
+
font: {{ color: "#9E9E9E", size: 10, face: "Inter, system-ui, sans-serif", align: "middle" }},
|
|
230
|
+
arrows: {{ to: {{ enabled: true, scaleFactor: 0.6 }} }},
|
|
231
|
+
smooth: {{ type: "curvedCW", roundness: 0.2 }}
|
|
232
|
+
}}""")
|
|
233
|
+
|
|
234
|
+
nodes_json = "[\n" + ",\n".join(nodes_js) + "\n]"
|
|
235
|
+
edges_json = "[\n" + ",\n".join(edges_js) + "\n]"
|
|
236
|
+
|
|
237
|
+
return f"""<!DOCTYPE html>
|
|
238
|
+
<html lang="en">
|
|
239
|
+
<head>
|
|
240
|
+
<meta charset="UTF-8">
|
|
241
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
242
|
+
<title>CodeGraph AI</title>
|
|
243
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
244
|
+
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
|
245
|
+
<style>
|
|
246
|
+
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
247
|
+
body {{
|
|
248
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
249
|
+
background: #0F1117;
|
|
250
|
+
color: #E0E0E0;
|
|
251
|
+
height: 100vh;
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-direction: column;
|
|
254
|
+
overflow: hidden;
|
|
255
|
+
}}
|
|
256
|
+
|
|
257
|
+
/* Background effect */
|
|
258
|
+
#graph-container::before {{
|
|
259
|
+
content: "";
|
|
260
|
+
position: absolute;
|
|
261
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
262
|
+
background-image: radial-gradient(circle at 2px 2px, rgba(255,255,255,0.03) 1px, transparent 0);
|
|
263
|
+
background-size: 32px 32px;
|
|
264
|
+
pointer-events: none;
|
|
265
|
+
z-index: 0;
|
|
266
|
+
}}
|
|
267
|
+
|
|
268
|
+
#graph-container::after {{
|
|
269
|
+
content: "";
|
|
270
|
+
position: absolute;
|
|
271
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
272
|
+
background: radial-gradient(circle at center, transparent 0%, rgba(15,17,23,0.4) 100%);
|
|
273
|
+
pointer-events: none;
|
|
274
|
+
z-index: 1;
|
|
275
|
+
}}
|
|
276
|
+
|
|
277
|
+
/* Header */
|
|
278
|
+
#header {{
|
|
279
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
280
|
+
padding: 16px 24px;
|
|
281
|
+
background: rgba(26, 29, 39, 0.8);
|
|
282
|
+
backdrop-filter: blur(10px);
|
|
283
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
284
|
+
flex-shrink: 0;
|
|
285
|
+
z-index: 10;
|
|
286
|
+
}}
|
|
287
|
+
#header h1 {{ font-size: 18px; font-weight: 600; color: #fff; letter-spacing: -0.01em; }}
|
|
288
|
+
#header h1 span {{ color: #4A90E2; }}
|
|
289
|
+
#stats {{ display: flex; gap: 20px; font-size: 13px; color: #78909C; }}
|
|
290
|
+
#stats b {{ color: #ffffff; }}
|
|
291
|
+
|
|
292
|
+
/* Legend */
|
|
293
|
+
#legend {{
|
|
294
|
+
display: flex; gap: 16px; align-items: center;
|
|
295
|
+
padding: 10px 24px;
|
|
296
|
+
background: rgba(26, 29, 39, 0.6);
|
|
297
|
+
backdrop-filter: blur(8px);
|
|
298
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
299
|
+
flex-shrink: 0;
|
|
300
|
+
flex-wrap: wrap;
|
|
301
|
+
z-index: 10;
|
|
302
|
+
}}
|
|
303
|
+
.legend-item {{ display: flex; align-items: center; gap: 8px; font-size: 12px; color: #B0BEC5; font-weight: 500; }}
|
|
304
|
+
.legend-dot {{ width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }}
|
|
305
|
+
|
|
306
|
+
/* Graph */
|
|
307
|
+
#graph-container {{ flex: 1; position: relative; background: #0F1117; }}
|
|
308
|
+
#graph {{ position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 2; }}
|
|
309
|
+
|
|
310
|
+
/* Tooltip override */
|
|
311
|
+
.vis-tooltip {{
|
|
312
|
+
background: #1E2130 !important;
|
|
313
|
+
border: 1px solid #2A2D3A !important;
|
|
314
|
+
color: #E0E0E0 !important;
|
|
315
|
+
font-size: 13px !important;
|
|
316
|
+
border-radius: 8px !important;
|
|
317
|
+
padding: 10px 14px !important;
|
|
318
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.5) !important;
|
|
319
|
+
backdrop-filter: blur(4px);
|
|
320
|
+
}}
|
|
321
|
+
</style>
|
|
322
|
+
</head>
|
|
323
|
+
<body>
|
|
324
|
+
|
|
325
|
+
<div id="header">
|
|
326
|
+
<h1>Code<span>Graph</span> AI</h1>
|
|
327
|
+
<div id="stats">
|
|
328
|
+
<span>Nodes: <b id="node-count">-</b></span>
|
|
329
|
+
<span>Edges: <b id="edge-count">-</b></span>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<div id="legend">
|
|
334
|
+
<span style="font-size:11px; color:#546E7A; font-weight: 700; margin-right:4px; letter-spacing: 0.05em;">NODES</span>
|
|
335
|
+
<div class="legend-item"><div class="legend-dot" style="background:#4A90E2;border-radius:2px;transform:rotate(45deg)"></div>File</div>
|
|
336
|
+
<div class="legend-item"><div class="legend-dot" style="background:#F5A623"></div>Class</div>
|
|
337
|
+
<div class="legend-item"><div class="legend-dot" style="background:#50C878"></div>Function</div>
|
|
338
|
+
<div class="legend-item"><div class="legend-dot" style="background:#7ED6A8"></div>Method</div>
|
|
339
|
+
<div class="legend-item"><div class="legend-dot" style="background:#B0BEC5;border-radius:2px"></div>Module</div>
|
|
340
|
+
<span style="font-size:11px; color:#546E7A; font-weight: 700; margin-left:12px; margin-right:4px; letter-spacing: 0.05em;">EDGES</span>
|
|
341
|
+
<div class="legend-item"><div style="width:20px;height:2px;background:#4A90E2;opacity:0.6"></div>contains</div>
|
|
342
|
+
<div class="legend-item"><div style="width:20px;height:2px;background:#50C878;opacity:0.6"></div>calls</div>
|
|
343
|
+
<div class="legend-item"><div style="width:20px;height:1px;background:#B0BEC5;border-top:1px dashed #B0BEC5"></div>imports</div>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<div id="graph-container">
|
|
347
|
+
<div id="graph"></div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<script>
|
|
351
|
+
const nodesRaw = {nodes_json};
|
|
352
|
+
nodesRaw.forEach(node => {{
|
|
353
|
+
if (node.title) {{
|
|
354
|
+
const div = document.createElement("div");
|
|
355
|
+
div.innerHTML = node.title;
|
|
356
|
+
node.title = div;
|
|
357
|
+
}}
|
|
358
|
+
}});
|
|
359
|
+
|
|
360
|
+
const nodes = new vis.DataSet(nodesRaw);
|
|
361
|
+
const edges = new vis.DataSet({edges_json});
|
|
362
|
+
|
|
363
|
+
document.getElementById("node-count").textContent = nodes.length;
|
|
364
|
+
document.getElementById("edge-count").textContent = edges.length;
|
|
365
|
+
|
|
366
|
+
const container = document.getElementById("graph");
|
|
367
|
+
const options = {{
|
|
368
|
+
physics: {{
|
|
369
|
+
enabled: true,
|
|
370
|
+
solver: "forceAtlas2Based",
|
|
371
|
+
forceAtlas2Based: {{
|
|
372
|
+
gravitationalConstant: -60,
|
|
373
|
+
centralGravity: 0.005,
|
|
374
|
+
springLength: 180,
|
|
375
|
+
springConstant: 0.04,
|
|
376
|
+
damping: 0.4,
|
|
377
|
+
avoidOverlap: 0.8
|
|
378
|
+
}},
|
|
379
|
+
maxVelocity: 50,
|
|
380
|
+
minVelocity: 0.5,
|
|
381
|
+
timestep: 0.3,
|
|
382
|
+
stabilization: {{ iterations: 200, updateInterval: 25 }}
|
|
383
|
+
}},
|
|
384
|
+
interaction: {{
|
|
385
|
+
hover: true,
|
|
386
|
+
tooltipDelay: 150,
|
|
387
|
+
navigationButtons: true,
|
|
388
|
+
keyboard: true,
|
|
389
|
+
zoomView: true,
|
|
390
|
+
hideEdgesOnDrag: false,
|
|
391
|
+
hideNodesOnDrag: false
|
|
392
|
+
}},
|
|
393
|
+
edges: {{
|
|
394
|
+
smooth: {{ type: "curvedCW", roundness: 0.15 }},
|
|
395
|
+
font: {{ strokeWidth: 2, strokeColor: "#0F1117", size: 11 }}
|
|
396
|
+
}},
|
|
397
|
+
nodes: {{
|
|
398
|
+
font: {{
|
|
399
|
+
size: 14,
|
|
400
|
+
face: "'Inter', system-ui, sans-serif",
|
|
401
|
+
strokeWidth: 3,
|
|
402
|
+
strokeColor: "#0F1117",
|
|
403
|
+
color: "#ffffff"
|
|
404
|
+
}},
|
|
405
|
+
shadow: {{
|
|
406
|
+
enabled: true,
|
|
407
|
+
color: "rgba(0,0,0,0.3)",
|
|
408
|
+
size: 10,
|
|
409
|
+
x: 0,
|
|
410
|
+
y: 0
|
|
411
|
+
}}
|
|
412
|
+
}}
|
|
413
|
+
}};
|
|
414
|
+
|
|
415
|
+
const network = new vis.Network(container, {{ nodes, edges }}, options);
|
|
416
|
+
network.once("stabilized", () => {{
|
|
417
|
+
network.setOptions({{ physics: {{ enabled: false }} }});
|
|
418
|
+
}});
|
|
419
|
+
</script>
|
|
420
|
+
</body>
|
|
421
|
+
</html>"""
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _darken(hex_color: str) -> str:
|
|
425
|
+
"""Darken a hex color by ~15% for border contrast."""
|
|
426
|
+
try:
|
|
427
|
+
h = hex_color.lstrip("#")
|
|
428
|
+
r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
|
429
|
+
r, g, b = max(0, r - 35), max(0, g - 35), max(0, b - 35)
|
|
430
|
+
return f"#{r:02x}{g:02x}{b:02x}"
|
|
431
|
+
except Exception:
|
|
432
|
+
return hex_color
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@app.command()
|
|
436
|
+
def ask(query: str = typer.Argument(..., help="Question about your codebase")):
|
|
437
|
+
"""Ask a question about your codebase (coming soon)."""
|
|
438
|
+
typer.echo(f"ask: coming soon — query was: {query}")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@app.command()
|
|
442
|
+
def explain(file: str = typer.Argument(..., help="File to explain")):
|
|
443
|
+
"""Get an AI-generated explanation of a file (coming soon)."""
|
|
444
|
+
typer.echo(f"explain: coming soon — file was: {file}")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@app.command()
|
|
448
|
+
def audit():
|
|
449
|
+
"""Audit the codebase for missing docs, unused code, hidden deps (coming soon)."""
|
|
450
|
+
typer.echo("audit: coming soon")
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
if __name__ == "__main__":
|
|
454
|
+
app()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Graph Builder for CodeGraph AI
|
|
3
|
+
|
|
4
|
+
Node types:
|
|
5
|
+
- file : a .py file
|
|
6
|
+
- function : top-level function
|
|
7
|
+
- class : a class
|
|
8
|
+
- method : a method belonging to a class
|
|
9
|
+
- module : an imported module/package
|
|
10
|
+
|
|
11
|
+
Edge types:
|
|
12
|
+
- contains : file → function, file → class, class → method
|
|
13
|
+
- calls : function/method → function/method
|
|
14
|
+
- imports : file → module
|
|
15
|
+
- defined_in : function/method → file
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import builtins
|
|
19
|
+
import networkx as nx
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from codegraph.parsers.python_parser import ParsedFile
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
BUILTIN_FUNCTIONS = set(dir(builtins))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GraphBuilder:
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.graph = nx.DiGraph()
|
|
30
|
+
self._function_to_file: dict[str, str] = {}
|
|
31
|
+
|
|
32
|
+
def add_file(self, parsed: ParsedFile) -> None:
|
|
33
|
+
file_id = self._file_node_id(parsed.filepath)
|
|
34
|
+
filename = Path(parsed.filepath).name
|
|
35
|
+
|
|
36
|
+
self._add_node(file_id, kind="file", label=filename, external=False)
|
|
37
|
+
|
|
38
|
+
for cls in parsed.classes:
|
|
39
|
+
cls_id = f"class:{cls}"
|
|
40
|
+
self._add_node(cls_id, kind="class", label=cls, external=False, file=file_id)
|
|
41
|
+
self._add_edge(file_id, cls_id, relation="contains")
|
|
42
|
+
|
|
43
|
+
for func in parsed.functions:
|
|
44
|
+
func_id = f"func:{func}"
|
|
45
|
+
self._add_node(func_id, kind="function", label=func, external=False, file=file_id)
|
|
46
|
+
self._add_edge(file_id, func_id, relation="contains")
|
|
47
|
+
self._add_edge(func_id, file_id, relation="defined_in")
|
|
48
|
+
self._function_to_file[func] = file_id
|
|
49
|
+
|
|
50
|
+
for cls_name, method_name in parsed.methods:
|
|
51
|
+
cls_id = f"class:{cls_name}"
|
|
52
|
+
method_id = f"func:{method_name}"
|
|
53
|
+
self._add_node(method_id, kind="method", label=method_name, external=False, file=file_id, cls=cls_name)
|
|
54
|
+
self._add_edge(cls_id, method_id, relation="contains")
|
|
55
|
+
self._add_edge(method_id, file_id, relation="defined_in")
|
|
56
|
+
self._function_to_file[method_name] = file_id
|
|
57
|
+
|
|
58
|
+
for imp in parsed.imports:
|
|
59
|
+
mod_id = f"module:{imp}"
|
|
60
|
+
self._add_node(mod_id, kind="module", label=imp, external=True)
|
|
61
|
+
self._add_edge(file_id, mod_id, relation="imports")
|
|
62
|
+
|
|
63
|
+
for caller, callee in parsed.calls:
|
|
64
|
+
if callee in BUILTIN_FUNCTIONS:
|
|
65
|
+
continue
|
|
66
|
+
caller_id = f"func:{caller}"
|
|
67
|
+
callee_id = f"func:{callee}"
|
|
68
|
+
if not self.graph.has_node(callee_id):
|
|
69
|
+
self._add_node(callee_id, kind="function", label=callee, external=True)
|
|
70
|
+
self._add_edge(caller_id, callee_id, relation="calls")
|
|
71
|
+
|
|
72
|
+
def build(self, parsed_files: list[ParsedFile]) -> nx.DiGraph:
|
|
73
|
+
for parsed in parsed_files:
|
|
74
|
+
if not parsed.errors:
|
|
75
|
+
self.add_file(parsed)
|
|
76
|
+
return self.graph
|
|
77
|
+
|
|
78
|
+
def summary(self) -> dict:
|
|
79
|
+
nodes_by_kind = {}
|
|
80
|
+
for _, data in self.graph.nodes(data=True):
|
|
81
|
+
kind = data.get("kind", "unknown")
|
|
82
|
+
nodes_by_kind[kind] = nodes_by_kind.get(kind, 0) + 1
|
|
83
|
+
|
|
84
|
+
edges_by_relation = {}
|
|
85
|
+
for _, _, data in self.graph.edges(data=True):
|
|
86
|
+
rel = data.get("relation", "unknown")
|
|
87
|
+
edges_by_relation[rel] = edges_by_relation.get(rel, 0) + 1
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"total_nodes": self.graph.number_of_nodes(),
|
|
91
|
+
"total_edges": self.graph.number_of_edges(),
|
|
92
|
+
"nodes_by_kind": nodes_by_kind,
|
|
93
|
+
"edges_by_relation": edges_by_relation,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> dict:
|
|
97
|
+
return {
|
|
98
|
+
"nodes": [{"id": node, **data} for node, data in self.graph.nodes(data=True)],
|
|
99
|
+
"edges": [{"source": src, "target": dst, **data} for src, dst, data in self.graph.edges(data=True)],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
def _file_node_id(self, filepath: str) -> str:
|
|
103
|
+
return f"file:{Path(filepath).name}"
|
|
104
|
+
|
|
105
|
+
def _add_node(self, node_id: str, **attrs) -> None:
|
|
106
|
+
if not self.graph.has_node(node_id):
|
|
107
|
+
self.graph.add_node(node_id, **attrs)
|
|
108
|
+
|
|
109
|
+
def _add_edge(self, src: str, dst: str, **attrs) -> None:
|
|
110
|
+
self.graph.add_edge(src, dst, **attrs)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python AST Parser for CodeGraph AI
|
|
3
|
+
Extracts functions, classes, imports, and call graphs from Python files.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ParsedFile:
|
|
15
|
+
filepath: str
|
|
16
|
+
functions: list[str] = field(default_factory=list) # top-level only
|
|
17
|
+
classes: list[str] = field(default_factory=list)
|
|
18
|
+
methods: list[tuple[str, str]] = field(default_factory=list) # (class_name, method_name)
|
|
19
|
+
imports: list[str] = field(default_factory=list)
|
|
20
|
+
calls: list[tuple[str, str]] = field(default_factory=list) # (caller, callee)
|
|
21
|
+
errors: list[str] = field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PythonParser:
|
|
25
|
+
"""
|
|
26
|
+
Parses a Python file using the built-in ast module.
|
|
27
|
+
Extracts structural information for graph construction.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def parse_file(self, filepath: str) -> ParsedFile:
|
|
31
|
+
result = ParsedFile(filepath=filepath)
|
|
32
|
+
source = self._read_file(filepath, result)
|
|
33
|
+
|
|
34
|
+
if source is None:
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
tree = self._parse_source(source, filepath, result)
|
|
38
|
+
|
|
39
|
+
if tree is None:
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
self._extract_imports(tree, result)
|
|
43
|
+
self._extract_functions_and_calls(tree, result)
|
|
44
|
+
self._extract_classes(tree, result)
|
|
45
|
+
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
# -------------------------
|
|
49
|
+
# Private helpers
|
|
50
|
+
# -------------------------
|
|
51
|
+
|
|
52
|
+
def _read_file(self, filepath: str, result: ParsedFile) -> Optional[str]:
|
|
53
|
+
try:
|
|
54
|
+
return Path(filepath).read_text(encoding="utf-8")
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
result.errors.append(f"File not found: {filepath}")
|
|
57
|
+
return None
|
|
58
|
+
except Exception as e:
|
|
59
|
+
result.errors.append(f"Could not read file: {e}")
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def _parse_source(self, source: str, filepath: str, result: ParsedFile) -> Optional[ast.AST]:
|
|
63
|
+
try:
|
|
64
|
+
return ast.parse(source, filename=filepath)
|
|
65
|
+
except SyntaxError as e:
|
|
66
|
+
result.errors.append(f"Syntax error: {e}")
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def _extract_imports(self, tree: ast.AST, result: ParsedFile):
|
|
70
|
+
for node in ast.walk(tree):
|
|
71
|
+
# e.g. import os, import sys
|
|
72
|
+
if isinstance(node, ast.Import):
|
|
73
|
+
for alias in node.names:
|
|
74
|
+
result.imports.append(alias.name)
|
|
75
|
+
|
|
76
|
+
# e.g. from pathlib import Path
|
|
77
|
+
elif isinstance(node, ast.ImportFrom):
|
|
78
|
+
module = node.module or ""
|
|
79
|
+
for alias in node.names:
|
|
80
|
+
result.imports.append(f"{module}.{alias.name}")
|
|
81
|
+
|
|
82
|
+
def _extract_functions_and_calls(self, tree: ast.AST, result: ParsedFile):
|
|
83
|
+
# Collect method names per class first
|
|
84
|
+
class_method_names: set[str] = set()
|
|
85
|
+
for node in ast.walk(tree):
|
|
86
|
+
if isinstance(node, ast.ClassDef):
|
|
87
|
+
for item in node.body:
|
|
88
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
89
|
+
class_method_names.add(item.name)
|
|
90
|
+
|
|
91
|
+
# Walk top-level for standalone functions
|
|
92
|
+
for node in ast.iter_child_nodes(tree):
|
|
93
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
94
|
+
result.functions.append(node.name)
|
|
95
|
+
self._extract_calls_in_func(node, node.name, result)
|
|
96
|
+
|
|
97
|
+
elif isinstance(node, ast.ClassDef):
|
|
98
|
+
for item in node.body:
|
|
99
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
100
|
+
result.methods.append((node.name, item.name))
|
|
101
|
+
self._extract_calls_in_func(item, item.name, result)
|
|
102
|
+
|
|
103
|
+
def _extract_calls_in_func(self, func_node: ast.AST, func_name: str, result: ParsedFile):
|
|
104
|
+
for child in ast.walk(func_node):
|
|
105
|
+
if isinstance(child, ast.Call):
|
|
106
|
+
callee = self._resolve_call_name(child)
|
|
107
|
+
if callee:
|
|
108
|
+
result.calls.append((func_name, callee))
|
|
109
|
+
|
|
110
|
+
def _extract_classes(self, tree: ast.AST, result: ParsedFile):
|
|
111
|
+
for node in ast.walk(tree):
|
|
112
|
+
if isinstance(node, ast.ClassDef):
|
|
113
|
+
result.classes.append(node.name)
|
|
114
|
+
|
|
115
|
+
def _resolve_call_name(self, call_node: ast.Call) -> Optional[str]:
|
|
116
|
+
"""Extract the name of a function being called."""
|
|
117
|
+
func = call_node.func
|
|
118
|
+
|
|
119
|
+
if isinstance(func, ast.Name):
|
|
120
|
+
return func.id # e.g. print(...)
|
|
121
|
+
|
|
122
|
+
if isinstance(func, ast.Attribute):
|
|
123
|
+
return func.attr # e.g. self.login() → login
|
|
124
|
+
|
|
125
|
+
return None
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codegraph-cli-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tool to analyze codebases and visualize knowledge graphs using AST
|
|
5
|
+
Author: Aditya Jogdand
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: code-analysis,ast,graph,cli,developer-tools
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: typer>=0.9.0
|
|
15
|
+
Requires-Dist: networkx>=3.0
|
|
16
|
+
Requires-Dist: pyvis>=0.3.2
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
codegraph/cli.py
|
|
3
|
+
codegraph/graph/builder.py
|
|
4
|
+
codegraph/parsers/python_parser.py
|
|
5
|
+
codegraph_cli_ai.egg-info/PKG-INFO
|
|
6
|
+
codegraph_cli_ai.egg-info/SOURCES.txt
|
|
7
|
+
codegraph_cli_ai.egg-info/dependency_links.txt
|
|
8
|
+
codegraph_cli_ai.egg-info/entry_points.txt
|
|
9
|
+
codegraph_cli_ai.egg-info/requires.txt
|
|
10
|
+
codegraph_cli_ai.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
codegraph
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "codegraph-cli-ai"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "CLI tool to analyze codebases and visualize knowledge graphs using AST"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Aditya Jogdand" }
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
keywords = ["code-analysis", "ast", "graph", "cli", "developer-tools"]
|
|
14
|
+
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.9",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
"typer>=0.9.0",
|
|
24
|
+
"networkx>=3.0",
|
|
25
|
+
"pyvis>=0.3.2"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
codegraph = "codegraph.cli:app"
|