jeo-code 0.7.30 → 0.7.32

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 (47) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.ja.md +5 -3
  3. package/README.ko.md +5 -3
  4. package/README.md +5 -3
  5. package/README.zh.md +5 -3
  6. package/package.json +3 -3
  7. package/scripts/install.sh +19 -0
  8. package/scripts/uninstall.sh +2 -2
  9. package/src/agent/AGENTS.md +12 -0
  10. package/src/agent/ast-edit-tool.ts +123 -0
  11. package/src/agent/ast-file-scan.ts +62 -0
  12. package/src/agent/ast-grep-tool.ts +107 -0
  13. package/src/agent/ast-match.ts +241 -0
  14. package/src/agent/compaction.ts +6 -19
  15. package/src/agent/debug-session.ts +435 -0
  16. package/src/agent/debug-tool.ts +89 -0
  17. package/src/agent/engine.ts +103 -18
  18. package/src/agent/goal-tool.ts +57 -0
  19. package/src/agent/irc-tool.ts +71 -0
  20. package/src/agent/job-registry.ts +163 -0
  21. package/src/agent/job-tool.ts +106 -0
  22. package/src/agent/lsp-rename-tool.ts +108 -0
  23. package/src/agent/lsp-tool.ts +177 -0
  24. package/src/agent/memory-graph.ts +6 -1
  25. package/src/agent/memory.ts +73 -52
  26. package/src/agent/session.ts +27 -0
  27. package/src/agent/subagent-registry.ts +40 -4
  28. package/src/agent/subagent-tool.ts +27 -7
  29. package/src/agent/subagents.ts +1 -1
  30. package/src/agent/task-tool.ts +27 -5
  31. package/src/agent/tools.ts +100 -5
  32. package/src/agent/ts-language-service.ts +173 -0
  33. package/src/ai/model-manager.ts +58 -26
  34. package/src/ai/sse.ts +22 -7
  35. package/src/ai/think-tags.ts +10 -1
  36. package/src/commands/deep-interview.ts +49 -1
  37. package/src/commands/launch/agents-slash.ts +489 -0
  38. package/src/commands/launch/flags.ts +27 -9
  39. package/src/commands/launch/model-slash.ts +290 -0
  40. package/src/commands/launch/session-slash.ts +305 -0
  41. package/src/commands/launch/slash-handlers.ts +6 -0
  42. package/src/commands/launch/system-slash.ts +81 -0
  43. package/src/commands/launch/tmux.ts +32 -0
  44. package/src/commands/launch.ts +227 -750
  45. package/src/tui/app.ts +32 -12
  46. package/src/tui/components/evolution.ts +23 -3
  47. package/src/tui/components/width.ts +11 -6
@@ -0,0 +1,435 @@
1
+ /**
2
+ * In-process CDP (V8 Inspector Protocol) client driving a spawned `node --inspect-brk`
3
+ * process (gjc `debug` parity, Node.js JavaScript/TypeScript ONLY).
4
+ *
5
+ * Scope, stated honestly:
6
+ * - Targets `node` specifically. Bun's own inspector speaks the WebKit Inspector
7
+ * Protocol dialect (Bun docs: "Bun speaks the WebKit Inspector Protocol"), and
8
+ * empirical testing here showed its `Debugger.paused`/resume sequencing does NOT
9
+ * behave like Node's — so Bun is NOT supported rather than silently half-working.
10
+ * - No DAP wire protocol, no other languages, no remote/attach mode — this speaks
11
+ * the underlying V8 protocol directly against a locally-spawned `node` process.
12
+ * - One debug session at a time, matching the tool's "one active session" model.
13
+ * `launch()` terminates any previous session first.
14
+ *
15
+ * Flow: `launch()` spawns `node --inspect-brk=0 <program> <args>`, connects over
16
+ * WebSocket, enables the Debugger/Runtime domains, and resumes through Node's own
17
+ * "Break on start" pause via `Runtime.runIfWaitingForDebugger` — the session is left
18
+ * PAUSED there so the caller can set breakpoints before the first line of real user
19
+ * code runs, then call `continue`.
20
+ */
21
+ import * as path from "node:path";
22
+
23
+ interface CdpLocation {
24
+ scriptId: string;
25
+ lineNumber: number;
26
+ columnNumber?: number;
27
+ }
28
+
29
+ interface CdpScope {
30
+ type: string;
31
+ object: { objectId?: string; description?: string };
32
+ }
33
+
34
+ interface CdpCallFrame {
35
+ callFrameId: string;
36
+ functionName: string;
37
+ location: CdpLocation;
38
+ scopeChain: CdpScope[];
39
+ }
40
+
41
+ interface PausedState {
42
+ callFrames: CdpCallFrame[];
43
+ reason: string;
44
+ }
45
+
46
+ /** Outcome of waiting for the next debug-relevant event: a real pause, the process
47
+ * actually exiting, or the script's execution context finishing while Node keeps
48
+ * the (now idle) process alive waiting for the debugger — see the
49
+ * `Runtime.executionContextDestroyed` handling in `handleMessage`. */
50
+ type WaitResult = PausedState | { exited: true; code: number | null } | { finished: true };
51
+
52
+ interface BreakpointRecord {
53
+ id: string;
54
+ file: string;
55
+ line: number;
56
+ condition?: string;
57
+ }
58
+
59
+ function escapeRegex(s: string): string {
60
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
61
+ }
62
+
63
+ const CONNECT_TIMEOUT_MS = 10_000;
64
+ const PAUSE_TIMEOUT_MS = 15_000;
65
+ const MAX_OUTPUT_CHARS = 20_000;
66
+
67
+ export class DebugSession {
68
+ private proc: Bun.Subprocess<"ignore", "pipe", "pipe"> | undefined;
69
+ private ws: WebSocket | undefined;
70
+ private nextId = 1;
71
+ private pending = new Map<number, { resolve: (v: any) => void; reject: (e: any) => void }>();
72
+ private paused: PausedState | undefined;
73
+ private exitCode: number | null | undefined;
74
+ private outputBuf = "";
75
+ private scriptUrls = new Map<string, string>(); // scriptId -> url
76
+ private breakpoints = new Map<string, BreakpointRecord>(); // cdp breakpointId -> record
77
+ private pausedWaiters: Array<(p: WaitResult) => void> = [];
78
+
79
+ get isActive(): boolean {
80
+ return !!this.proc && this.exitCode === undefined;
81
+ }
82
+
83
+ get isPaused(): boolean {
84
+ return !!this.paused;
85
+ }
86
+
87
+ private appendOutput(chunk: string): void {
88
+ this.outputBuf += chunk;
89
+ if (this.outputBuf.length > MAX_OUTPUT_CHARS) {
90
+ this.outputBuf = "…[truncated]…\n" + this.outputBuf.slice(this.outputBuf.length - MAX_OUTPUT_CHARS);
91
+ }
92
+ }
93
+
94
+ output(): string {
95
+ return this.outputBuf;
96
+ }
97
+
98
+ private async drain(stream: ReadableStream<Uint8Array>): Promise<void> {
99
+ const reader = stream.getReader();
100
+ const decoder = new TextDecoder();
101
+ try {
102
+ while (true) {
103
+ const { value, done } = await reader.read();
104
+ if (done) break;
105
+ this.appendOutput(decoder.decode(value, { stream: true }));
106
+ }
107
+ } catch {
108
+ // stream torn down on terminate — nothing further to drain
109
+ }
110
+ }
111
+
112
+ private send(method: string, params: Record<string, any> = {}): Promise<any> {
113
+ if (!this.ws) return Promise.reject(new Error("No active debug session."));
114
+ const id = this.nextId++;
115
+ return new Promise((resolve, reject) => {
116
+ this.pending.set(id, { resolve, reject });
117
+ this.ws!.send(JSON.stringify({ id, method, params }));
118
+ });
119
+ }
120
+
121
+ private handleMessage(raw: string): void {
122
+ let msg: any;
123
+ try {
124
+ msg = JSON.parse(raw);
125
+ } catch {
126
+ return;
127
+ }
128
+ if (msg.id !== undefined && this.pending.has(msg.id)) {
129
+ const p = this.pending.get(msg.id)!;
130
+ this.pending.delete(msg.id);
131
+ if (msg.error) p.reject(new Error(msg.error.message ?? JSON.stringify(msg.error)));
132
+ else p.resolve(msg.result);
133
+ return;
134
+ }
135
+ if (msg.method === "Debugger.scriptParsed") {
136
+ this.scriptUrls.set(msg.params.scriptId, msg.params.url);
137
+ } else if (msg.method === "Debugger.paused") {
138
+ this.paused = { callFrames: msg.params.callFrames, reason: msg.params.reason };
139
+ const waiters = this.pausedWaiters.splice(0);
140
+ for (const w of waiters) w(this.paused);
141
+ } else if (msg.method === "Debugger.resumed") {
142
+ this.paused = undefined;
143
+ } else if (msg.method === "Debugger.breakpointResolved") {
144
+ const rec = this.breakpoints.get(msg.params.breakpointId);
145
+ if (rec) this.scriptUrls.set(msg.params.location.scriptId, this.scriptUrls.get(msg.params.location.scriptId) ?? rec.file);
146
+ } else if (msg.method === "Runtime.executionContextDestroyed") {
147
+ // Node keeps the process alive after the script finishes while a debugger is
148
+ // attached (it waits to be told to exit) — proc.exited would hang forever, so
149
+ // this event is the real "the script is done" signal. Settle any waiter with
150
+ // `finished`, then kill the now-idle process ourselves; nothing further to debug.
151
+ const waiters = this.pausedWaiters.splice(0);
152
+ for (const w of waiters) w({ finished: true });
153
+ void this.terminate();
154
+ }
155
+ }
156
+
157
+ /** Resolve on the next `Debugger.paused`, OR immediately if the process has already
158
+ * exited (so a `continue`/`step` after the last breakpoint doesn't hang forever). */
159
+ private waitForPausedOrExit(): Promise<WaitResult> {
160
+ if (this.exitCode !== undefined) return Promise.resolve({ exited: true, code: this.exitCode });
161
+ return new Promise((resolve) => {
162
+ let done = false;
163
+ const settle = (v: WaitResult) => {
164
+ if (done) return;
165
+ done = true;
166
+ resolve(v);
167
+ };
168
+ this.pausedWaiters.push(settle);
169
+ this.proc?.exited.then((code) => settle({ exited: true, code }));
170
+ setTimeout(() => settle({ exited: true, code: null }), PAUSE_TIMEOUT_MS);
171
+ });
172
+ }
173
+
174
+ async launch(program: string, args: string[], cwd: string): Promise<{ success: boolean; output: string; error?: string }> {
175
+ await this.terminate();
176
+
177
+ let proc: Bun.Subprocess<"ignore", "pipe", "pipe">;
178
+ try {
179
+ proc = Bun.spawn(["node", "--inspect-brk=0", program, ...args], { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
180
+ } catch (err: any) {
181
+ return { success: false, output: "", error: `Failed to spawn 'node': ${err.message}` };
182
+ }
183
+ this.proc = proc;
184
+ this.exitCode = undefined;
185
+ this.outputBuf = "";
186
+ this.scriptUrls.clear();
187
+ this.breakpoints.clear();
188
+ this.paused = undefined;
189
+ void proc.exited.then((code) => { if (this.proc === proc) this.exitCode = code; });
190
+ void this.drain(proc.stdout);
191
+
192
+ const url = await this.waitForInspectorUrl(proc.stderr);
193
+ if (!url) {
194
+ await this.terminate();
195
+ return { success: false, output: "", error: "node did not print an inspector WebSocket URL in time — is 'node' installed and on PATH?" };
196
+ }
197
+
198
+ try {
199
+ const ws = new WebSocket(url);
200
+ this.ws = ws;
201
+ // Guard against a STALE socket delivering buffered messages after a newer
202
+ // session has replaced `this.ws` (e.g. a killed process's socket flushing its
203
+ // last frames concurrently with the next `launch()`) — without this, a
204
+ // straggler event would mutate the singleton's live session state from a dead
205
+ // session's listener closure.
206
+ ws.addEventListener("message", (ev) => { if (this.ws === ws) this.handleMessage(ev.data as string); });
207
+ await new Promise<void>((resolve, reject) => {
208
+ const t = setTimeout(() => reject(new Error("Timed out connecting to the inspector WebSocket.")), CONNECT_TIMEOUT_MS);
209
+ ws.addEventListener("open", () => { clearTimeout(t); resolve(); }, { once: true });
210
+ ws.addEventListener("error", () => { clearTimeout(t); reject(new Error("Inspector WebSocket connection failed.")); }, { once: true });
211
+ });
212
+ await this.send("Debugger.enable");
213
+ await this.send("Runtime.enable");
214
+ await this.send("Runtime.runIfWaitingForDebugger");
215
+ } catch (err: any) {
216
+ await this.terminate();
217
+ return { success: false, output: "", error: err.message };
218
+ }
219
+
220
+ const settled = await this.waitForPausedOrExit();
221
+ if ("exited" in settled) {
222
+ return { success: false, output: "", error: `Program exited (code ${settled.code}) before an initial breakpoint could be established.` };
223
+ }
224
+ if ("finished" in settled) {
225
+ return { success: false, output: "", error: "The program finished before an initial breakpoint could be established." };
226
+ }
227
+ return {
228
+ success: true,
229
+ output: `Launched 'node ${program} ${args.join(" ")}'. Paused at program start (${settled.reason}). ` +
230
+ `Set breakpoints, then 'continue' to run.`,
231
+ };
232
+ }
233
+
234
+ private async waitForInspectorUrl(stderr: ReadableStream<Uint8Array>): Promise<string | undefined> {
235
+ const reader = stderr.getReader();
236
+ const decoder = new TextDecoder();
237
+ let buf = "";
238
+ const deadline = Date.now() + CONNECT_TIMEOUT_MS;
239
+ try {
240
+ while (Date.now() < deadline) {
241
+ const { value, done } = await Promise.race([
242
+ reader.read(),
243
+ new Promise<{ value: undefined; done: false }>((r) => setTimeout(() => r({ value: undefined, done: false }), 200)),
244
+ ]);
245
+ if (done) break;
246
+ if (value) {
247
+ const text = decoder.decode(value, { stream: true });
248
+ buf += text;
249
+ this.appendOutput(text);
250
+ const m = /ws:\/\/\S+/.exec(buf);
251
+ if (m) return m[0];
252
+ }
253
+ }
254
+ } finally {
255
+ reader.releaseLock();
256
+ // Keep draining the rest of stderr into captured output after url discovery.
257
+ void this.drain(stderr).catch(() => {});
258
+ }
259
+ return undefined;
260
+ }
261
+
262
+ async setBreakpoint(file: string, line1: number, cwd: string, condition?: string): Promise<{ success: boolean; output: string; error?: string }> {
263
+ if (!this.isActive) return { success: false, output: "", error: "No active debug session. Launch one first." };
264
+ const absPath = path.resolve(cwd, file);
265
+ try {
266
+ const res = await this.send("Debugger.setBreakpointByUrl", {
267
+ lineNumber: line1 - 1,
268
+ urlRegex: escapeRegex(absPath),
269
+ condition,
270
+ });
271
+ this.breakpoints.set(res.breakpointId, { id: res.breakpointId, file: absPath, line: line1, condition });
272
+ const resolvedNote = res.locations?.length ? "" : " (not yet resolved — script not loaded yet, will resolve once it is)";
273
+ return { success: true, output: `Breakpoint ${res.breakpointId} set at ${file}:${line1}${resolvedNote}.` };
274
+ } catch (err: any) {
275
+ return { success: false, output: "", error: err.message };
276
+ }
277
+ }
278
+
279
+ async removeBreakpoint(id: string): Promise<{ success: boolean; output: string; error?: string }> {
280
+ if (!this.isActive) return { success: false, output: "", error: "No active debug session." };
281
+ if (!this.breakpoints.has(id)) return { success: false, output: "", error: `Unknown breakpoint id '${id}'.` };
282
+ try {
283
+ await this.send("Debugger.removeBreakpoint", { breakpointId: id });
284
+ this.breakpoints.delete(id);
285
+ return { success: true, output: `Breakpoint ${id} removed.` };
286
+ } catch (err: any) {
287
+ return { success: false, output: "", error: err.message };
288
+ }
289
+ }
290
+
291
+ private async resumeLike(method: string, label: string): Promise<{ success: boolean; output: string; error?: string }> {
292
+ if (!this.isActive) return { success: false, output: "", error: "No active debug session." };
293
+ if (!this.isPaused) return { success: false, output: "", error: `Cannot '${label}': the session is not paused.` };
294
+ try {
295
+ await this.send(method);
296
+ } catch (err: any) {
297
+ return { success: false, output: "", error: err.message };
298
+ }
299
+ const settled = await this.waitForPausedOrExit();
300
+ if ("exited" in settled) {
301
+ return { success: true, output: `Program exited (code ${settled.code}).` };
302
+ }
303
+ if ("finished" in settled) {
304
+ return { success: true, output: "Program finished (execution completed)." };
305
+ }
306
+ return { success: true, output: this.formatPaused(settled) };
307
+ }
308
+
309
+ continue(): Promise<{ success: boolean; output: string; error?: string }> {
310
+ return this.resumeLike("Debugger.resume", "continue");
311
+ }
312
+
313
+ stepOver(): Promise<{ success: boolean; output: string; error?: string }> {
314
+ return this.resumeLike("Debugger.stepOver", "step_over");
315
+ }
316
+
317
+ stepIn(): Promise<{ success: boolean; output: string; error?: string }> {
318
+ return this.resumeLike("Debugger.stepInto", "step_in");
319
+ }
320
+
321
+ stepOut(): Promise<{ success: boolean; output: string; error?: string }> {
322
+ return this.resumeLike("Debugger.stepOut", "step_out");
323
+ }
324
+
325
+ async pause(): Promise<{ success: boolean; output: string; error?: string }> {
326
+ if (!this.isActive) return { success: false, output: "", error: "No active debug session." };
327
+ if (this.isPaused) return { success: true, output: this.formatPaused(this.paused!) };
328
+ try {
329
+ await this.send("Debugger.pause");
330
+ } catch (err: any) {
331
+ return { success: false, output: "", error: err.message };
332
+ }
333
+ const settled = await this.waitForPausedOrExit();
334
+ if ("exited" in settled) return { success: true, output: `Program exited (code ${settled.code}).` };
335
+ if ("finished" in settled) return { success: true, output: "Program finished (execution completed)." };
336
+ return { success: true, output: this.formatPaused(settled) };
337
+ }
338
+
339
+ async evaluate(expression: string, frameIndex = 0): Promise<{ success: boolean; output: string; error?: string }> {
340
+ if (!this.isActive) return { success: false, output: "", error: "No active debug session." };
341
+ try {
342
+ let result: any;
343
+ if (this.isPaused) {
344
+ const frame = this.paused!.callFrames[frameIndex];
345
+ if (!frame) return { success: false, output: "", error: `No stack frame at index ${frameIndex}.` };
346
+ result = await this.send("Debugger.evaluateOnCallFrame", { callFrameId: frame.callFrameId, expression });
347
+ } else {
348
+ result = await this.send("Runtime.evaluate", { expression });
349
+ }
350
+ if (result.exceptionDetails) {
351
+ return { success: false, output: "", error: this.describeRemoteObject(result.result) };
352
+ }
353
+ return { success: true, output: this.describeRemoteObject(result.result) };
354
+ } catch (err: any) {
355
+ return { success: false, output: "", error: err.message };
356
+ }
357
+ }
358
+
359
+ private describeRemoteObject(obj: any): string {
360
+ if (!obj) return "undefined";
361
+ if (obj.type === "object" && obj.subtype === "error") return obj.description ?? "Error";
362
+ if (obj.value !== undefined) return typeof obj.value === "string" ? obj.value : JSON.stringify(obj.value);
363
+ return obj.description ?? obj.className ?? obj.type ?? "undefined";
364
+ }
365
+
366
+ private formatPaused(p: PausedState): string {
367
+ const lines = [`Paused (${p.reason}):`];
368
+ p.callFrames.slice(0, 10).forEach((f, i) => lines.push(` #${i} ${this.frameLabel(f)}`));
369
+ return lines.join("\n");
370
+ }
371
+
372
+ private frameLabel(f: CdpCallFrame): string {
373
+ const url = this.scriptUrls.get(f.location.scriptId) ?? f.location.scriptId;
374
+ return `${f.functionName || "(anonymous)"} (${url}:${f.location.lineNumber + 1})`;
375
+ }
376
+
377
+ stackTrace(): { success: boolean; output: string; error?: string } {
378
+ if (!this.isActive) return { success: false, output: "", error: "No active debug session." };
379
+ if (!this.isPaused) return { success: false, output: "", error: "The session is not paused." };
380
+ return { success: true, output: this.formatPaused(this.paused!) };
381
+ }
382
+
383
+ scopes(frameIndex = 0): { success: boolean; output: string; error?: string; scopes?: { type: string; objectId?: string }[] } {
384
+ if (!this.isActive) return { success: false, output: "", error: "No active debug session." };
385
+ if (!this.isPaused) return { success: false, output: "", error: "The session is not paused." };
386
+ const frame = this.paused!.callFrames[frameIndex];
387
+ if (!frame) return { success: false, output: "", error: `No stack frame at index ${frameIndex}.` };
388
+ const scopes = frame.scopeChain.map((s) => ({ type: s.type, objectId: s.object.objectId }));
389
+ return { success: true, output: scopes.map((s) => s.objectId ? `${s.type}: ${s.objectId}` : `${s.type} (no variables)`).join("\n"), scopes };
390
+ }
391
+
392
+ async variables(objectId: string): Promise<{ success: boolean; output: string; error?: string }> {
393
+ if (!this.isActive) return { success: false, output: "", error: "No active debug session." };
394
+ try {
395
+ const res = await this.send("Runtime.getProperties", { objectId, ownProperties: true });
396
+ const lines = (res.result ?? [])
397
+ .filter((p: any) => p.enumerable !== false)
398
+ .map((p: any) => `${p.name} = ${this.describeRemoteObject(p.value)}`);
399
+ return { success: true, output: lines.length ? lines.join("\n") : "(no properties)" };
400
+ } catch (err: any) {
401
+ return { success: false, output: "", error: err.message };
402
+ }
403
+ }
404
+
405
+ threads(): { success: boolean; output: string } {
406
+ return { success: true, output: this.isActive ? "1 thread: main (node is single-threaded here)" : "No active debug session." };
407
+ }
408
+
409
+ async terminate(): Promise<{ success: boolean; output: string }> {
410
+ const wasActive = this.isActive;
411
+ try {
412
+ this.ws?.close();
413
+ } catch { /* best-effort */ }
414
+ this.ws = undefined;
415
+ if (this.proc) {
416
+ try {
417
+ this.proc.kill();
418
+ } catch { /* best-effort */ }
419
+ }
420
+ this.proc = undefined;
421
+ this.paused = undefined;
422
+ this.pending.clear();
423
+ const waiters = this.pausedWaiters.splice(0);
424
+ for (const w of waiters) w({ exited: true, code: this.exitCode ?? null });
425
+ return { success: true, output: wasActive ? "Debug session terminated." : "No active debug session." };
426
+ }
427
+ }
428
+
429
+ /** Process-wide singleton — one active debug session (gjc `debug` tool parity). */
430
+ export const debugSession = new DebugSession();
431
+
432
+ // Safety net: never leave a debugged `node` process running past the CLI's own exit.
433
+ process.on("exit", () => {
434
+ void debugSession.terminate();
435
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `debug` tool — Node.js debugging via the in-process CDP client in
3
+ * `debug-session.ts` (gjc `debug` parity, Node.js JS/TS ONLY — no Bun target, no
4
+ * DAP wire protocol, no other languages; see that file's header for the full scope
5
+ * statement). MUTATING: runs arbitrary user code as a real OS process, so it is
6
+ * excluded from read-only subagent roles the same way `bash`/`computer` are.
7
+ */
8
+ import type { ToolHandler } from "./engine";
9
+ import type { ToolResult } from "./tools";
10
+ import { debugSession } from "./debug-session";
11
+
12
+ /** One-line protocol description appended to the launch system prompt. */
13
+ export const DEBUG_TOOL_PROTOCOL_LINE =
14
+ `debug {action, ...} — Node.js debugging (one active session; 'launch' replaces any previous one). ` +
15
+ `launch {program, args?, cwd?} spawns 'node --inspect-brk' and pauses at program start; set_breakpoint ` +
16
+ `{file, line, condition?} / remove_breakpoint {id}; continue / step_over / step_in / step_out / pause resume ` +
17
+ `or step and report the next pause (or program exit); evaluate {expression, frame?}; stack_trace; scopes ` +
18
+ `{frame?} (lists 'type: objectId'); variables {object_id}; threads; output (captured stdout/stderr so far); ` +
19
+ `terminate.`;
20
+
21
+ function err(message: string): ToolResult {
22
+ return { success: false, output: "", error: message };
23
+ }
24
+
25
+ export function createDebugTool(): ToolHandler {
26
+ return async (args: Record<string, any>, cwd: string): Promise<ToolResult> => {
27
+ const action = String(args.action ?? "").trim().toLowerCase();
28
+
29
+ if (action === "launch") {
30
+ const program = typeof args.program === "string" ? args.program.trim() : "";
31
+ if (!program) return err(`debug {action:"launch"} requires a non-empty "program".`);
32
+ const scriptArgs = Array.isArray(args.args) ? args.args.map((a: unknown) => String(a)) : [];
33
+ const resolvedCwd = typeof args.cwd === "string" && args.cwd.trim() ? args.cwd : cwd;
34
+ return debugSession.launch(program, scriptArgs, resolvedCwd);
35
+ }
36
+
37
+ if (action === "set_breakpoint") {
38
+ const file = typeof args.file === "string" ? args.file.trim() : "";
39
+ if (!file) return err(`debug {action:"set_breakpoint"} requires a non-empty "file".`);
40
+ const line = typeof args.line === "number" ? args.line : parseInt(String(args.line ?? ""), 10);
41
+ if (!Number.isFinite(line) || line < 1) return err(`debug {action:"set_breakpoint"} requires a positive integer "line".`);
42
+ const condition = typeof args.condition === "string" && args.condition.trim() ? args.condition : undefined;
43
+ return debugSession.setBreakpoint(file, line, cwd, condition);
44
+ }
45
+
46
+ if (action === "remove_breakpoint") {
47
+ const id = typeof args.id === "string" ? args.id.trim() : "";
48
+ if (!id) return err(`debug {action:"remove_breakpoint"} requires a non-empty "id".`);
49
+ return debugSession.removeBreakpoint(id);
50
+ }
51
+
52
+ if (action === "continue") return debugSession.continue();
53
+ if (action === "step_over") return debugSession.stepOver();
54
+ if (action === "step_in") return debugSession.stepIn();
55
+ if (action === "step_out") return debugSession.stepOut();
56
+ if (action === "pause") return debugSession.pause();
57
+
58
+ if (action === "evaluate") {
59
+ const expression = typeof args.expression === "string" ? args.expression : "";
60
+ if (!expression.trim()) return err(`debug {action:"evaluate"} requires a non-empty "expression".`);
61
+ const frame = typeof args.frame === "number" ? args.frame : 0;
62
+ return debugSession.evaluate(expression, frame);
63
+ }
64
+
65
+ if (action === "stack_trace") return debugSession.stackTrace();
66
+
67
+ if (action === "scopes") {
68
+ const frame = typeof args.frame === "number" ? args.frame : 0;
69
+ const res = debugSession.scopes(frame);
70
+ return { success: res.success, output: res.output, error: res.error };
71
+ }
72
+
73
+ if (action === "variables") {
74
+ const objectId = typeof args.object_id === "string" ? args.object_id
75
+ : typeof args.objectId === "string" ? args.objectId : "";
76
+ if (!objectId) return err(`debug {action:"variables"} requires a non-empty "object_id" (from a prior 'scopes' call).`);
77
+ return debugSession.variables(objectId);
78
+ }
79
+
80
+ if (action === "threads") return debugSession.threads();
81
+ if (action === "output") return { success: true, output: debugSession.output() || "(no output yet)" };
82
+ if (action === "terminate") return debugSession.terminate();
83
+
84
+ return err(
85
+ `Unknown debug action '${action}'. Use launch | set_breakpoint | remove_breakpoint | continue | ` +
86
+ `step_over | step_in | step_out | pause | evaluate | stack_trace | scopes | variables | threads | output | terminate.`,
87
+ );
88
+ };
89
+ }