ctxgraph 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ctxgraph/__init__.py +0 -0
- ctxgraph/analyzers/__init__.py +0 -0
- ctxgraph/analyzers/python/__init__.py +0 -0
- ctxgraph/analyzers/python/importer.py +140 -0
- ctxgraph/analyzers/python/semantic.py +75 -0
- ctxgraph/analyzers/python/symbols.py +221 -0
- ctxgraph/capsule/__init__.py +0 -0
- ctxgraph/capsule/renderer.py +183 -0
- ctxgraph/cli/__init__.py +0 -0
- ctxgraph/cli/main.py +253 -0
- ctxgraph/clients/__init__.py +0 -0
- ctxgraph/clients/models.py +44 -0
- ctxgraph/config/__init__.py +0 -0
- ctxgraph/config/providers.py +150 -0
- ctxgraph/config/settings.py +232 -0
- ctxgraph/exclude/__init__.py +0 -0
- ctxgraph/exclude/patterns.py +75 -0
- ctxgraph/graph/__init__.py +0 -0
- ctxgraph/graph/builder.py +76 -0
- ctxgraph/graph/models.py +83 -0
- ctxgraph/graph/query.py +135 -0
- ctxgraph/graph/storage.py +224 -0
- ctxgraph/mcp/__init__.py +0 -0
- ctxgraph/mcp/server.py +216 -0
- ctxgraph/view/__init__.py +0 -0
- ctxgraph/view/visualizer.py +282 -0
- ctxgraph/wrapper/__init__.py +0 -0
- ctxgraph/wrapper/claude.py +139 -0
- ctxgraph-0.1.0.dist-info/METADATA +448 -0
- ctxgraph-0.1.0.dist-info/RECORD +33 -0
- ctxgraph-0.1.0.dist-info/WHEEL +5 -0
- ctxgraph-0.1.0.dist-info/entry_points.txt +3 -0
- ctxgraph-0.1.0.dist-info/top_level.txt +1 -0
ctxgraph/mcp/server.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Phase 2: MCP (Model Context Protocol) Server
|
|
3
|
+
|
|
4
|
+
This server implements the MCP protocol to allow Claude Desktop
|
|
5
|
+
and other MCP-compatible clients to query the knowledge graph
|
|
6
|
+
directly through tools instead of static context capsules.
|
|
7
|
+
|
|
8
|
+
Protocol: https://modelcontextprotocol.io
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
ctx serve # Start MCP server (stdio mode)
|
|
12
|
+
ctx serve --port 8080 # Start MCP server (SSE mode)
|
|
13
|
+
|
|
14
|
+
Claude Desktop config:
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"ctxgraph": {
|
|
18
|
+
"command": "ctx",
|
|
19
|
+
"args": ["serve"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Optional
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import mcp.types as types
|
|
34
|
+
from mcp.server import NotificationOptions, Server
|
|
35
|
+
from mcp.server.models import InitializationOptions
|
|
36
|
+
|
|
37
|
+
HAS_MCP = True
|
|
38
|
+
except ImportError:
|
|
39
|
+
HAS_MCP = False
|
|
40
|
+
|
|
41
|
+
from ctxgraph.capsule.renderer import render_capsule, render_project_overview
|
|
42
|
+
from ctxgraph.clients.models import ModelMode, get_mode_config
|
|
43
|
+
from ctxgraph.graph.builder import get_storage
|
|
44
|
+
from ctxgraph.graph.query import search_relevant_nodes
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_server(repo_path: Optional[str] = None) -> Optional[Any]:
|
|
48
|
+
if not HAS_MCP:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
52
|
+
storage = get_storage(path)
|
|
53
|
+
|
|
54
|
+
server = Server("ctxgraph")
|
|
55
|
+
|
|
56
|
+
@server.list_tools()
|
|
57
|
+
async def list_tools() -> list[types.Tool]:
|
|
58
|
+
return [
|
|
59
|
+
types.Tool(
|
|
60
|
+
name="search_graph",
|
|
61
|
+
description="Search the codebase knowledge graph for relevant files, classes, and functions",
|
|
62
|
+
inputSchema={
|
|
63
|
+
"type": "object",
|
|
64
|
+
"properties": {
|
|
65
|
+
"query": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": "Search query describing what you're looking for",
|
|
68
|
+
},
|
|
69
|
+
"max_results": {
|
|
70
|
+
"type": "integer",
|
|
71
|
+
"description": "Maximum number of results",
|
|
72
|
+
"default": 15,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
"required": ["query"],
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
types.Tool(
|
|
79
|
+
name="get_context_capsule",
|
|
80
|
+
description="Generate a token-efficient context capsule for a specific task",
|
|
81
|
+
inputSchema={
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"query": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"description": "The task description for context generation",
|
|
87
|
+
},
|
|
88
|
+
"mode": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"enum": ["fast", "balanced", "deep"],
|
|
91
|
+
"description": "Detail level: fast (minimal), balanced, deep (comprehensive)",
|
|
92
|
+
"default": "balanced",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
"required": ["query"],
|
|
96
|
+
},
|
|
97
|
+
),
|
|
98
|
+
types.Tool(
|
|
99
|
+
name="get_file_dependencies",
|
|
100
|
+
description="Get the dependency graph for a specific file",
|
|
101
|
+
inputSchema={
|
|
102
|
+
"type": "object",
|
|
103
|
+
"properties": {
|
|
104
|
+
"file_path": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"description": "Path to the file (relative to repo root)",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
"required": ["file_path"],
|
|
110
|
+
},
|
|
111
|
+
),
|
|
112
|
+
types.Tool(
|
|
113
|
+
name="get_project_overview",
|
|
114
|
+
description="Get a high-level overview of the entire codebase structure",
|
|
115
|
+
inputSchema={
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {},
|
|
118
|
+
},
|
|
119
|
+
),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
@server.call_tool()
|
|
123
|
+
async def call_tool(
|
|
124
|
+
name: str, arguments: dict
|
|
125
|
+
) -> list[types.TextContent]:
|
|
126
|
+
nonlocal storage
|
|
127
|
+
if storage is None:
|
|
128
|
+
storage = get_storage(path)
|
|
129
|
+
|
|
130
|
+
if storage is None:
|
|
131
|
+
return [types.TextContent(
|
|
132
|
+
type="text",
|
|
133
|
+
text="No graph found. Run `ctx build` first.",
|
|
134
|
+
)]
|
|
135
|
+
|
|
136
|
+
if name == "search_graph":
|
|
137
|
+
query = arguments.get("query", "")
|
|
138
|
+
max_results = arguments.get("max_results", 15)
|
|
139
|
+
results = search_relevant_nodes(storage, query, max_nodes=max_results)
|
|
140
|
+
lines = [f"Search results for: {query}", ""]
|
|
141
|
+
for node, score in results:
|
|
142
|
+
lines.append(f" [{node.type}] {node.name} ({node.path}) - score: {score}")
|
|
143
|
+
if node.summary:
|
|
144
|
+
lines.append(f" {node.summary}")
|
|
145
|
+
return [types.TextContent(type="text", text="\n".join(lines))]
|
|
146
|
+
|
|
147
|
+
elif name == "get_context_capsule":
|
|
148
|
+
query = arguments.get("query", "")
|
|
149
|
+
mode_str = arguments.get("mode", "balanced")
|
|
150
|
+
mode = ModelMode.from_str(mode_str)
|
|
151
|
+
cfg = get_mode_config(mode)
|
|
152
|
+
capsule = render_capsule(storage, query, max_nodes=cfg["max_nodes"])
|
|
153
|
+
return [types.TextContent(type="text", text=capsule)]
|
|
154
|
+
|
|
155
|
+
elif name == "get_file_dependencies":
|
|
156
|
+
file_path = arguments.get("file_path", "")
|
|
157
|
+
node_id = f"file:{file_path}"
|
|
158
|
+
node = storage.get_node(node_id)
|
|
159
|
+
if not node:
|
|
160
|
+
return [types.TextContent(
|
|
161
|
+
type="text",
|
|
162
|
+
text=f"File not found in graph: {file_path}",
|
|
163
|
+
)]
|
|
164
|
+
edges = storage.get_edges_for_nodes({node_id})
|
|
165
|
+
lines = [f"Dependencies for: {file_path}", ""]
|
|
166
|
+
for e in edges:
|
|
167
|
+
target = storage.get_node(e.target_id)
|
|
168
|
+
source = storage.get_node(e.source_id)
|
|
169
|
+
if target and source:
|
|
170
|
+
lines.append(f" {source.name} --{e.relation}--> {target.name}")
|
|
171
|
+
return [types.TextContent(type="text", text="\n".join(lines))]
|
|
172
|
+
|
|
173
|
+
elif name == "get_project_overview":
|
|
174
|
+
overview = render_project_overview(storage)
|
|
175
|
+
return [types.TextContent(type="text", text=overview)]
|
|
176
|
+
|
|
177
|
+
else:
|
|
178
|
+
return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
179
|
+
|
|
180
|
+
return server
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def run_server(repo_path: Optional[str] = None, port: Optional[int] = None):
|
|
184
|
+
if not HAS_MCP:
|
|
185
|
+
print(
|
|
186
|
+
"MCP support requires additional dependencies.\n"
|
|
187
|
+
"Install with: pip install ctxgraph[mcp]",
|
|
188
|
+
file=sys.stderr,
|
|
189
|
+
)
|
|
190
|
+
sys.exit(1)
|
|
191
|
+
|
|
192
|
+
server = create_server(repo_path)
|
|
193
|
+
if server is None:
|
|
194
|
+
print("Failed to create MCP server", file=sys.stderr)
|
|
195
|
+
sys.exit(1)
|
|
196
|
+
|
|
197
|
+
from mcp.server.stdio import stdio_server
|
|
198
|
+
|
|
199
|
+
import anyio
|
|
200
|
+
|
|
201
|
+
async def _run():
|
|
202
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
203
|
+
await server.run(
|
|
204
|
+
read_stream,
|
|
205
|
+
write_stream,
|
|
206
|
+
InitializationOptions(
|
|
207
|
+
server_name="ctxgraph",
|
|
208
|
+
server_version="0.1.0",
|
|
209
|
+
capabilities=server.get_capabilities(
|
|
210
|
+
notification_options=NotificationOptions(),
|
|
211
|
+
experimental_capabilities={},
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
anyio.run(_run)
|
|
File without changes
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ctxgraph.graph.storage import Storage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def render_view(storage: Storage) -> str:
|
|
10
|
+
nodes = storage.get_all_nodes()
|
|
11
|
+
edges = storage.get_all_edges()
|
|
12
|
+
|
|
13
|
+
file_nodes = [n for n in nodes if n.type == "file"]
|
|
14
|
+
symbol_nodes = [n for n in nodes if n.type != "file"]
|
|
15
|
+
|
|
16
|
+
graph_data = {
|
|
17
|
+
"nodes": [],
|
|
18
|
+
"links": [],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
file_node_ids = {n.id for n in file_nodes}
|
|
22
|
+
|
|
23
|
+
for node in file_nodes:
|
|
24
|
+
graph_data["nodes"].append(
|
|
25
|
+
{
|
|
26
|
+
"id": node.id,
|
|
27
|
+
"label": _short_path(node.path or node.name),
|
|
28
|
+
"type": "file",
|
|
29
|
+
"summary": (node.summary or "")[:100],
|
|
30
|
+
"importance": node.importance,
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
for node in symbol_nodes:
|
|
35
|
+
graph_data["nodes"].append(
|
|
36
|
+
{
|
|
37
|
+
"id": node.id,
|
|
38
|
+
"label": node.name,
|
|
39
|
+
"type": node.type,
|
|
40
|
+
"summary": (node.summary or "")[:100],
|
|
41
|
+
"importance": node.importance,
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
seen_links = set()
|
|
46
|
+
for edge in edges:
|
|
47
|
+
if edge.source_id in file_node_ids or edge.target_id in file_node_ids:
|
|
48
|
+
key = (edge.source_id, edge.target_id)
|
|
49
|
+
if key not in seen_links:
|
|
50
|
+
seen_links.add(key)
|
|
51
|
+
graph_data["links"].append(
|
|
52
|
+
{
|
|
53
|
+
"source": edge.source_id,
|
|
54
|
+
"target": edge.target_id,
|
|
55
|
+
"relation": edge.relation,
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
json_data = json.dumps(graph_data, indent=2)
|
|
60
|
+
|
|
61
|
+
template = _get_html_template()
|
|
62
|
+
return template.replace("/* GRAPH_DATA */", json_data)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _short_path(path: str) -> str:
|
|
66
|
+
parts = path.split("/")
|
|
67
|
+
if len(parts) > 3:
|
|
68
|
+
return "/".join(parts[:2] + ["..."] + parts[-1:])
|
|
69
|
+
return path
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_html_template() -> str:
|
|
73
|
+
return """<!DOCTYPE html>
|
|
74
|
+
<html lang="en">
|
|
75
|
+
<head>
|
|
76
|
+
<meta charset="UTF-8">
|
|
77
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
78
|
+
<title>ctxgraph - Knowledge Graph</title>
|
|
79
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
80
|
+
<style>
|
|
81
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
82
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; overflow: hidden; }
|
|
83
|
+
#container { width: 100vw; height: 100vh; position: relative; }
|
|
84
|
+
svg { width: 100%; height: 100%; }
|
|
85
|
+
#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; }
|
|
86
|
+
#toolbar input { background: #0d1117; border: 1px solid #30363d; color: #c9d1d9; padding: 6px 12px; border-radius: 4px; width: 220px; font-size: 13px; }
|
|
87
|
+
#toolbar select { background: #0d1117; border: 1px solid #30363d; color: #c9d1d9; padding: 6px 8px; border-radius: 4px; font-size: 13px; }
|
|
88
|
+
#toolbar label { font-size: 13px; color: #8b949e; }
|
|
89
|
+
#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; }
|
|
90
|
+
.legend-item { display: flex; align-items: center; gap: 6px; }
|
|
91
|
+
.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
|
|
92
|
+
#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); }
|
|
93
|
+
#tooltip .tt-name { font-weight: 600; color: #58a6ff; margin-bottom: 4px; }
|
|
94
|
+
#tooltip .tt-type { color: #8b949e; font-size: 11px; margin-bottom: 4px; }
|
|
95
|
+
#tooltip .tt-summary { color: #c9d1d9; font-size: 12px; }
|
|
96
|
+
#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; }
|
|
97
|
+
.link { stroke-opacity: 0.4; }
|
|
98
|
+
.link.imports { stroke: #58a6ff; }
|
|
99
|
+
.link.calls { stroke: #3fb950; }
|
|
100
|
+
.link.defines { stroke: #d29922; }
|
|
101
|
+
.link.extends { stroke: #bc8cff; }
|
|
102
|
+
.node { cursor: pointer; transition: opacity 0.2s; }
|
|
103
|
+
.node:hover { opacity: 0.8; }
|
|
104
|
+
.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; }
|
|
105
|
+
.node.highlighted .node-label { fill: #c9d1d9; font-weight: 600; }
|
|
106
|
+
.link.highlighted { stroke-opacity: 0.8; }
|
|
107
|
+
</style>
|
|
108
|
+
</head>
|
|
109
|
+
<body>
|
|
110
|
+
<div id="container">
|
|
111
|
+
<div id="toolbar">
|
|
112
|
+
<label>Search:</label>
|
|
113
|
+
<input type="text" id="search" placeholder="Search nodes..." oninput="filterGraph(this.value)">
|
|
114
|
+
<label>Filter:</label>
|
|
115
|
+
<select id="filterType" onchange="filterByType(this.value)">
|
|
116
|
+
<option value="all">All</option>
|
|
117
|
+
<option value="file">Files</option>
|
|
118
|
+
<option value="class">Classes</option>
|
|
119
|
+
<option value="function">Functions</option>
|
|
120
|
+
</select>
|
|
121
|
+
</div>
|
|
122
|
+
<div id="legend">
|
|
123
|
+
<div class="legend-item"><div class="legend-dot" style="background:#58a6ff"></div> File</div>
|
|
124
|
+
<div class="legend-item"><div class="legend-dot" style="background:#d29922"></div> Class</div>
|
|
125
|
+
<div class="legend-item"><div class="legend-dot" style="background:#3fb950"></div> Function</div>
|
|
126
|
+
<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>
|
|
127
|
+
<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>
|
|
128
|
+
</div>
|
|
129
|
+
<div id="stats"></div>
|
|
130
|
+
<div id="tooltip">
|
|
131
|
+
<div class="tt-name"></div>
|
|
132
|
+
<div class="tt-type"></div>
|
|
133
|
+
<div class="tt-summary"></div>
|
|
134
|
+
</div>
|
|
135
|
+
<svg></svg>
|
|
136
|
+
</div>
|
|
137
|
+
<script>
|
|
138
|
+
const graphData = /* GRAPH_DATA */;
|
|
139
|
+
const width = window.innerWidth;
|
|
140
|
+
const height = window.innerHeight;
|
|
141
|
+
|
|
142
|
+
const svg = d3.select("svg");
|
|
143
|
+
const g = svg.append("g");
|
|
144
|
+
|
|
145
|
+
const zoom = d3.zoom()
|
|
146
|
+
.scaleExtent([0.1, 4])
|
|
147
|
+
.on("zoom", (event) => g.attr("transform", event.transform));
|
|
148
|
+
|
|
149
|
+
svg.call(zoom);
|
|
150
|
+
|
|
151
|
+
const colorMap = { file: "#58a6ff", class: "#d29922", function: "#3fb950", module: "#bc8cff" };
|
|
152
|
+
|
|
153
|
+
const simulation = d3.forceSimulation(graphData.nodes)
|
|
154
|
+
.force("link", d3.forceLink(graphData.links).id(d => d.id).distance(d => {
|
|
155
|
+
return d.relation === "imports" ? 100 : 80;
|
|
156
|
+
}))
|
|
157
|
+
.force("charge", d3.forceManyBody().strength(-200))
|
|
158
|
+
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
159
|
+
.force("collision", d3.forceCollide(20));
|
|
160
|
+
|
|
161
|
+
const link = g.append("g")
|
|
162
|
+
.selectAll("line")
|
|
163
|
+
.data(graphData.links)
|
|
164
|
+
.join("line")
|
|
165
|
+
.attr("class", d => `link ${d.relation}`)
|
|
166
|
+
.attr("stroke-width", 1.5);
|
|
167
|
+
|
|
168
|
+
const node = g.append("g")
|
|
169
|
+
.selectAll("g")
|
|
170
|
+
.data(graphData.nodes)
|
|
171
|
+
.join("g")
|
|
172
|
+
.attr("class", "node")
|
|
173
|
+
.call(d3.drag()
|
|
174
|
+
.on("start", (event, d) => {
|
|
175
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
176
|
+
d.fx = d.x;
|
|
177
|
+
d.fy = d.y;
|
|
178
|
+
})
|
|
179
|
+
.on("drag", (event, d) => {
|
|
180
|
+
d.fx = event.x;
|
|
181
|
+
d.fy = event.y;
|
|
182
|
+
})
|
|
183
|
+
.on("end", (event, d) => {
|
|
184
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
185
|
+
d.fx = null;
|
|
186
|
+
d.fy = null;
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
node.append("circle")
|
|
191
|
+
.attr("r", d => 5 + (d.importance || 0.5) * 8)
|
|
192
|
+
.attr("fill", d => colorMap[d.type] || "#8b949e")
|
|
193
|
+
.attr("stroke", "#161b22")
|
|
194
|
+
.attr("stroke-width", 1.5);
|
|
195
|
+
|
|
196
|
+
node.append("text")
|
|
197
|
+
.attr("class", "node-label")
|
|
198
|
+
.text(d => d.label)
|
|
199
|
+
.attr("dx", d => 10 + (d.importance || 0.5) * 5)
|
|
200
|
+
.attr("dy", 4);
|
|
201
|
+
|
|
202
|
+
node.on("mouseover", function(event, d) {
|
|
203
|
+
const tt = d3.select("#tooltip");
|
|
204
|
+
tt.style("display", "block");
|
|
205
|
+
tt.select(".tt-name").text(d.label);
|
|
206
|
+
tt.select(".tt-type").text(`Type: ${d.type}`);
|
|
207
|
+
tt.select(".tt-summary").text(d.summary || "");
|
|
208
|
+
|
|
209
|
+
const connected = new Set();
|
|
210
|
+
graphData.links.forEach(l => {
|
|
211
|
+
const sid = typeof l.source === 'object' ? l.source.id : l.source;
|
|
212
|
+
const tid = typeof l.target === 'object' ? l.target.id : l.target;
|
|
213
|
+
if (sid === d.id) connected.add(tid);
|
|
214
|
+
if (tid === d.id) connected.add(sid);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
d3.selectAll(".node").each(function(n) {
|
|
218
|
+
if (n.id === d.id || connected.has(n.id)) {
|
|
219
|
+
d3.select(this).classed("highlighted", true);
|
|
220
|
+
d3.select(this).select("circle").attr("opacity", 1);
|
|
221
|
+
d3.select(this).select("text").attr("opacity", 1);
|
|
222
|
+
} else {
|
|
223
|
+
d3.select(this).select("circle").attr("opacity", 0.15);
|
|
224
|
+
d3.select(this).select("text").attr("opacity", 0.15);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
d3.selectAll(".link").each(function(l) {
|
|
229
|
+
const sid = typeof l.source === 'object' ? l.source.id : l.source;
|
|
230
|
+
const tid = typeof l.target === 'object' ? l.target.id : l.target;
|
|
231
|
+
if (sid === d.id || tid === d.id) {
|
|
232
|
+
d3.select(this).classed("highlighted", true);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
})
|
|
236
|
+
.on("mousemove", function(event) {
|
|
237
|
+
d3.select("#tooltip")
|
|
238
|
+
.style("left", (event.pageX + 12) + "px")
|
|
239
|
+
.style("top", (event.pageY - 10) + "px");
|
|
240
|
+
})
|
|
241
|
+
.on("mouseout", function() {
|
|
242
|
+
d3.select("#tooltip").style("display", "none");
|
|
243
|
+
d3.selectAll(".node").each(function(n) {
|
|
244
|
+
d3.select(this).classed("highlighted", false);
|
|
245
|
+
d3.select(this).select("circle").attr("opacity", 1);
|
|
246
|
+
d3.select(this).select("text").attr("opacity", 1);
|
|
247
|
+
});
|
|
248
|
+
d3.selectAll(".link").classed("highlighted", false);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
simulation.on("tick", () => {
|
|
252
|
+
link
|
|
253
|
+
.attr("x1", d => d.source.x)
|
|
254
|
+
.attr("y1", d => d.source.y)
|
|
255
|
+
.attr("x2", d => d.target.x)
|
|
256
|
+
.attr("y2", d => d.target.y);
|
|
257
|
+
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
d3.select("#stats").text(`Nodes: ${graphData.nodes.length} | Edges: ${graphData.links.length}`);
|
|
261
|
+
|
|
262
|
+
window.filterGraph = function(query) {
|
|
263
|
+
const q = query.toLowerCase();
|
|
264
|
+
d3.selectAll(".node").each(function(d) {
|
|
265
|
+
const match = !q || d.label.toLowerCase().includes(q) || (d.summary && d.summary.toLowerCase().includes(q));
|
|
266
|
+
d3.select(this).style("display", match ? null : "none");
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
window.filterByType = function(type) {
|
|
271
|
+
d3.selectAll(".node").each(function(d) {
|
|
272
|
+
const match = type === "all" || d.type === type;
|
|
273
|
+
d3.select(this).style("display", match ? null : "none");
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
window.addEventListener("resize", () => {
|
|
278
|
+
svg.attr("width", window.innerWidth).attr("height", window.innerHeight);
|
|
279
|
+
});
|
|
280
|
+
</script>
|
|
281
|
+
</body>
|
|
282
|
+
</html>"""
|
|
File without changes
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ctxgraph.clients.models import ModelMode, get_mode_config
|
|
8
|
+
from ctxgraph.graph.builder import get_storage
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from ctxgraph.capsule.renderer import render_capsule, render_project_overview
|
|
12
|
+
|
|
13
|
+
HAS_CAPSULE = True
|
|
14
|
+
except ImportError:
|
|
15
|
+
HAS_CAPSULE = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
args = sys.argv[1:]
|
|
20
|
+
|
|
21
|
+
if not args:
|
|
22
|
+
print("Usage: ccg [--mode fast|balanced|deep] <query>")
|
|
23
|
+
print(" ccg --overview")
|
|
24
|
+
print(" ccg --chat <query>")
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
mode = ModelMode.BALANCED
|
|
28
|
+
is_overview = False
|
|
29
|
+
is_chat = False
|
|
30
|
+
query_parts = []
|
|
31
|
+
|
|
32
|
+
for arg in args:
|
|
33
|
+
if arg == "--overview" or arg == "-o":
|
|
34
|
+
is_overview = True
|
|
35
|
+
elif arg == "--chat" or arg == "-c":
|
|
36
|
+
is_chat = True
|
|
37
|
+
elif arg.startswith("--mode="):
|
|
38
|
+
mode = ModelMode.from_str(arg.split("=", 1)[1])
|
|
39
|
+
elif arg == "--mode" or arg == "-m":
|
|
40
|
+
continue
|
|
41
|
+
else:
|
|
42
|
+
query_parts.append(arg)
|
|
43
|
+
|
|
44
|
+
if not is_overview and not query_parts:
|
|
45
|
+
print("No query provided.")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
query = " ".join(query_parts)
|
|
49
|
+
mode_cfg = get_mode_config(mode)
|
|
50
|
+
|
|
51
|
+
repo_path = Path.cwd()
|
|
52
|
+
capsule_text = ""
|
|
53
|
+
storage = None
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
storage = get_storage(repo_path)
|
|
57
|
+
except Exception:
|
|
58
|
+
storage = None
|
|
59
|
+
|
|
60
|
+
if storage:
|
|
61
|
+
if is_overview:
|
|
62
|
+
capsule_text = render_project_overview(storage)
|
|
63
|
+
else:
|
|
64
|
+
capsule_text = render_capsule(
|
|
65
|
+
storage,
|
|
66
|
+
query,
|
|
67
|
+
max_nodes=mode_cfg["max_nodes"],
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
capsule_text = (
|
|
71
|
+
"[INFO]No ctxgraph data found. "
|
|
72
|
+
"Run `ctx build` for enriched context."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
mode_hint = {
|
|
76
|
+
ModelMode.FAST: "Use fast, concise reasoning. Prioritize direct answers.",
|
|
77
|
+
ModelMode.BALANCED: "",
|
|
78
|
+
ModelMode.DEEP: "Take your time. Analyze deeply before responding.",
|
|
79
|
+
}.get(mode, "")
|
|
80
|
+
|
|
81
|
+
augmented_prompt = (
|
|
82
|
+
f"[CONTEXT]\n{capsule_text}\n[/CONTEXT]\n\n"
|
|
83
|
+
f"TASK: {query}\n\n"
|
|
84
|
+
f"{mode_hint}\n" if mode_hint else ""
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
claude_cmd = _find_claude()
|
|
88
|
+
if not claude_cmd:
|
|
89
|
+
print(
|
|
90
|
+
"[red]Claude CLI not found. Install it or ensure it's in PATH.[/red]",
|
|
91
|
+
file=sys.stderr,
|
|
92
|
+
)
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
if is_chat:
|
|
96
|
+
context_file = repo_path / ".ctxgraph" / "context.md"
|
|
97
|
+
context_file.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
context_file.write_text(capsule_text, encoding="utf-8")
|
|
99
|
+
|
|
100
|
+
system_msg = (
|
|
101
|
+
f"Project context is loaded from {context_file}.\n"
|
|
102
|
+
f"Read it first, then help the user.\n"
|
|
103
|
+
f"{mode_hint}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
proc = subprocess.Popen(
|
|
107
|
+
[claude_cmd, "-p", system_msg],
|
|
108
|
+
stdin=sys.stdin,
|
|
109
|
+
stdout=sys.stdout,
|
|
110
|
+
stderr=sys.stderr,
|
|
111
|
+
)
|
|
112
|
+
proc.wait()
|
|
113
|
+
else:
|
|
114
|
+
proc = subprocess.run(
|
|
115
|
+
[claude_cmd, "-p", augmented_prompt],
|
|
116
|
+
capture_output=False,
|
|
117
|
+
)
|
|
118
|
+
sys.exit(proc.returncode)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _find_claude() -> str | None:
|
|
122
|
+
claude_names = ["claude", "claude.exe"]
|
|
123
|
+
for name in claude_names:
|
|
124
|
+
try:
|
|
125
|
+
result = subprocess.run(
|
|
126
|
+
["where", name] if sys.platform == "win32" else ["which", name],
|
|
127
|
+
capture_output=True,
|
|
128
|
+
text=True,
|
|
129
|
+
timeout=5,
|
|
130
|
+
)
|
|
131
|
+
if result.returncode == 0:
|
|
132
|
+
return name
|
|
133
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
134
|
+
pass
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
main()
|