codegraphy-lite 1.0.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.
- codegraphy_lite/__init__.py +7 -0
- codegraphy_lite/cli.py +113 -0
- codegraphy_lite/mcp.py +338 -0
- codegraphy_lite/models.py +204 -0
- codegraphy_lite/scanner.py +330 -0
- codegraphy_lite/server.py +414 -0
- codegraphy_lite/storage.py +201 -0
- codegraphy_lite/web/assets/codegraphy-lite-ui.css +323 -0
- codegraphy_lite/web/assets/codegraphy-lite-ui.js +305 -0
- codegraphy_lite/web/assets/graph-D7gaURIl.js +7 -0
- codegraphy_lite/web/assets/icons-CvV5lYnA.js +1 -0
- codegraphy_lite/web/assets/index-BH1KbUBa.css +1 -0
- codegraphy_lite/web/assets/index-BPD6wClD.js +50 -0
- codegraphy_lite/web/assets/markdown-Bj3RCaMp.js +14 -0
- codegraphy_lite/web/favicon.ico +0 -0
- codegraphy_lite/web/favicon.png +0 -0
- codegraphy_lite/web/favicon.svg +1 -0
- codegraphy_lite/web/index.html +22 -0
- codegraphy_lite-1.0.0.dist-info/METADATA +227 -0
- codegraphy_lite-1.0.0.dist-info/RECORD +24 -0
- codegraphy_lite-1.0.0.dist-info/WHEEL +5 -0
- codegraphy_lite-1.0.0.dist-info/entry_points.txt +3 -0
- codegraphy_lite-1.0.0.dist-info/licenses/LICENSE +21 -0
- codegraphy_lite-1.0.0.dist-info/top_level.txt +1 -0
codegraphy_lite/cli.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""CLI entrypoint for CodeGraphy Lite.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
codegraphy-lite [path] [--port 8000] [--host 127.0.0.1] [--no-browser] [--scan]
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import webbrowser
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import uvicorn
|
|
17
|
+
|
|
18
|
+
from codegraphy_lite import __version__
|
|
19
|
+
from codegraphy_lite.scanner import build_graph_and_workspace, scan_project
|
|
20
|
+
from codegraphy_lite.server import app
|
|
21
|
+
from codegraphy_lite.storage import get_storage_dir, load_graph, save_graph, save_workspace, set_active_root
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def open_browser_later(url: str, delay_sec: float = 1.0):
|
|
25
|
+
def _open():
|
|
26
|
+
time.sleep(delay_sec)
|
|
27
|
+
try:
|
|
28
|
+
webbrowser.open(url)
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
t = threading.Thread(target=_open, daemon=True)
|
|
33
|
+
t.start()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main():
|
|
37
|
+
parser = argparse.ArgumentParser(
|
|
38
|
+
prog="codegraphy-lite",
|
|
39
|
+
description="CodeGraphy Lite — Standalone Open-Source Visual Architecture Graph & MCP Server",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"path",
|
|
43
|
+
nargs="?",
|
|
44
|
+
default=".",
|
|
45
|
+
help="Target project directory to visualize (default: current directory)",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--port",
|
|
49
|
+
type=int,
|
|
50
|
+
default=8000,
|
|
51
|
+
help="HTTP server port (default: 8000)",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--host",
|
|
55
|
+
default="127.0.0.1",
|
|
56
|
+
help="HTTP server bind host (default: 127.0.0.1)",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--no-browser",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="Do not automatically launch the browser",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--scan",
|
|
65
|
+
action="store_true",
|
|
66
|
+
help="Force automatic project directory scan on startup (up to 35 files)",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"-v", "--version",
|
|
70
|
+
action="version",
|
|
71
|
+
version=f"codegraphy-lite {__version__}",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
args = parser.parse_args()
|
|
75
|
+
|
|
76
|
+
project_root = Path(args.path).resolve()
|
|
77
|
+
if not project_root.exists() or not project_root.is_dir():
|
|
78
|
+
print(f"Error: Directory '{project_root}' does not exist.", file=sys.stderr)
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
|
|
81
|
+
set_active_root(project_root)
|
|
82
|
+
|
|
83
|
+
# Check if graph exists; if not or if --scan requested, run Lite scanner
|
|
84
|
+
storage_dir = get_storage_dir(project_root)
|
|
85
|
+
existing_graph = load_graph(project_root)
|
|
86
|
+
|
|
87
|
+
if args.scan or not existing_graph.nodes:
|
|
88
|
+
print(f"[*] Scanning project architecture in '{project_root}' (Lite limit: max 35 files)...")
|
|
89
|
+
try:
|
|
90
|
+
preview = scan_project(project_root)
|
|
91
|
+
graph, ws = build_graph_and_workspace(preview)
|
|
92
|
+
save_graph(graph, project_root)
|
|
93
|
+
save_workspace(ws, project_root)
|
|
94
|
+
print(f"[+] Discovered {preview.total_files} files, {len(graph.nodes)} topics, {len(graph.edges)} dependency edges.")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f"[!] Warning: Initial project scan skipped ({e})", file=sys.stderr)
|
|
97
|
+
|
|
98
|
+
url = f"http://{args.host}:{args.port}"
|
|
99
|
+
print(f"\n=======================================================")
|
|
100
|
+
print(f" CodeGraphy Lite v{__version__} (Open Source Edition)")
|
|
101
|
+
print(f" Visual Architecture Canvas & MCP Server")
|
|
102
|
+
print(f" Project Root: {project_root}")
|
|
103
|
+
print(f" Web UI: {url}")
|
|
104
|
+
print(f"=======================================================\n")
|
|
105
|
+
|
|
106
|
+
if not args.no_browser:
|
|
107
|
+
open_browser_later(url, delay_sec=0.8)
|
|
108
|
+
|
|
109
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
main()
|
codegraphy_lite/mcp.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Model Context Protocol (MCP) STDIO Server for CodeGraphy Lite.
|
|
2
|
+
|
|
3
|
+
Exposes 3 basic workspace/graph architectural tools to Claude Code, Cursor, Windsurf,
|
|
4
|
+
and agentic coding assistants via standard JSON-RPC 2.0 over STDIO.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from codegraphy_lite import __version__
|
|
17
|
+
from codegraphy_lite.models import GraphState, WorkspaceModel
|
|
18
|
+
from codegraphy_lite.scanner import extract_ast_signatures
|
|
19
|
+
from codegraphy_lite.storage import get_active_root, load_graph, load_workspace, set_active_root
|
|
20
|
+
|
|
21
|
+
PROTOCOL_VERSION = "2024-11-05"
|
|
22
|
+
SERVER_NAME = "codegraphy-lite-mcp"
|
|
23
|
+
MAX_PAYLOAD_CHARS = 32000
|
|
24
|
+
|
|
25
|
+
SECRET_PATTERNS = [
|
|
26
|
+
re.compile(r"\b(?:sk|whsec)_[a-zA-Z0-9_\-]{16,}\b"),
|
|
27
|
+
re.compile(r"\bsk-(?:live|test|ant|proj)?[a-zA-Z0-9_\-]{16,}\b"),
|
|
28
|
+
re.compile(r"\bAIza[0-9A-Za-z\-_]{35}\b"),
|
|
29
|
+
re.compile(r"\bghp_[0-9a-zA-Z]{36}\b"),
|
|
30
|
+
re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def scrub_secrets(val: Any) -> Any:
|
|
35
|
+
"""Redacts potential API tokens and keys."""
|
|
36
|
+
if isinstance(val, str):
|
|
37
|
+
text = val
|
|
38
|
+
for pat in SECRET_PATTERNS:
|
|
39
|
+
text = pat.sub("[REDACTED_SECRET]", text)
|
|
40
|
+
return text
|
|
41
|
+
if isinstance(val, dict):
|
|
42
|
+
return {k: scrub_secrets(v) for k, v in val.items()}
|
|
43
|
+
if isinstance(val, list):
|
|
44
|
+
return [scrub_secrets(item) for item in val]
|
|
45
|
+
return val
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
TOOLS_MANIFEST = [
|
|
49
|
+
{
|
|
50
|
+
"name": "list_workspace_topics",
|
|
51
|
+
"description": "Lists all architectural topics/nodes in the workspace with their status, lanes, attached files, and descriptions.",
|
|
52
|
+
"inputSchema": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"status": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"enum": ["all", "todo", "in_progress", "done"],
|
|
58
|
+
"description": "Filter topics by status (default: all)",
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "get_topic_context",
|
|
65
|
+
"description": "Retrieves comprehensive architectural context for a topic, including 1-hop connected dependencies and AST-extracted signatures from attached files.",
|
|
66
|
+
"inputSchema": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"topic": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"description": "Topic ID or label to inspect",
|
|
72
|
+
},
|
|
73
|
+
"include_ast": {
|
|
74
|
+
"type": "boolean",
|
|
75
|
+
"description": "Whether to include AST-extracted class/function signatures (default: true)",
|
|
76
|
+
"default": True,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
"required": ["topic"],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "get_architecture_summary",
|
|
84
|
+
"description": "Returns a high-level architectural overview: total topics, dependency edges, lanes, status distribution, and key architectural modules.",
|
|
85
|
+
"inputSchema": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CodeGraphyLiteMCP:
|
|
94
|
+
def __init__(self, root_dir: Optional[str | Path] = None) -> None:
|
|
95
|
+
if root_dir:
|
|
96
|
+
set_active_root(root_dir)
|
|
97
|
+
elif "CODEGRAPHY_ROOT" in os.environ:
|
|
98
|
+
set_active_root(os.environ["CODEGRAPHY_ROOT"])
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def root(self) -> Path:
|
|
102
|
+
return get_active_root()
|
|
103
|
+
|
|
104
|
+
def handle_list_workspace_topics(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
105
|
+
graph = load_graph(self.root)
|
|
106
|
+
status_filter = args.get("status", "all")
|
|
107
|
+
|
|
108
|
+
topics = []
|
|
109
|
+
for n in graph.nodes:
|
|
110
|
+
d = n.data
|
|
111
|
+
if status_filter != "all" and d.status != status_filter:
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
incoming = sum(1 for e in graph.edges if e.target == n.id)
|
|
115
|
+
outgoing = sum(1 for e in graph.edges if e.source == n.id)
|
|
116
|
+
files = [f.filepath for f in d.attached_files]
|
|
117
|
+
|
|
118
|
+
topics.append({
|
|
119
|
+
"id": n.id,
|
|
120
|
+
"label": d.label,
|
|
121
|
+
"status": d.status,
|
|
122
|
+
"lane_id": d.lane_id,
|
|
123
|
+
"description": d.description,
|
|
124
|
+
"files_count": len(files),
|
|
125
|
+
"files": files,
|
|
126
|
+
"incoming_dependencies": incoming,
|
|
127
|
+
"outgoing_dependencies": outgoing,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
"root_path": str(self.root),
|
|
132
|
+
"total_topics": len(topics),
|
|
133
|
+
"topics": topics,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def handle_get_topic_context(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
137
|
+
query = args.get("topic", "").strip().lower()
|
|
138
|
+
if not query:
|
|
139
|
+
raise ValueError("Parameter 'topic' is required.")
|
|
140
|
+
include_ast = args.get("include_ast", True)
|
|
141
|
+
|
|
142
|
+
graph = load_graph(self.root)
|
|
143
|
+
target = None
|
|
144
|
+
for n in graph.nodes:
|
|
145
|
+
if n.id.lower() == query or n.data.label.lower() == query:
|
|
146
|
+
target = n
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
if not target:
|
|
150
|
+
raise ValueError(f"Topic '{query}' not found in workspace.")
|
|
151
|
+
|
|
152
|
+
# Dependencies
|
|
153
|
+
incoming_ids = [e.source for e in graph.edges if e.target == target.id]
|
|
154
|
+
outgoing_ids = [e.target for e in graph.edges if e.source == target.id]
|
|
155
|
+
|
|
156
|
+
def resolve_label(nid: str) -> str:
|
|
157
|
+
m = next((x.data.label for x in graph.nodes if x.id == nid), nid)
|
|
158
|
+
return m
|
|
159
|
+
|
|
160
|
+
# Files & AST signatures
|
|
161
|
+
files_data = []
|
|
162
|
+
for f in target.data.attached_files:
|
|
163
|
+
file_info: Dict[str, Any] = {"filepath": f.filepath}
|
|
164
|
+
full_p = self.root / f.filepath
|
|
165
|
+
if full_p.exists() and full_p.is_file():
|
|
166
|
+
file_info["exists"] = True
|
|
167
|
+
if include_ast:
|
|
168
|
+
try:
|
|
169
|
+
content = full_p.read_text(encoding="utf-8", errors="ignore")
|
|
170
|
+
file_info["signatures"] = extract_ast_signatures(content)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
file_info["signatures"] = f"# Error extracting AST: {e}"
|
|
173
|
+
else:
|
|
174
|
+
file_info["exists"] = False
|
|
175
|
+
file_info["signatures"] = "# File not found on disk"
|
|
176
|
+
files_data.append(file_info)
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
"topic_id": target.id,
|
|
180
|
+
"label": target.data.label,
|
|
181
|
+
"status": target.data.status,
|
|
182
|
+
"description": target.data.description,
|
|
183
|
+
"connected_dependencies": {
|
|
184
|
+
"incoming": [resolve_label(x) for x in incoming_ids],
|
|
185
|
+
"outgoing": [resolve_label(x) for x in outgoing_ids],
|
|
186
|
+
},
|
|
187
|
+
"attached_files": files_data,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
def handle_get_architecture_summary(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
191
|
+
graph = load_graph(self.root)
|
|
192
|
+
ws = load_workspace(self.root)
|
|
193
|
+
|
|
194
|
+
status_counts = {"todo": 0, "in_progress": 0, "done": 0}
|
|
195
|
+
for n in graph.nodes:
|
|
196
|
+
st = n.data.status
|
|
197
|
+
if st in status_counts:
|
|
198
|
+
status_counts[st] += 1
|
|
199
|
+
|
|
200
|
+
degree_map: Dict[str, int] = {}
|
|
201
|
+
for e in graph.edges:
|
|
202
|
+
degree_map[e.source] = degree_map.get(e.source, 0) + 1
|
|
203
|
+
degree_map[e.target] = degree_map.get(e.target, 0) + 1
|
|
204
|
+
|
|
205
|
+
top_components = sorted(
|
|
206
|
+
[{"topic_id": k, "degree": v} for k, v in degree_map.items()],
|
|
207
|
+
key=lambda x: x["degree"],
|
|
208
|
+
reverse=True,
|
|
209
|
+
)[:6]
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
"project_name": ws.name or self.root.name,
|
|
213
|
+
"root_path": str(self.root),
|
|
214
|
+
"edition": "CodeGraphy Lite (Open Source)",
|
|
215
|
+
"total_topics": len(graph.nodes),
|
|
216
|
+
"total_dependencies": len(graph.edges),
|
|
217
|
+
"status_distribution": status_counts,
|
|
218
|
+
"lanes": [l.title for l in ws.lanes],
|
|
219
|
+
"top_connected_topics": top_components,
|
|
220
|
+
"pro_features_available": [
|
|
221
|
+
"AST Smart Context (-85% tokens)",
|
|
222
|
+
"Blast Radius (Reverse Impact)",
|
|
223
|
+
"Test Impact Static Analysis",
|
|
224
|
+
"Git Architecture Diff Overlay",
|
|
225
|
+
"Full 11 MCP Tools Suite",
|
|
226
|
+
],
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
def call_tool(self, name: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
230
|
+
if name == "list_workspace_topics":
|
|
231
|
+
return self.handle_list_workspace_topics(args)
|
|
232
|
+
elif name == "get_topic_context":
|
|
233
|
+
return self.handle_get_topic_context(args)
|
|
234
|
+
elif name == "get_architecture_summary":
|
|
235
|
+
return self.handle_get_architecture_summary(args)
|
|
236
|
+
else:
|
|
237
|
+
raise ValueError(
|
|
238
|
+
f"Unknown or proprietary tool '{name}'. "
|
|
239
|
+
"CodeGraphy Lite includes 3 core workspace tools. "
|
|
240
|
+
"Upgrade to CodeGraphy Pro for the full 11-tool suite."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def dispatch(self, request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
244
|
+
req_id = request.get("id")
|
|
245
|
+
method = request.get("method")
|
|
246
|
+
params = request.get("params", {})
|
|
247
|
+
|
|
248
|
+
if method == "initialize":
|
|
249
|
+
return {
|
|
250
|
+
"jsonrpc": "2.0",
|
|
251
|
+
"id": req_id,
|
|
252
|
+
"result": {
|
|
253
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
254
|
+
"serverInfo": {
|
|
255
|
+
"name": SERVER_NAME,
|
|
256
|
+
"version": __version__,
|
|
257
|
+
},
|
|
258
|
+
"capabilities": {
|
|
259
|
+
"tools": {"listChanged": False},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
elif method == "tools/list":
|
|
264
|
+
return {
|
|
265
|
+
"jsonrpc": "2.0",
|
|
266
|
+
"id": req_id,
|
|
267
|
+
"result": {"tools": TOOLS_MANIFEST},
|
|
268
|
+
}
|
|
269
|
+
elif method == "tools/call":
|
|
270
|
+
tool_name = params.get("name")
|
|
271
|
+
args = params.get("arguments", {})
|
|
272
|
+
try:
|
|
273
|
+
raw_res = self.call_tool(tool_name, args)
|
|
274
|
+
scrubbed = scrub_secrets(raw_res)
|
|
275
|
+
text_out = json.dumps(scrubbed, indent=2, ensure_ascii=False)
|
|
276
|
+
if len(text_out) > MAX_PAYLOAD_CHARS:
|
|
277
|
+
text_out = text_out[:MAX_PAYLOAD_CHARS] + "\n... [Payload truncated for Lite MCP]"
|
|
278
|
+
return {
|
|
279
|
+
"jsonrpc": "2.0",
|
|
280
|
+
"id": req_id,
|
|
281
|
+
"result": {
|
|
282
|
+
"content": [{"type": "text", "text": text_out}],
|
|
283
|
+
"isError": False,
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
except Exception as e:
|
|
287
|
+
return {
|
|
288
|
+
"jsonrpc": "2.0",
|
|
289
|
+
"id": req_id,
|
|
290
|
+
"result": {
|
|
291
|
+
"content": [{"type": "text", "text": f"Error: {str(e)}"}],
|
|
292
|
+
"isError": True,
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
elif method == "ping":
|
|
296
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {}}
|
|
297
|
+
elif method in ("notifications/initialized", "$/cancelRequest"):
|
|
298
|
+
return None
|
|
299
|
+
return {
|
|
300
|
+
"jsonrpc": "2.0",
|
|
301
|
+
"id": req_id,
|
|
302
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def run_stdio_server():
|
|
307
|
+
server = CodeGraphyLiteMCP()
|
|
308
|
+
while True:
|
|
309
|
+
try:
|
|
310
|
+
line = sys.stdin.readline()
|
|
311
|
+
if not line:
|
|
312
|
+
break
|
|
313
|
+
line = line.strip()
|
|
314
|
+
if not line:
|
|
315
|
+
continue
|
|
316
|
+
req = json.loads(line)
|
|
317
|
+
res = server.dispatch(req)
|
|
318
|
+
if res is not None:
|
|
319
|
+
sys.stdout.write(json.dumps(res, ensure_ascii=False) + "\n")
|
|
320
|
+
sys.stdout.flush()
|
|
321
|
+
except (KeyboardInterrupt, SystemExit):
|
|
322
|
+
break
|
|
323
|
+
except Exception as e:
|
|
324
|
+
err_res = {
|
|
325
|
+
"jsonrpc": "2.0",
|
|
326
|
+
"id": None,
|
|
327
|
+
"error": {"code": -32603, "message": f"MCP STDIO Server Error: {str(e)}"},
|
|
328
|
+
}
|
|
329
|
+
sys.stdout.write(json.dumps(err_res) + "\n")
|
|
330
|
+
sys.stdout.flush()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def main():
|
|
334
|
+
run_stdio_server()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
if __name__ == "__main__":
|
|
338
|
+
main()
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Data models for CodeGraphy Lite.
|
|
2
|
+
|
|
3
|
+
Clean, standalone Pydantic models for React Flow graph, workspace domain,
|
|
4
|
+
topics, lanes, tasks, and preview scanner payloads.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import uuid
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple
|
|
12
|
+
from pydantic import BaseModel, Field, model_validator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def utc_now_iso() -> str:
|
|
16
|
+
return datetime.now(timezone.utc).isoformat()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def new_id(prefix: str = "") -> str:
|
|
20
|
+
uid = str(uuid.uuid4())
|
|
21
|
+
return f"{prefix}_{uid[:8]}" if prefix else uid
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
NodeStatus = Literal["todo", "in_progress", "done"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FileReference(BaseModel):
|
|
28
|
+
filepath: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NodeData(BaseModel):
|
|
32
|
+
id: str
|
|
33
|
+
label: str
|
|
34
|
+
description: str = ""
|
|
35
|
+
status: NodeStatus = "todo"
|
|
36
|
+
lane_id: Optional[str] = None
|
|
37
|
+
task_id: Optional[str] = None
|
|
38
|
+
parent_topic_id: Optional[str] = None
|
|
39
|
+
subtopic_count: int = 0
|
|
40
|
+
done_subtopic_count: int = 0
|
|
41
|
+
progress_percent: int = 0
|
|
42
|
+
attached_files: List[FileReference] = Field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Position(BaseModel):
|
|
46
|
+
x: float = 0.0
|
|
47
|
+
y: float = 0.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ReactFlowNode(BaseModel):
|
|
51
|
+
id: str
|
|
52
|
+
type: str = "contextNode"
|
|
53
|
+
position: Position = Field(default_factory=Position)
|
|
54
|
+
data: NodeData
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class EdgeData(BaseModel):
|
|
58
|
+
id: str
|
|
59
|
+
source: str
|
|
60
|
+
target: str
|
|
61
|
+
sourceHandle: Optional[str] = None
|
|
62
|
+
targetHandle: Optional[str] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GraphState(BaseModel):
|
|
66
|
+
nodes: List[ReactFlowNode] = Field(default_factory=list)
|
|
67
|
+
edges: List[EdgeData] = Field(default_factory=list)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class FileSource(BaseModel):
|
|
71
|
+
id: str = Field(default_factory=lambda: new_id("src"))
|
|
72
|
+
path: str
|
|
73
|
+
type: Literal["file", "directory"] = "file"
|
|
74
|
+
recursive: bool = True
|
|
75
|
+
include: List[str] = Field(default_factory=lambda: [".py", ".ts", ".tsx", ".js", ".md", ".json"])
|
|
76
|
+
exclude: List[str] = Field(
|
|
77
|
+
default_factory=lambda: [
|
|
78
|
+
".git", "node_modules", "dist", "build", ".venv", "venv",
|
|
79
|
+
"__pycache__", "release", ".codegraphy", ".archflow"
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TopicLane(BaseModel):
|
|
85
|
+
id: str = Field(default_factory=lambda: new_id("lane"))
|
|
86
|
+
title: str = ""
|
|
87
|
+
name: Optional[str] = None
|
|
88
|
+
order: int = 0
|
|
89
|
+
collapsed: bool = False
|
|
90
|
+
color: str = "#38bdf8"
|
|
91
|
+
|
|
92
|
+
@model_validator(mode="before")
|
|
93
|
+
@classmethod
|
|
94
|
+
def sync_name_and_title(cls, data: Any) -> Any:
|
|
95
|
+
if isinstance(data, dict):
|
|
96
|
+
t = data.get("title") or data.get("name") or "Lane"
|
|
97
|
+
data["title"] = t
|
|
98
|
+
data["name"] = t
|
|
99
|
+
return data
|
|
100
|
+
|
|
101
|
+
def model_dump(self, *args, **kwargs) -> Dict[str, Any]:
|
|
102
|
+
d = super().model_dump(*args, **kwargs)
|
|
103
|
+
val = d.get("name") or d.get("title") or ""
|
|
104
|
+
d["name"] = val
|
|
105
|
+
d["title"] = val
|
|
106
|
+
return d
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TaskItem(BaseModel):
|
|
110
|
+
id: str = Field(default_factory=lambda: new_id("task"))
|
|
111
|
+
title: str
|
|
112
|
+
description: str = ""
|
|
113
|
+
status: NodeStatus = "todo"
|
|
114
|
+
lane_id: Optional[str] = None
|
|
115
|
+
assigned_user_id: Optional[str] = None
|
|
116
|
+
progress: int = 0
|
|
117
|
+
topic_ids: List[str] = Field(default_factory=list)
|
|
118
|
+
updated_at: str = Field(default_factory=utc_now_iso)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Topic(BaseModel):
|
|
122
|
+
id: str
|
|
123
|
+
label: str
|
|
124
|
+
description: str = ""
|
|
125
|
+
status: NodeStatus = "todo"
|
|
126
|
+
lane_id: Optional[str] = None
|
|
127
|
+
task_id: Optional[str] = None
|
|
128
|
+
parent_topic_id: Optional[str] = None
|
|
129
|
+
position: Position = Field(default_factory=Position)
|
|
130
|
+
attached_files: List[FileReference] = Field(default_factory=list)
|
|
131
|
+
sources: List[FileSource] = Field(default_factory=list)
|
|
132
|
+
ai_context_settings: Dict[str, Any] = Field(
|
|
133
|
+
default_factory=lambda: {"include_neighbors": True, "strip_bodies": True}
|
|
134
|
+
)
|
|
135
|
+
updated_at: str = Field(default_factory=utc_now_iso)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class WorkspaceModel(BaseModel):
|
|
139
|
+
version: str = "2.0"
|
|
140
|
+
id: str = "default_workspace"
|
|
141
|
+
name: str = "CodeGraphy Workspace"
|
|
142
|
+
root_path: Optional[str] = None
|
|
143
|
+
lanes: List[TopicLane] = Field(
|
|
144
|
+
default_factory=lambda: [
|
|
145
|
+
TopicLane(id="lane_plan", title="Planning", order=0, color="#a855f7"),
|
|
146
|
+
TopicLane(id="lane_dev", title="Development", order=1, color="#38bdf8"),
|
|
147
|
+
TopicLane(id="lane_test", title="Testing", order=2, color="#eab308"),
|
|
148
|
+
TopicLane(id="lane_done", title="Done", order=3, color="#22c55e"),
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
topics: List[Topic] = Field(default_factory=list)
|
|
152
|
+
edges: List[EdgeData] = Field(default_factory=list)
|
|
153
|
+
tasks: List[TaskItem] = Field(default_factory=list)
|
|
154
|
+
file_sources: List[FileSource] = Field(default_factory=list)
|
|
155
|
+
updated_at: str = Field(default_factory=utc_now_iso)
|
|
156
|
+
|
|
157
|
+
def get_topic_progress(self, topic_id: str) -> Tuple[int, int, int]:
|
|
158
|
+
children = [t for t in self.topics if t.parent_topic_id == topic_id]
|
|
159
|
+
if not children:
|
|
160
|
+
t = next((x for x in self.topics if x.id == topic_id), None)
|
|
161
|
+
pct = 100 if t and t.status == "done" else (50 if t and t.status == "in_progress" else 0)
|
|
162
|
+
return 0, (1 if pct == 100 else 0), pct
|
|
163
|
+
total = len(children)
|
|
164
|
+
done = sum(1 for c in children if c.status == "done")
|
|
165
|
+
pct = int((done / total) * 100) if total else 0
|
|
166
|
+
return total, done, pct
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class ScannedEdgePreview(BaseModel):
|
|
170
|
+
source_topic_id: str
|
|
171
|
+
target_topic_id: str
|
|
172
|
+
source_file: str
|
|
173
|
+
target_module: str
|
|
174
|
+
line_number: int = 1
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ScannedTopicPreview(BaseModel):
|
|
178
|
+
id: str
|
|
179
|
+
label: str
|
|
180
|
+
category: str = "module"
|
|
181
|
+
files: List[str] = Field(default_factory=list)
|
|
182
|
+
incoming_count: int = 0
|
|
183
|
+
outgoing_count: int = 0
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class ScanPreviewRequest(BaseModel):
|
|
187
|
+
root_path: str
|
|
188
|
+
ignore_dirs: List[str] = Field(
|
|
189
|
+
default_factory=lambda: [
|
|
190
|
+
".git", ".venv", "venv", "node_modules", "__pycache__",
|
|
191
|
+
"dist", "build", "release", ".codegraphy", ".archflow",
|
|
192
|
+
".idea", ".vscode", "coverage"
|
|
193
|
+
]
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class ScanPreviewResponse(BaseModel):
|
|
198
|
+
root_path: str
|
|
199
|
+
project_name: str
|
|
200
|
+
total_files: int
|
|
201
|
+
topics: List[ScannedTopicPreview] = Field(default_factory=list)
|
|
202
|
+
edges: List[ScannedEdgePreview] = Field(default_factory=list)
|
|
203
|
+
unresolved_imports: List[str] = Field(default_factory=list)
|
|
204
|
+
ignored_dirs_triggered: List[str] = Field(default_factory=list)
|