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.
Files changed (85) hide show
  1. jac_coder/__init__.jac +0 -0
  2. jac_coder/api.jac +82 -0
  3. jac_coder/cli_entry.py +25 -0
  4. jac_coder/config.jac +36 -0
  5. jac_coder/context.jac +17 -0
  6. jac_coder/data/examples/ai_agent.md +90 -0
  7. jac_coder/data/examples/blog_app.md +386 -0
  8. jac_coder/data/examples/core_patterns.md +321 -0
  9. jac_coder/data/examples/todo_app.md +321 -0
  10. jac_coder/data/reference/ai.md +131 -0
  11. jac_coder/data/reference/backend.md +215 -0
  12. jac_coder/data/reference/frontend.md +271 -0
  13. jac_coder/data/reference/osp.md +229 -0
  14. jac_coder/data/reference/pitfalls.md +141 -0
  15. jac_coder/data/reference/syntax.md +159 -0
  16. jac_coder/data/rules/core_jac.md +559 -0
  17. jac_coder/data/rules/fullstack.md +362 -0
  18. jac_coder/data/rules/workflow.md +88 -0
  19. jac_coder/events.jac +110 -0
  20. jac_coder/impl/api.impl.jac +399 -0
  21. jac_coder/impl/config.impl.jac +163 -0
  22. jac_coder/impl/context.impl.jac +117 -0
  23. jac_coder/impl/mcp_manager.impl.jac +380 -0
  24. jac_coder/impl/memory.impl.jac +247 -0
  25. jac_coder/impl/nodes.impl.jac +259 -0
  26. jac_coder/impl/permission.impl.jac +62 -0
  27. jac_coder/impl/walkers.impl.jac +298 -0
  28. jac_coder/mcp_manager.jac +35 -0
  29. jac_coder/memory.jac +15 -0
  30. jac_coder/nodes.jac +306 -0
  31. jac_coder/permission.jac +19 -0
  32. jac_coder/serve_entry.jac +30 -0
  33. jac_coder/server.jac +324 -0
  34. jac_coder/tool/__init__.jac +17 -0
  35. jac_coder/tool/checked.jac +10 -0
  36. jac_coder/tool/delegation.jac +23 -0
  37. jac_coder/tool/filesystem.jac +25 -0
  38. jac_coder/tool/git.jac +18 -0
  39. jac_coder/tool/guarded.jac +23 -0
  40. jac_coder/tool/impl/checked.impl.jac +38 -0
  41. jac_coder/tool/impl/delegation.impl.jac +157 -0
  42. jac_coder/tool/impl/filesystem.impl.jac +781 -0
  43. jac_coder/tool/impl/git.impl.jac +115 -0
  44. jac_coder/tool/impl/guarded.impl.jac +72 -0
  45. jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
  46. jac_coder/tool/impl/jac_docs.impl.jac +136 -0
  47. jac_coder/tool/impl/jac_tools.impl.jac +79 -0
  48. jac_coder/tool/impl/mcp.impl.jac +32 -0
  49. jac_coder/tool/impl/preview.impl.jac +233 -0
  50. jac_coder/tool/impl/question.impl.jac +29 -0
  51. jac_coder/tool/impl/scaffold.impl.jac +231 -0
  52. jac_coder/tool/impl/search.impl.jac +85 -0
  53. jac_coder/tool/impl/shell.impl.jac +89 -0
  54. jac_coder/tool/impl/task.impl.jac +12 -0
  55. jac_coder/tool/impl/think.impl.jac +4 -0
  56. jac_coder/tool/impl/todo.impl.jac +58 -0
  57. jac_coder/tool/impl/validate.impl.jac +236 -0
  58. jac_coder/tool/impl/web.impl.jac +91 -0
  59. jac_coder/tool/jac_analyzer.jac +21 -0
  60. jac_coder/tool/jac_docs.jac +9 -0
  61. jac_coder/tool/jac_tools.jac +11 -0
  62. jac_coder/tool/mcp.jac +17 -0
  63. jac_coder/tool/preview.jac +31 -0
  64. jac_coder/tool/question.jac +7 -0
  65. jac_coder/tool/scaffold.jac +10 -0
  66. jac_coder/tool/search.jac +14 -0
  67. jac_coder/tool/shell.jac +12 -0
  68. jac_coder/tool/task.jac +9 -0
  69. jac_coder/tool/think.jac +5 -0
  70. jac_coder/tool/todo.jac +12 -0
  71. jac_coder/tool/validate.jac +11 -0
  72. jac_coder/tool/vision.jac +17 -0
  73. jac_coder/tool/web.jac +10 -0
  74. jac_coder/util/__init__.jac +18 -0
  75. jac_coder/util/colors.jac +20 -0
  76. jac_coder/util/impl/sandbox.impl.jac +38 -0
  77. jac_coder/util/impl/tool_output.impl.jac +208 -0
  78. jac_coder/util/sandbox.jac +8 -0
  79. jac_coder/util/tool_output.jac +29 -0
  80. jac_coder/walkers.jac +67 -0
  81. jac_coder-0.1.0.dist-info/METADATA +9 -0
  82. jac_coder-0.1.0.dist-info/RECORD +85 -0
  83. jac_coder-0.1.0.dist-info/WHEEL +5 -0
  84. jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
  85. jac_coder-0.1.0.dist-info/top_level.txt +1 -0
jac_coder/nodes.jac ADDED
@@ -0,0 +1,306 @@
1
+ import os;
2
+
3
+ import from uuid { uuid4 }
4
+ import from datetime { datetime }
5
+ import from jac_coder.config { llm, get_config, get_data_dir }
6
+
7
+ # MainAgent tools — full orchestrator set
8
+ import from jac_coder.tool.think { think }
9
+ import from jac_coder.tool.delegation { spawn_agent }
10
+ import from jac_coder.tool.todo { update_todos }
11
+ import from jac_coder.tool.jac_docs { jac_docs }
12
+ import from jac_coder.tool.question { ask_question }
13
+ import from jac_coder.tool.scaffold { scaffold_project }
14
+ import from jac_coder.tool.web { web_fetch, web_search }
15
+ import from jac_coder.tool.guarded { run_command }
16
+ import from jac_coder.tool.jac_tools { jac_check, jac_run }
17
+ import from jac_coder.tool.search { grep_search, find_files }
18
+ import from jac_coder.tool.filesystem { read_file, list_files }
19
+ import from jac_coder.tool.git { git_status, git_diff, git_commit, git_log }
20
+ import from jac_coder.tool.checked { write_code, edit_code }
21
+ import from jac_coder.tool.jac_analyzer { analyze_project, find_symbol }
22
+ import from jac_coder.tool.validate { validate_project }
23
+ import from jac_coder.tool.preview { capture_preview, evaluate_preview }
24
+ import from jac_coder.tool.mcp { mcp_call }
25
+
26
+
27
+ def load_subagent_rules() -> dict {
28
+ data_dir = get_data_dir();
29
+ rules: dict = {};
30
+ rules_dir = os.path.join(data_dir, "rules");
31
+ # Load all rule files from rules/ directory
32
+ for (key, filename) in [
33
+ ("core_jac_rules", "core_jac.md"),
34
+ ("fullstack_rules", "fullstack.md"),
35
+ ("workflow_rules", "workflow.md")
36
+ ] {
37
+ filepath = os.path.join(rules_dir, filename);
38
+ try {
39
+ with open(filepath, "r") as f {
40
+ rules[key] = f.read();
41
+ }
42
+ } except (FileNotFoundError, OSError) {
43
+ rules[key] = "";
44
+ }
45
+ }
46
+ # Build TOC of available docs for jac_docs() tool
47
+ toc_lines: list = ["Call jac_docs(query) to look up Jac syntax. Available topics:"];
48
+ for subdir in ["reference", "examples"] {
49
+ search_dir = os.path.join(data_dir, subdir);
50
+ if not os.path.isdir(search_dir) {
51
+ continue;
52
+ }
53
+ for fname in sorted(os.listdir(search_dir)) {
54
+ if not fname.endswith(".md") {
55
+ continue;
56
+ }
57
+ filepath = os.path.join(search_dir, fname);
58
+ try {
59
+ with open(filepath, "r") as f {
60
+ content = f.read();
61
+ }
62
+ headings = [
63
+ line.replace("## ", "").strip()
64
+ for line in content.split("\n")
65
+ if line.startswith("## ")
66
+ ];
67
+ label = subdir + "/" + fname.replace(".md", "");
68
+ toc_lines.append(f" {label}: {', '.join(headings)}");
69
+ } except (FileNotFoundError, OSError) { }
70
+ }
71
+ }
72
+ rules["available_docs"] = "\n".join(toc_lines);
73
+ return rules;
74
+ }
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Response objects
78
+ # ---------------------------------------------------------------------------
79
+ obj AgentResponse {
80
+ has content: str;
81
+ has agent_mode: str;
82
+ has tools_used: list[str];
83
+ has files_modified: list[str];
84
+ has has_errors: bool;
85
+ has next_steps: list[str];
86
+ }
87
+
88
+ sem AgentResponse.content = "Main response text to display to user";
89
+ sem AgentResponse.agent_mode = "Always 'main' for MainAgent responses";
90
+ sem AgentResponse.tools_used = "List of tool names that were called during execution";
91
+ sem AgentResponse.files_modified = "List of file paths that were written or edited";
92
+ sem AgentResponse.has_errors = "TRUE if any tool execution failed, FALSE otherwise";
93
+ sem AgentResponse.next_steps = "Suggested follow-up actions for the user (empty list if none)";
94
+
95
+
96
+ obj SubAgentResult {
97
+ has content: str;
98
+ has files_modified: list[str];
99
+ has errors: list[str];
100
+ has tools_used: list[str];
101
+ has iterations_used: int;
102
+ }
103
+
104
+ sem SubAgentResult.content = "The SubAgent's response to the task";
105
+ sem SubAgentResult.files_modified = "Files written or edited during execution";
106
+ sem SubAgentResult.errors = "Errors encountered (jac_check failures, tool errors)";
107
+ sem SubAgentResult.tools_used = "Tools called during execution";
108
+ sem SubAgentResult.iterations_used = "Number of ReAct iterations consumed";
109
+
110
+
111
+ obj ProjectScan {
112
+ has architecture: str;
113
+ has file_map: dict[str, str];
114
+ has conventions: list[str];
115
+ }
116
+
117
+
118
+ obj SessionLearnings {
119
+ has new_files: dict[str, str];
120
+ has new_conventions: list[str];
121
+ has new_decisions: list[str];
122
+ has known_issues: list[str];
123
+ }
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # McpRegistry — persists MCP server configs in the graph
128
+ # ---------------------------------------------------------------------------
129
+ node McpRegistry {
130
+ has servers: dict[str, dict] = {};
131
+ }
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # ProjectMemory — persists codebase knowledge across sessions
136
+ # ---------------------------------------------------------------------------
137
+ node ProjectMemory {
138
+ has project_dir: str = "",
139
+ architecture: str = "",
140
+ file_map: dict[str, str] = {},
141
+ conventions: list[str] = [],
142
+ past_decisions: list[str] = [],
143
+ known_issues: list[str] = [],
144
+ graph_topology: str = "",
145
+ node_details: str = "",
146
+ walker_details: str = "",
147
+ import_map: str = "",
148
+ scan_attempted: bool = False,
149
+ last_updated: str = "";
150
+
151
+ def profile_project(file_tree: str, key_files: str) -> ProjectScan by llm(
152
+ temperature=0.1
153
+ );
154
+
155
+ def collect_learnings(
156
+ files_modified: list[str], session_summary: str
157
+ ) -> SessionLearnings by llm(temperature=0.1);
158
+
159
+ def summarize() -> str;
160
+ }
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # Session — persistent chat state
165
+ # ---------------------------------------------------------------------------
166
+ node Session {
167
+ has id: str = "",
168
+ title: str = "New Session",
169
+ directory: str = "",
170
+ agent: str = "main",
171
+ status: str = "active",
172
+ created_at: str = "",
173
+ updated_at: str = "",
174
+ chat_history: list[dict] = [],
175
+ last_agent: str = "",
176
+ active_files: list[str] = [],
177
+ pending_errors: list[str] = [],
178
+ project_summary: str = "";
179
+
180
+ def postinit;
181
+ }
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # MainAgent — the orchestrator node
186
+ # ---------------------------------------------------------------------------
187
+ node MainAgent {
188
+ def respond(message: str, chat_history: list[dict]) -> str by llm(
189
+ tools=[
190
+ # Think — explicit reasoning before acting or delegating
191
+ think,
192
+ # Understand — read, search, analyze the codebase
193
+ read_file,
194
+ grep_search,
195
+ find_files,
196
+ list_files,
197
+ analyze_project,
198
+ find_symbol,
199
+ jac_docs,
200
+ # Act — handle simple tasks directly
201
+ edit_code,
202
+ write_code,
203
+ run_command,
204
+ jac_run,
205
+ validate_project,
206
+ scaffold_project,
207
+ # Git — first-class version control
208
+ git_status,
209
+ git_diff,
210
+ git_log,
211
+ git_commit,
212
+ # Web — search and fetch external resources
213
+ web_fetch,
214
+ web_search,
215
+ # Visual — screenshot and evaluate live preview UI
216
+ capture_preview,
217
+ evaluate_preview,
218
+ # Delegate — spawn SubAgent walkers for complex tasks
219
+ spawn_agent,
220
+ # Interact — ask user questions, track todos
221
+ ask_question,
222
+ update_todos,
223
+ # MCP — call tools from connected MCP servers
224
+ mcp_call
225
+ ],
226
+ incl_info=_subagent_rules,
227
+ max_react_iterations=40,
228
+ temperature=0.2,
229
+ stream=True,
230
+ logging=True
231
+ );
232
+ }
233
+
234
+
235
+ # ---------------------------------------------------------------------------
236
+ # SubAgent walkers — spawned on MainAgent node for task delegation
237
+ # ---------------------------------------------------------------------------
238
+ glob _subagent_rules: dict = load_subagent_rules();
239
+
240
+ walker WorkerAgent {
241
+ has task: str;
242
+ has result_content: str = "";
243
+ has result_tools: list[str] = [];
244
+ has result_files: list[str] = [];
245
+
246
+ def do_work(task_str: str) -> str by llm(
247
+ tools=[
248
+ jac_docs,
249
+ analyze_project,
250
+ find_symbol,
251
+ read_file,
252
+ list_files,
253
+ grep_search,
254
+ find_files,
255
+ write_code,
256
+ edit_code,
257
+ run_command,
258
+ jac_run,
259
+ validate_project,
260
+ scaffold_project,
261
+ git_status,
262
+ git_diff,
263
+ git_commit,
264
+ git_log,
265
+ web_fetch,
266
+ web_search,
267
+ capture_preview,
268
+ evaluate_preview,
269
+ mcp_call
270
+ ],
271
+ incl_info=_subagent_rules,
272
+ max_react_iterations=30,
273
+ temperature=0.2,
274
+ stream=True,
275
+ logging=True
276
+ );
277
+ can execute with MainAgent entry;
278
+ }
279
+
280
+ walker ExplorerAgent {
281
+ has task: str;
282
+ has result_content: str = "";
283
+ has result_tools: list[str] = [];
284
+ has result_files: list[str] = [];
285
+
286
+ def do_work(task_str: str) -> str by llm(
287
+ tools=[
288
+ jac_docs,
289
+ analyze_project,
290
+ find_symbol,
291
+ read_file,
292
+ list_files,
293
+ grep_search,
294
+ find_files,
295
+ git_log,
296
+ web_search,
297
+ web_fetch
298
+ ],
299
+ incl_info=_subagent_rules,
300
+ max_react_iterations=30,
301
+ temperature=0.2,
302
+ stream=True,
303
+ logging=True
304
+ );
305
+ can execute with MainAgent entry;
306
+ }
@@ -0,0 +1,19 @@
1
+ import fnmatch;
2
+
3
+ obj PermissionRule {
4
+ has tool: str = "*",
5
+ pattern: str = "*",
6
+ action: str = "ask"; # "allow" | "deny" | "ask"
7
+ }
8
+
9
+ obj PermissionEngine {
10
+ has rules: list = [],
11
+ always_allowed: dict = {};
12
+
13
+ def init_defaults() -> None;
14
+ def check(tool_id: str, resource: str = "") -> str;
15
+ def remember_allow(tool_id: str, pattern: str = "*") -> None;
16
+ def enable_web_mode() -> None;
17
+ }
18
+
19
+ glob permission_engine: PermissionEngine = PermissionEngine();
@@ -0,0 +1,30 @@
1
+ """Entry point for `jac-coder-server` command.
2
+
3
+ Mirrors the jaseci cli_boot.jac pattern — jaclang's import hook allows this
4
+ .jac file to be referenced directly from pyproject.toml [project.scripts]:
5
+
6
+ jac-coder-server = "jac_coder.serve_entry:main"
7
+
8
+ Finds server.jac inside the installed jac_coder package and runs it with
9
+ `jac run`. Works both in dev (editable install) and after pip install.
10
+ """
11
+
12
+ import os;
13
+ import sys;
14
+ import subprocess;
15
+ import from pathlib { Path }
16
+
17
+
18
+ """Start the JacCoder stdio server."""
19
+ def main -> None {
20
+ # __file__ is jac_coder/serve_entry.jac — server.jac lives next to it.
21
+ pkg_dir = Path(__file__).parent;
22
+ server_jac = pkg_dir / "server.jac";
23
+
24
+ if not server_jac.exists() {
25
+ print(f"Error: server.jac not found at {server_jac}", file=sys.stderr);
26
+ sys.exit(1);
27
+ }
28
+
29
+ sys.exit(subprocess.call(["jac", "run", str(server_jac)] + sys.argv[1:]));
30
+ }
jac_coder/server.jac ADDED
@@ -0,0 +1,324 @@
1
+ """JacCoder stdio server.
2
+
3
+ This is the bridge between the VS Code extension (TypeScript) and the
4
+ JacCoder AI agent (Jac). The extension spawns this file as a subprocess
5
+ and they talk to each other through stdin/stdout using JSON messages.
6
+
7
+ How it works:
8
+ Extension --> sends a JSON request --> stdin --> this server
9
+ This server --> does the work --> stdout --> extension
10
+
11
+ Message format (JSON-RPC 2.0):
12
+ Request (extension asks something, has an "id"):
13
+ health → check if server is alive
14
+ session.create → start a new chat session
15
+ session.list → get all sessions
16
+ session.get → get one session by id
17
+ session.close → close a session
18
+ chat → send a message to the AI agent
19
+ mcp.add → register an MCP tool server
20
+ mcp.remove → remove an MCP tool server
21
+ mcp.list → list all MCP tool servers
22
+
23
+ Notification (fire-and-forget, no "id"):
24
+ chat.cancel → stop the currently running agent (sent by extension)
25
+ server.ready → server started successfully (sent by us, once)
26
+ chat.event → live updates while agent is thinking/running tools
27
+ """
28
+
29
+ import sys;
30
+ import os;
31
+ import json;
32
+ import asyncio;
33
+ import logging;
34
+ import threading;
35
+ import from typing { Any }
36
+
37
+ import from jac_coder.api {
38
+ initialize,
39
+ chat as _api_chat,
40
+ create_session as _api_create_session,
41
+ list_sessions as _api_list_sessions,
42
+ get_session as _api_get_session,
43
+ close_session as _api_close_session,
44
+ api_mcp_add as _api_mcp_add,
45
+ api_mcp_remove as _api_mcp_remove,
46
+ api_mcp_list as _api_mcp_list
47
+ }
48
+
49
+
50
+ # Debug logs — goes to stderr, not stdout, so JSON messages stay clean.
51
+ glob logger = logging.getLogger("jaccoder.stdio");
52
+
53
+ # Save a private copy of stdout before we redirect it to stderr.
54
+ # All JSON writes use this — so accidental print() calls never reach the extension.
55
+ glob _real_stdout: Any = os.fdopen(os.dup(1), 'w', 1);
56
+
57
+ # Prevents two threads writing JSON at the same time and mixing the output.
58
+ glob _stdout_lock: Any = threading.Lock();
59
+
60
+ # Tracks cancelled sessions — set to True when user hits cancel.
61
+ glob _stop_flags: dict = {};
62
+
63
+ # Holds the running thread per session — used to force-stop it on cancel.
64
+ glob _chat_threads: dict = {};
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # JSON-RPC 2.0 message builders
69
+ # These just build the correct dict shape — nothing else.
70
+ # ---------------------------------------------------------------------------
71
+
72
+ """Build a success response for a request."""
73
+ def _ok(req_id: Any, result: Any) -> dict {
74
+ return {"jsonrpc": "2.0", "id": req_id, "result": result};
75
+ }
76
+
77
+ """Build an error response for a request."""
78
+ def _err(req_id: Any, code: int, message: str) -> dict {
79
+ return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}};
80
+ }
81
+
82
+ """Build a server-push notification (no id — no reply expected)."""
83
+ def _notify(method: str, params: dict) -> dict {
84
+ return {"jsonrpc": "2.0", "method": method, "params": params};
85
+ }
86
+
87
+ """Build a cancelled response — sent when the user aborts a chat turn."""
88
+ def _cancelled(req_id: Any) -> dict {
89
+ return _ok(req_id, {"response": "", "agent": "cancelled", "cancelled": True});
90
+ }
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Writes one JSON message to the extension — one at a time, thread-safe.
95
+ # ---------------------------------------------------------------------------
96
+
97
+ """Write one JSON message to stdout (thread-safe)."""
98
+ def _write(msg: dict) -> None {
99
+ global _stdout_lock, _real_stdout;
100
+ line = json.dumps(msg, ensure_ascii=False);
101
+ with _stdout_lock {
102
+ _real_stdout.write(line + "\n");
103
+ _real_stdout.flush();
104
+ }
105
+ }
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Chat handler
110
+ #
111
+ # Each chat turn runs in its own thread because the LLM call is blocking
112
+ # (can take seconds or minutes). We can't block the main asyncio loop or
113
+ # we'd never be able to receive a chat.cancel notification while waiting.
114
+ #
115
+ # Flow:
116
+ # 1. Thread starts, calls the AI agent (_api_chat)
117
+ # 2. Agent fires on_event() for every tool it runs → we stream those back
118
+ # 3. When agent finishes → we send the final response
119
+ # 4. If user cancelled → we send a cancelled frame instead
120
+ # ---------------------------------------------------------------------------
121
+
122
+ """Run one chat turn in a background thread and stream results back."""
123
+ def _handle_chat(req_id: Any, params: dict) -> None {
124
+ session_id: str = params.get("session_id", "");
125
+ message: str = params.get("message", "");
126
+ agent_context: str = params.get("agent_context", "") or "";
127
+
128
+ _stop_flags[session_id] = False;
129
+
130
+ # Called by the agent each time it does something (reads a file, calls a tool, etc.)
131
+ # We forward these as chat.event notifications so the UI can show live progress.
132
+ def on_event(event_type: str, data: dict) -> None {
133
+ if _stop_flags.get(session_id) {
134
+ raise KeyboardInterrupt("cancelled"); # unwinds the agent immediately
135
+ }
136
+ _write(_notify("chat.event", {"type": event_type, **data}));
137
+ }
138
+
139
+ try {
140
+ result = _api_chat(
141
+ session_id=session_id,
142
+ message=message,
143
+ on_event=on_event,
144
+ agent_context=agent_context
145
+ );
146
+ if _stop_flags.get(session_id) {
147
+ _write(_cancelled(req_id));
148
+ } else {
149
+ _write(_ok(req_id, result));
150
+ }
151
+ } except (KeyboardInterrupt, InterruptedError) {
152
+ _write(_cancelled(req_id));
153
+ } except Exception as e {
154
+ logger.error("chat error: %s", e);
155
+ _write(_err(req_id, -32000, str(e)));
156
+ } finally {
157
+ _stop_flags.pop(session_id, None);
158
+ _chat_threads.pop(session_id, None);
159
+ }
160
+ }
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # Request dispatcher
165
+ #
166
+ # Routes each incoming request to the right API call and sends back the result.
167
+ # Runs inside the asyncio event loop so Jac graph operations (root, spawn, -->)
168
+ # are always available.
169
+ # ---------------------------------------------------------------------------
170
+
171
+ """Route one JSON-RPC request to the correct API function."""
172
+ async def _dispatch(req_id: Any, method: str, params: dict) -> None {
173
+ try {
174
+ if method == "health" {
175
+ _write(_ok(req_id, {"status": "ok", "service": "jac-coder"}));
176
+
177
+ } elif method == "session.create" {
178
+ _write(_ok(req_id, _api_create_session(
179
+ directory=params.get("directory", ""),
180
+ title=params.get("title", "New Session")
181
+ )));
182
+
183
+ } elif method == "session.list" {
184
+ _write(_ok(req_id, {"sessions": _api_list_sessions()}));
185
+
186
+ } elif method == "session.get" {
187
+ _write(_ok(req_id, _api_get_session(session_id=params["session_id"])));
188
+
189
+ } elif method == "session.close" {
190
+ _write(_ok(req_id, _api_close_session(session_id=params["session_id"])));
191
+
192
+ } elif method == "chat" {
193
+ # Chat runs in a thread — LLM calls are blocking and can take a long time.
194
+ # The thread writes events and the final response directly via _write().
195
+ sid: str = params.get("session_id", "");
196
+ chat_thread = threading.Thread(
197
+ target=_handle_chat,
198
+ args=(req_id, params),
199
+ daemon=True,
200
+ name=f"chat-{sid[:8]}"
201
+ );
202
+ _chat_threads[sid] = chat_thread;
203
+ chat_thread.start();
204
+
205
+ } elif method == "mcp.add" {
206
+ _write(_ok(req_id, _api_mcp_add(
207
+ name=params["name"],
208
+ config=params["config"]
209
+ )));
210
+
211
+ } elif method == "mcp.remove" {
212
+ _write(_ok(req_id, _api_mcp_remove(name=params["name"])));
213
+
214
+ } elif method == "mcp.list" {
215
+ _write(_ok(req_id, {"servers": _api_mcp_list()}));
216
+
217
+ } else {
218
+ _write(_err(req_id, -32601, f"Method not found: {method}"));
219
+ }
220
+
221
+ } except KeyError as exc {
222
+ _write(_err(req_id, -32602, f"Missing required param: {exc}"));
223
+ } except BaseException as exc {
224
+ logger.error("dispatch [%s]: %s", method, exc);
225
+ _write(_err(req_id, -32000, str(exc)));
226
+ }
227
+ }
228
+
229
+
230
+ # ---------------------------------------------------------------------------
231
+ # Main loop
232
+ #
233
+ # Reads one JSON message per line from stdin.
234
+ # - If the message has an "id" → it's a request, dispatch it.
235
+ # - If no "id" → it's a notification (currently only chat.cancel).
236
+ # - Empty line → EOF → extension closed, exit cleanly.
237
+ # ---------------------------------------------------------------------------
238
+
239
+ """Read stdin forever and dispatch each incoming message."""
240
+ async def _main() -> None {
241
+ loop = asyncio.get_event_loop();
242
+
243
+ # Tell the extension we are ready to receive requests.
244
+ _write(_notify("server.ready", {"service": "jac-coder"}));
245
+
246
+ while True {
247
+ try {
248
+ # run_in_executor keeps stdin reading off the async loop
249
+ # so the loop stays free to handle other tasks while waiting.
250
+ line = await loop.run_in_executor(None, sys.stdin.readline);
251
+ } except Exception as e {
252
+ logger.error("stdin read error: %s", e);
253
+ break;
254
+ }
255
+
256
+ if not line {
257
+ # Empty string = EOF = extension disconnected. Exit cleanly.
258
+ break;
259
+ }
260
+
261
+ line = line.strip();
262
+ if not line {
263
+ continue; # blank line, skip
264
+ }
265
+
266
+ try {
267
+ msg: dict = json.loads(line);
268
+ } except Exception {
269
+ _write(_err(None, -32700, "Invalid JSON"));
270
+ continue;
271
+ }
272
+
273
+ method: str = msg.get("method", "");
274
+ params: dict = msg.get("params") or {};
275
+ req_id: Any = msg.get("id");
276
+
277
+ if "id" not in msg {
278
+ # Notification (no reply needed) — only chat.cancel is handled.
279
+ if method == "chat.cancel" {
280
+ sid: str = params.get("session_id", "");
281
+ if sid {
282
+ _stop_flags[sid] = True;
283
+ # Force-stop the thread even if it's stuck in a blocking LLM call.
284
+ running_thread = _chat_threads.get(sid);
285
+ if running_thread and running_thread.is_alive() {
286
+ import ctypes;
287
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(
288
+ ctypes.c_ulong(running_thread.ident),
289
+ ctypes.py_object(KeyboardInterrupt)
290
+ );
291
+ }
292
+ }
293
+ }
294
+ continue;
295
+ }
296
+
297
+ # It's a request — schedule it on the event loop and keep reading.
298
+ asyncio.ensure_future(_dispatch(req_id, method, params));
299
+ }
300
+ }
301
+
302
+
303
+ # ---------------------------------------------------------------------------
304
+ # Entry point — runs when: jac run server.jac
305
+ # ---------------------------------------------------------------------------
306
+
307
+ with entry {
308
+ # Log warnings and above to stderr (safe — won't touch the JSON channel).
309
+ logging.basicConfig(
310
+ level=logging.WARNING,
311
+ format="[jaccoder] %(levelname)s %(name)s: %(message)s",
312
+ stream=sys.stderr
313
+ );
314
+
315
+ # Redirect stdout → stderr so accidental print() calls never corrupt the JSON pipe.
316
+ os.dup2(2, 1);
317
+ sys.stdout = sys.stderr;
318
+
319
+ # Boot the Jac agent (loads config, connects LLM, sets up the graph).
320
+ initialize("web");
321
+
322
+ # Start the async event loop — runs forever until stdin closes.
323
+ asyncio.run(_main());
324
+ }
@@ -0,0 +1,17 @@
1
+ import from jac_coder.tool.shell { bash_exec }
2
+ import from jac_coder.tool.think { think }
3
+ import from jac_coder.tool.delegation { spawn_agent }
4
+ import from jac_coder.tool.jac_docs { jac_docs }
5
+ import from jac_coder.tool.question { ask_question }
6
+ import from jac_coder.tool.web { web_fetch, web_search }
7
+ import from jac_coder.tool.scaffold { scaffold_project }
8
+ import from jac_coder.tool.search { grep_search, find_files }
9
+ import from jac_coder.tool.todo { update_todos, get_todos }
10
+ import from jac_coder.tool.jac_tools { jac_check, jac_run }
11
+ import from jac_coder.tool.validate { validate_project }
12
+ import from jac_coder.tool.git { git_status, git_diff, git_commit, git_log }
13
+ import from jac_coder.tool.checked { write_code, edit_code }
14
+ import from jac_coder.tool.guarded { write_file_guarded, edit_file_guarded, run_command }
15
+ import from jac_coder.tool.filesystem { read_file, write_file, edit_file, list_files }
16
+ import from jac_coder.tool.jac_analyzer { analyze_project, find_symbol }
17
+ import from jac_coder.tool.preview { capture_preview, evaluate_preview }