jac-coder 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.
- jac_coder/__init__.jac +0 -0
- jac_coder/api.jac +82 -0
- jac_coder/cli_entry.py +25 -0
- jac_coder/config.jac +36 -0
- jac_coder/context.jac +17 -0
- jac_coder/data/examples/ai_agent.md +90 -0
- jac_coder/data/examples/blog_app.md +386 -0
- jac_coder/data/examples/core_patterns.md +321 -0
- jac_coder/data/examples/todo_app.md +321 -0
- jac_coder/data/reference/ai.md +131 -0
- jac_coder/data/reference/backend.md +215 -0
- jac_coder/data/reference/frontend.md +271 -0
- jac_coder/data/reference/osp.md +229 -0
- jac_coder/data/reference/pitfalls.md +141 -0
- jac_coder/data/reference/syntax.md +159 -0
- jac_coder/data/rules/core_jac.md +559 -0
- jac_coder/data/rules/fullstack.md +362 -0
- jac_coder/data/rules/workflow.md +88 -0
- jac_coder/events.jac +110 -0
- jac_coder/impl/api.impl.jac +399 -0
- jac_coder/impl/config.impl.jac +163 -0
- jac_coder/impl/context.impl.jac +117 -0
- jac_coder/impl/mcp_manager.impl.jac +380 -0
- jac_coder/impl/memory.impl.jac +247 -0
- jac_coder/impl/nodes.impl.jac +259 -0
- jac_coder/impl/permission.impl.jac +62 -0
- jac_coder/impl/walkers.impl.jac +298 -0
- jac_coder/mcp_manager.jac +35 -0
- jac_coder/memory.jac +15 -0
- jac_coder/nodes.jac +306 -0
- jac_coder/permission.jac +19 -0
- jac_coder/serve_entry.jac +30 -0
- jac_coder/server.jac +324 -0
- jac_coder/tool/__init__.jac +17 -0
- jac_coder/tool/checked.jac +10 -0
- jac_coder/tool/delegation.jac +23 -0
- jac_coder/tool/filesystem.jac +25 -0
- jac_coder/tool/git.jac +18 -0
- jac_coder/tool/guarded.jac +23 -0
- jac_coder/tool/impl/checked.impl.jac +38 -0
- jac_coder/tool/impl/delegation.impl.jac +157 -0
- jac_coder/tool/impl/filesystem.impl.jac +781 -0
- jac_coder/tool/impl/git.impl.jac +115 -0
- jac_coder/tool/impl/guarded.impl.jac +72 -0
- jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
- jac_coder/tool/impl/jac_docs.impl.jac +136 -0
- jac_coder/tool/impl/jac_tools.impl.jac +79 -0
- jac_coder/tool/impl/mcp.impl.jac +32 -0
- jac_coder/tool/impl/preview.impl.jac +233 -0
- jac_coder/tool/impl/question.impl.jac +29 -0
- jac_coder/tool/impl/scaffold.impl.jac +231 -0
- jac_coder/tool/impl/search.impl.jac +85 -0
- jac_coder/tool/impl/shell.impl.jac +89 -0
- jac_coder/tool/impl/task.impl.jac +12 -0
- jac_coder/tool/impl/think.impl.jac +4 -0
- jac_coder/tool/impl/todo.impl.jac +58 -0
- jac_coder/tool/impl/validate.impl.jac +236 -0
- jac_coder/tool/impl/web.impl.jac +91 -0
- jac_coder/tool/jac_analyzer.jac +21 -0
- jac_coder/tool/jac_docs.jac +9 -0
- jac_coder/tool/jac_tools.jac +11 -0
- jac_coder/tool/mcp.jac +17 -0
- jac_coder/tool/preview.jac +31 -0
- jac_coder/tool/question.jac +7 -0
- jac_coder/tool/scaffold.jac +10 -0
- jac_coder/tool/search.jac +14 -0
- jac_coder/tool/shell.jac +12 -0
- jac_coder/tool/task.jac +9 -0
- jac_coder/tool/think.jac +5 -0
- jac_coder/tool/todo.jac +12 -0
- jac_coder/tool/validate.jac +11 -0
- jac_coder/tool/vision.jac +17 -0
- jac_coder/tool/web.jac +10 -0
- jac_coder/util/__init__.jac +18 -0
- jac_coder/util/colors.jac +20 -0
- jac_coder/util/impl/sandbox.impl.jac +38 -0
- jac_coder/util/impl/tool_output.impl.jac +208 -0
- jac_coder/util/sandbox.jac +8 -0
- jac_coder/util/tool_output.jac +29 -0
- jac_coder/walkers.jac +67 -0
- jac_coder-0.1.0.dist-info/METADATA +9 -0
- jac_coder-0.1.0.dist-info/RECORD +85 -0
- jac_coder-0.1.0.dist-info/WHEEL +5 -0
- jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
- jac_coder-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""MCP server registry implementation.
|
|
2
|
+
|
|
3
|
+
One persistent asyncio event loop runs in a background thread (mcp-manager).
|
|
4
|
+
Each MCP server gets one long-lived connection — stdio subprocess or HTTP
|
|
5
|
+
session — opened on first use and kept alive across all tool calls.
|
|
6
|
+
|
|
7
|
+
On failure the session is evicted and the next call reconnects automatically
|
|
8
|
+
(one retry). Servers are cleanly disconnected when removed.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os;
|
|
12
|
+
import asyncio;
|
|
13
|
+
import sys;
|
|
14
|
+
import threading;
|
|
15
|
+
import from contextlib { AsyncExitStack }
|
|
16
|
+
import from mcp { ClientSession }
|
|
17
|
+
import from mcp.client.stdio { stdio_client, StdioServerParameters }
|
|
18
|
+
import from mcp.client.streamable_http { streamablehttp_client }
|
|
19
|
+
import from mcp.client.sse { sse_client }
|
|
20
|
+
|
|
21
|
+
# Cached update info for jac-mcp (populated by background thread on startup).
|
|
22
|
+
glob _jac_mcp_update_info: dict = {};
|
|
23
|
+
|
|
24
|
+
# Persistent manager loop — one background thread, all MCP sessions live here.
|
|
25
|
+
glob _mgr_loop: Any = None;
|
|
26
|
+
glob _mgr_thread: Any = None;
|
|
27
|
+
glob _mgr_lock: Any = threading.Lock();
|
|
28
|
+
glob _sessions: dict = {}; # name -> ClientSession (open inside _mgr_loop)
|
|
29
|
+
glob _exit_stacks: dict = {}; # name -> AsyncExitStack (keeps transports alive)
|
|
30
|
+
glob _tools_cache: dict = {}; # {"tools": [...], "ts": float} — cached tool list
|
|
31
|
+
glob _TOOLS_CACHE_TTL: int = 30; # seconds before tool list is re-fetched
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
"""Background thread: check PyPI for latest jac-mcp version (3s timeout, fails silently)."""
|
|
35
|
+
def _check_jac_mcp_version() -> None {
|
|
36
|
+
try {
|
|
37
|
+
import json;
|
|
38
|
+
import from urllib.request { urlopen, Request };
|
|
39
|
+
import from importlib.metadata { version };
|
|
40
|
+
current: str = version("jac-mcp");
|
|
41
|
+
req = Request(
|
|
42
|
+
"https://pypi.org/pypi/jac-mcp/json",
|
|
43
|
+
headers={"User-Agent": "jac-coder/update-check"}
|
|
44
|
+
);
|
|
45
|
+
with urlopen(req, timeout=3) as resp {
|
|
46
|
+
data: dict = json.loads(resp.read().decode());
|
|
47
|
+
latest: str = data["info"]["version"];
|
|
48
|
+
_jac_mcp_update_info["current"] = current;
|
|
49
|
+
_jac_mcp_update_info["latest"] = latest;
|
|
50
|
+
_jac_mcp_update_info["update_available"] = latest != current;
|
|
51
|
+
}
|
|
52
|
+
} except Exception { }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Graph-based config persistence
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
"""Find or create the McpRegistry node attached to root."""
|
|
61
|
+
def _get_registry() -> Any {
|
|
62
|
+
import from jac_coder.nodes { McpRegistry };
|
|
63
|
+
for edge_anchor in list(root().__jac__.edges) {
|
|
64
|
+
try {
|
|
65
|
+
target_anchor = edge_anchor.target;
|
|
66
|
+
target_anchor.populate();
|
|
67
|
+
if isinstance(target_anchor.archetype, McpRegistry) {
|
|
68
|
+
return target_anchor.archetype;
|
|
69
|
+
}
|
|
70
|
+
} except Exception { }
|
|
71
|
+
}
|
|
72
|
+
save(root());
|
|
73
|
+
reg = McpRegistry();
|
|
74
|
+
root() ++> reg;
|
|
75
|
+
commit();
|
|
76
|
+
return reg;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
"""Load all server configs from the persistent registry node."""
|
|
80
|
+
def _load_configs() -> dict {
|
|
81
|
+
return dict(_get_registry().servers);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
"""Persist the full server config dict back to the registry node."""
|
|
85
|
+
def _save_configs(configs: dict) -> None {
|
|
86
|
+
reg = _get_registry();
|
|
87
|
+
reg.servers = configs;
|
|
88
|
+
save(reg);
|
|
89
|
+
commit();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# Tool / result helpers
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
"""Build a tool descriptor dict from a server name and MCP tool object."""
|
|
98
|
+
def _tool_dict(server: str, t: Any) -> dict {
|
|
99
|
+
return {
|
|
100
|
+
"server": server,
|
|
101
|
+
"name": t.name,
|
|
102
|
+
"description": t.description or "",
|
|
103
|
+
"inputSchema": t.inputSchema if hasattr(t, "inputSchema") else {}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
"""Extract plain text from an MCP tool result, joining multiple content items."""
|
|
108
|
+
def _extract_text(result: Any) -> str {
|
|
109
|
+
texts = [item.text for item in result.content if hasattr(item, "text")];
|
|
110
|
+
return "\n".join(texts) if texts else "ok";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Persistent manager loop
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
"""Return the shared asyncio event loop, starting the background thread if needed."""
|
|
119
|
+
def _get_manager_loop() -> Any {
|
|
120
|
+
global _mgr_loop, _mgr_thread;
|
|
121
|
+
with _mgr_lock {
|
|
122
|
+
if _mgr_loop is None or _mgr_thread is None or not _mgr_thread.is_alive() {
|
|
123
|
+
_mgr_loop = asyncio.new_event_loop();
|
|
124
|
+
_mgr_thread = threading.Thread(
|
|
125
|
+
target=_mgr_loop.run_forever,
|
|
126
|
+
daemon=True,
|
|
127
|
+
name="mcp-manager"
|
|
128
|
+
);
|
|
129
|
+
_mgr_thread.start();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return _mgr_loop;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
"""Submit a coroutine to the persistent manager loop and block until it completes."""
|
|
136
|
+
def _submit(coro: Any) -> Any {
|
|
137
|
+
loop = _get_manager_loop();
|
|
138
|
+
future = asyncio.run_coroutine_threadsafe(coro, loop);
|
|
139
|
+
return future.result(timeout=60);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# Connection lifecycle (async — run inside manager loop via _submit)
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
"""Open a persistent connection for *name* using the right transport and store session + exit stack."""
|
|
148
|
+
async def _open_connection(name: str, config: dict) -> None {
|
|
149
|
+
transport = config.get("type", "stdio");
|
|
150
|
+
stack = AsyncExitStack();
|
|
151
|
+
try {
|
|
152
|
+
if transport == "stdio" {
|
|
153
|
+
cmd_list = config.get("command", []);
|
|
154
|
+
if not cmd_list {
|
|
155
|
+
raise ValueError("no command configured for stdio server");
|
|
156
|
+
}
|
|
157
|
+
env = {**os.environ, **config.get("env", {})};
|
|
158
|
+
params = StdioServerParameters(command=cmd_list[0], args=cmd_list[1:], env=env);
|
|
159
|
+
(read, write) = await stack.enter_async_context(stdio_client(params));
|
|
160
|
+
} elif transport == "http" {
|
|
161
|
+
url = config.get("url", "");
|
|
162
|
+
if not url {
|
|
163
|
+
raise ValueError("no url configured for http server");
|
|
164
|
+
}
|
|
165
|
+
headers = config.get("headers", {});
|
|
166
|
+
(read, write, _) = await stack.enter_async_context(
|
|
167
|
+
streamablehttp_client(url, headers=headers)
|
|
168
|
+
);
|
|
169
|
+
} elif transport == "sse" {
|
|
170
|
+
url = config.get("url", "");
|
|
171
|
+
if not url {
|
|
172
|
+
raise ValueError("no url configured for sse server");
|
|
173
|
+
}
|
|
174
|
+
headers = config.get("headers", {});
|
|
175
|
+
(read, write) = await stack.enter_async_context(
|
|
176
|
+
sse_client(url, headers=headers)
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
raise ValueError(f"unknown transport '{transport}'");
|
|
180
|
+
}
|
|
181
|
+
session = await stack.enter_async_context(ClientSession(read, write));
|
|
182
|
+
await session.initialize();
|
|
183
|
+
_sessions[name] = session;
|
|
184
|
+
_exit_stacks[name] = stack;
|
|
185
|
+
} except Exception {
|
|
186
|
+
await stack.aclose();
|
|
187
|
+
raise;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
"""Close and evict the session for *name* — no-op if not connected."""
|
|
192
|
+
async def _close_connection(name: str) -> None {
|
|
193
|
+
stack = _exit_stacks.pop(name, None);
|
|
194
|
+
_sessions.pop(name, None);
|
|
195
|
+
if stack {
|
|
196
|
+
try {
|
|
197
|
+
await stack.aclose();
|
|
198
|
+
} except Exception { }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
"""Return the cached session for *name*, opening a fresh connection if not yet connected."""
|
|
203
|
+
async def _ensure_session(name: str, config: dict) -> Any {
|
|
204
|
+
if name not in _sessions {
|
|
205
|
+
await _open_connection(name, config);
|
|
206
|
+
}
|
|
207
|
+
return _sessions[name];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Async MCP operations — with one automatic reconnect on failure
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
"""List all tools exposed by *name*, reconnecting once if the session is stale."""
|
|
216
|
+
async def _async_list_tools(name: str, config: dict) -> list {
|
|
217
|
+
for attempt in range(2) {
|
|
218
|
+
try {
|
|
219
|
+
session = await _ensure_session(name, config);
|
|
220
|
+
result = await session.list_tools();
|
|
221
|
+
return [_tool_dict(name, t) for t in result.tools];
|
|
222
|
+
} except Exception {
|
|
223
|
+
if attempt == 0 {
|
|
224
|
+
await _close_connection(name);
|
|
225
|
+
} else {
|
|
226
|
+
raise;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
"""Call *tool_name* on *name* with *arguments*, reconnecting once if the session is stale."""
|
|
233
|
+
async def _async_call_tool(name: str, config: dict, tool_name: str, arguments: dict) -> str {
|
|
234
|
+
for attempt in range(2) {
|
|
235
|
+
try {
|
|
236
|
+
session = await _ensure_session(name, config);
|
|
237
|
+
result = await session.call_tool(tool_name, arguments);
|
|
238
|
+
return _extract_text(result);
|
|
239
|
+
} except Exception {
|
|
240
|
+
if attempt == 0 {
|
|
241
|
+
await _close_connection(name);
|
|
242
|
+
} else {
|
|
243
|
+
raise;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# Public API implementations
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
"""Validate config, open a persistent connection, and save the server to the registry."""
|
|
255
|
+
impl mcp_add_server(name: str, config: dict) -> dict {
|
|
256
|
+
if not name or not name.strip() {
|
|
257
|
+
return {"error": "name is required"};
|
|
258
|
+
}
|
|
259
|
+
transport = config.get("type", "");
|
|
260
|
+
if transport not in ("stdio", "http", "sse") {
|
|
261
|
+
return {"error": "type must be 'stdio', 'http', or 'sse'"};
|
|
262
|
+
}
|
|
263
|
+
if transport == "stdio" and not config.get("command") {
|
|
264
|
+
return {"error": "command is required for stdio servers"};
|
|
265
|
+
}
|
|
266
|
+
if transport in ("http", "sse") and not config.get("url") {
|
|
267
|
+
return {"error": "url is required for http/sse servers"};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Drop any stale connection before establishing the new one.
|
|
271
|
+
_submit(_close_connection(name));
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
tools = _submit(_async_list_tools(name, config));
|
|
275
|
+
} except Exception as e {
|
|
276
|
+
return {"error": f"Failed to connect to '{name}': {e}"};
|
|
277
|
+
}
|
|
278
|
+
configs = _load_configs();
|
|
279
|
+
configs[name] = config;
|
|
280
|
+
_save_configs(configs);
|
|
281
|
+
_tools_cache.clear(); # Invalidate cache — new server added
|
|
282
|
+
return {
|
|
283
|
+
"status": "connected",
|
|
284
|
+
"name": name,
|
|
285
|
+
"tool_count": len(tools),
|
|
286
|
+
"tools": [t["name"] for t in tools]
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
"""Close the connection and remove the server from the registry."""
|
|
292
|
+
impl mcp_remove_server(name: str) -> dict {
|
|
293
|
+
configs = _load_configs();
|
|
294
|
+
if name not in configs {
|
|
295
|
+
return {"error": f"Server '{name}' not found"};
|
|
296
|
+
}
|
|
297
|
+
_submit(_close_connection(name));
|
|
298
|
+
configs.pop(name);
|
|
299
|
+
_save_configs(configs);
|
|
300
|
+
_tools_cache.clear(); # Invalidate cache — server removed
|
|
301
|
+
return {"status": "removed", "name": name};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
"""Return all registered servers with their connection status and tool counts."""
|
|
306
|
+
impl mcp_list_servers() -> list {
|
|
307
|
+
configs = _load_configs();
|
|
308
|
+
result: list = [];
|
|
309
|
+
for (name, config) in configs.items() {
|
|
310
|
+
entry: dict = {
|
|
311
|
+
"name": name,
|
|
312
|
+
"type": config.get("type", ""),
|
|
313
|
+
"builtin": config.get("builtin", False),
|
|
314
|
+
"update_available": name == "jac-mcp" and _jac_mcp_update_info.get("update_available", False),
|
|
315
|
+
"latest_version": _jac_mcp_update_info.get("latest", "") if name == "jac-mcp" else ""
|
|
316
|
+
};
|
|
317
|
+
try {
|
|
318
|
+
tools = _submit(_async_list_tools(name, config));
|
|
319
|
+
entry["status"] = "connected";
|
|
320
|
+
entry["tool_count"] = len(tools);
|
|
321
|
+
entry["tools"] = [t["name"] for t in tools];
|
|
322
|
+
} except Exception as e {
|
|
323
|
+
entry["status"] = "error";
|
|
324
|
+
entry["error"] = str(e);
|
|
325
|
+
entry["tool_count"] = 0;
|
|
326
|
+
entry["tools"] = [];
|
|
327
|
+
}
|
|
328
|
+
result.append(entry);
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
"""Save a built-in server to the registry without a connectivity check."""
|
|
335
|
+
impl mcp_register_builtin(name: str, config: dict) -> None {
|
|
336
|
+
configs = _load_configs();
|
|
337
|
+
if name not in configs {
|
|
338
|
+
config["builtin"] = True;
|
|
339
|
+
configs[name] = config;
|
|
340
|
+
_save_configs(configs);
|
|
341
|
+
}
|
|
342
|
+
if name == "jac-mcp" {
|
|
343
|
+
threading.Thread(target=_check_jac_mcp_version, daemon=True).start();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
"""Return a flat list of all tools from all registered servers (cached for 30s)."""
|
|
349
|
+
impl mcp_get_tools() -> list {
|
|
350
|
+
import time;
|
|
351
|
+
now = time.time();
|
|
352
|
+
if _tools_cache and (now - _tools_cache.get("ts", 0)) < _TOOLS_CACHE_TTL {
|
|
353
|
+
return _tools_cache["tools"];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
configs = _load_configs();
|
|
357
|
+
all_tools: list = [];
|
|
358
|
+
for (name, config) in configs.items() {
|
|
359
|
+
try {
|
|
360
|
+
all_tools.extend(_submit(_async_list_tools(name, config)));
|
|
361
|
+
} except Exception as e {
|
|
362
|
+
sys.stderr.write(f"[mcp] get_tools({name}): {e}\n");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
_tools_cache["tools"] = all_tools;
|
|
367
|
+
_tools_cache["ts"] = now;
|
|
368
|
+
return all_tools;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
"""Call a specific tool on a specific server and return the result as text."""
|
|
373
|
+
impl mcp_call_tool(server_name: str, tool_name: str, arguments: dict) -> str {
|
|
374
|
+
configs = _load_configs();
|
|
375
|
+
config = configs.get(server_name);
|
|
376
|
+
if not config {
|
|
377
|
+
return f"Error: MCP server '{server_name}' not configured. Use mcp.add to add it.";
|
|
378
|
+
}
|
|
379
|
+
return _submit(_async_call_tool(server_name, config, tool_name, arguments));
|
|
380
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
impl find_or_create_memory(project_dir: str) -> ProjectMemory | None {
|
|
2
|
+
if not project_dir {
|
|
3
|
+
return None;
|
|
4
|
+
}
|
|
5
|
+
real_dir = os.path.realpath(project_dir);
|
|
6
|
+
matches = [root()-->][?:ProjectMemory][?project_dir==real_dir];
|
|
7
|
+
if matches {
|
|
8
|
+
return matches[0];
|
|
9
|
+
}
|
|
10
|
+
memory = ProjectMemory(project_dir=real_dir);
|
|
11
|
+
root() ++> memory;
|
|
12
|
+
return memory;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
impl _init_memory(memory: ProjectMemory, project_dir: str) -> None {
|
|
17
|
+
memory.scan_attempted = True;
|
|
18
|
+
|
|
19
|
+
# Try AST-based analysis first (fast, accurate, no LLM cost)
|
|
20
|
+
ast_success = _init_memory_ast(memory, project_dir);
|
|
21
|
+
|
|
22
|
+
if not ast_success {
|
|
23
|
+
# Fallback: LLM-based scan (slower, costs tokens, but works if AST fails)
|
|
24
|
+
_init_memory_llm(memory, project_dir);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
memory.last_updated = datetime.now().isoformat();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _init_memory_ast(memory: ProjectMemory, project_dir: str) -> bool {
|
|
32
|
+
jac_files = _find_jac_files(project_dir);
|
|
33
|
+
if not jac_files {
|
|
34
|
+
return False;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
analysis = _analyze_project(project_dir);
|
|
39
|
+
} except Exception as e {
|
|
40
|
+
sys.stderr.write(f"[memory] AST analysis failed: {e}\n");
|
|
41
|
+
return False;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
archetypes = analysis.get("archetypes", []);
|
|
45
|
+
if not archetypes {
|
|
46
|
+
return False;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Build node details
|
|
50
|
+
nodes = [
|
|
51
|
+
a
|
|
52
|
+
for a in archetypes
|
|
53
|
+
if a["kind"] == "node"
|
|
54
|
+
];
|
|
55
|
+
walkers = [
|
|
56
|
+
a
|
|
57
|
+
for a in archetypes
|
|
58
|
+
if a["kind"] == "walker"
|
|
59
|
+
];
|
|
60
|
+
edges = [
|
|
61
|
+
a
|
|
62
|
+
for a in archetypes
|
|
63
|
+
if a["kind"] == "edge"
|
|
64
|
+
];
|
|
65
|
+
client_comps = [
|
|
66
|
+
a
|
|
67
|
+
for a in archetypes
|
|
68
|
+
if a.get("rel_file", "").endswith(".cl.jac")
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
# Architecture summary
|
|
72
|
+
arch_parts: list = [];
|
|
73
|
+
if nodes {
|
|
74
|
+
arch_parts.append(f"{len(nodes)} nodes");
|
|
75
|
+
}
|
|
76
|
+
if walkers {
|
|
77
|
+
arch_parts.append(f"{len(walkers)} walkers");
|
|
78
|
+
}
|
|
79
|
+
if edges {
|
|
80
|
+
arch_parts.append(f"{len(edges)} edges");
|
|
81
|
+
}
|
|
82
|
+
if client_comps {
|
|
83
|
+
arch_parts.append(f"{len(client_comps)} client components");
|
|
84
|
+
}
|
|
85
|
+
memory.architecture = f"Jac project with {', '.join(arch_parts)}";
|
|
86
|
+
|
|
87
|
+
# Node details (compact format for context injection)
|
|
88
|
+
if nodes {
|
|
89
|
+
node_lines: list = ["Nodes:"];
|
|
90
|
+
for n in nodes[:15] {
|
|
91
|
+
fields_str = ", ".join(n["fields"][:6]) if n["fields"] else "no fields";
|
|
92
|
+
abilities_str = "";
|
|
93
|
+
if n["abilities"] {
|
|
94
|
+
abilities_str = " | " + ", ".join(n["abilities"][:4]);
|
|
95
|
+
}
|
|
96
|
+
node_lines.append(
|
|
97
|
+
f" {n['name']} ({n['rel_file']}:{n['line']}) — {fields_str}{abilities_str}"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
memory.node_details = "\n".join(node_lines);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Walker details with entry points
|
|
104
|
+
if walkers {
|
|
105
|
+
walker_lines: list = ["Walkers:"];
|
|
106
|
+
for w in walkers[:15] {
|
|
107
|
+
visits = "";
|
|
108
|
+
if w["entry_points"] {
|
|
109
|
+
visit_names = [ep["node_type"] for ep in w["entry_points"]];
|
|
110
|
+
visits = " — visits: " + " → ".join(visit_names);
|
|
111
|
+
}
|
|
112
|
+
walker_lines.append(f" {w['name']} ({w['rel_file']}:{w['line']}){visits}");
|
|
113
|
+
}
|
|
114
|
+
memory.walker_details = "\n".join(walker_lines);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Client components
|
|
118
|
+
if client_comps {
|
|
119
|
+
comp_lines: list = ["Client Components:"];
|
|
120
|
+
for c in client_comps[:10] {
|
|
121
|
+
state = ", ".join(c["fields"][:4]) if c["fields"] else "no state";
|
|
122
|
+
comp_lines.append(f" {c['name']} ({c['rel_file']}:{c['line']}) — {state}");
|
|
123
|
+
}
|
|
124
|
+
# Append to node_details
|
|
125
|
+
if memory.node_details {
|
|
126
|
+
memory.node_details += "\n\n" + "\n".join(comp_lines);
|
|
127
|
+
} else {
|
|
128
|
+
memory.node_details = "\n".join(comp_lines);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Edge details → graph topology
|
|
133
|
+
if edges {
|
|
134
|
+
edge_strs: list = [];
|
|
135
|
+
for e in edges[:10] {
|
|
136
|
+
fields_str_e = ", ".join(e["fields"][:3]) if e["fields"] else "";
|
|
137
|
+
edge_strs.append(
|
|
138
|
+
f"{e['name']}({fields_str_e})" if fields_str_e else e["name"]
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
memory.graph_topology = ", ".join(edge_strs);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Import map
|
|
145
|
+
all_imports = analysis.get("imports", {});
|
|
146
|
+
exports = analysis.get("exports", {});
|
|
147
|
+
if exports {
|
|
148
|
+
imp_lines: list = ["Import map:"];
|
|
149
|
+
for (file, syms) in sorted(exports.items())[:15] {
|
|
150
|
+
imp_lines.append(f" {file}: exports [{', '.join(syms[:8])}]");
|
|
151
|
+
}
|
|
152
|
+
memory.import_map = "\n".join(imp_lines);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# File map (from exports — more accurate than LLM guesses)
|
|
156
|
+
for (file, syms) in exports.items() {
|
|
157
|
+
memory.file_map[file] = ", ".join(syms[:6]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
print(
|
|
161
|
+
f"[memory] AST scan complete: {len(archetypes)} archetypes from {len(
|
|
162
|
+
jac_files
|
|
163
|
+
)} files"
|
|
164
|
+
);
|
|
165
|
+
return True;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _init_memory_llm(memory: ProjectMemory, project_dir: str) -> None {
|
|
170
|
+
jac_files: list = [];
|
|
171
|
+
try {
|
|
172
|
+
for (dirpath, dirnames, filenames) in os.walk(project_dir) {
|
|
173
|
+
dirnames[:] = [
|
|
174
|
+
d
|
|
175
|
+
for d in dirnames
|
|
176
|
+
if not d.startswith(".") and d not in ("node_modules", "__pycache__")
|
|
177
|
+
];
|
|
178
|
+
for filename in filenames {
|
|
179
|
+
if filename.endswith(".jac") or filename == "jac.toml" {
|
|
180
|
+
rel = os.path.relpath(os.path.join(dirpath, filename), project_dir);
|
|
181
|
+
jac_files.append(rel);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} except Exception { }
|
|
186
|
+
|
|
187
|
+
if not jac_files {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
file_tree = "\n".join(jac_files[:30]);
|
|
192
|
+
key_parts: list = [];
|
|
193
|
+
for fname in ["jac.toml", "main.jac"] {
|
|
194
|
+
fpath = os.path.join(project_dir, fname);
|
|
195
|
+
try {
|
|
196
|
+
with open(fpath, "r") as f {
|
|
197
|
+
content = f.read()[:2000];
|
|
198
|
+
}
|
|
199
|
+
key_parts.append(f"=== {fname} ===\n{content}");
|
|
200
|
+
} except Exception { }
|
|
201
|
+
}
|
|
202
|
+
key_files = "\n\n".join(key_parts) if key_parts else "No key files found";
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
scan = memory.profile_project(file_tree=file_tree, key_files=key_files);
|
|
206
|
+
memory.architecture = scan.architecture;
|
|
207
|
+
memory.file_map = scan.file_map;
|
|
208
|
+
memory.conventions = scan.conventions;
|
|
209
|
+
} except Exception as e {
|
|
210
|
+
sys.stderr.write(f"[memory] LLM scan failed: {e}\n");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
impl update_memory_from_session(
|
|
216
|
+
memory: ProjectMemory, files_modified: list[str], session_summary: str
|
|
217
|
+
) -> None {
|
|
218
|
+
if not files_modified and not session_summary {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
learnings = memory.collect_learnings(
|
|
223
|
+
files_modified=files_modified, session_summary=session_summary
|
|
224
|
+
);
|
|
225
|
+
for (path, desc) in learnings.new_files.items() {
|
|
226
|
+
memory.file_map[path] = desc;
|
|
227
|
+
}
|
|
228
|
+
for conv in learnings.new_conventions {
|
|
229
|
+
if conv not in memory.conventions {
|
|
230
|
+
memory.conventions.append(conv);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for decision in learnings.new_decisions {
|
|
234
|
+
if decision not in memory.past_decisions {
|
|
235
|
+
memory.past_decisions.append(decision);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for issue in learnings.known_issues {
|
|
239
|
+
if issue not in memory.known_issues {
|
|
240
|
+
memory.known_issues.append(issue);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
memory.last_updated = datetime.now().isoformat();
|
|
244
|
+
} except Exception as e {
|
|
245
|
+
sys.stderr.write(f"[memory] update failed: {e}\n");
|
|
246
|
+
}
|
|
247
|
+
}
|