agent-dbg 0.1.3 → 0.1.4
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.
- package/.claude/settings.local.json +27 -1
- package/TODO.md +299 -0
- package/demo/DEMO.md +71 -0
- package/demo/order-processor.js +35 -0
- package/dist/main.js +1250 -187
- package/package.json +3 -1
- package/src/commands/attach.ts +2 -1
- package/src/commands/break-fn.ts +41 -0
- package/src/commands/launch.ts +2 -1
- package/src/daemon/client.ts +1 -1
- package/src/daemon/entry.ts +83 -45
- package/src/daemon/server.ts +67 -34
- package/src/daemon/session-breakpoints.ts +2 -1
- package/src/daemon/session-mutation.ts +1 -0
- package/src/daemon/session.ts +7 -5
- package/src/daemon/spawn.ts +21 -5
- package/src/dap/client.ts +252 -0
- package/src/dap/session.ts +1151 -0
- package/src/main.ts +1 -0
- package/src/protocol/messages.ts +12 -0
- package/tests/fixtures/dap/hello +0 -0
- package/tests/fixtures/dap/hello.c +8 -0
- package/tests/fixtures/dap/hello.dSYM/Contents/Info.plist +20 -0
- package/tests/fixtures/dap/hello.dSYM/Contents/Resources/DWARF/hello +0 -0
- package/tests/fixtures/dap/hello.dSYM/Contents/Resources/Relocations/aarch64/hello.yml +5 -0
- package/tests/fixtures/hotpatch-active-fn.js +7 -0
- package/tests/integration/daemon-logging.test.ts +8 -9
- package/tests/integration/mutation.test.ts +33 -0
- package/tests/unit/daemon-logger.test.ts +2 -2
- package/bun.lock +0 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dbg",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Node.js Debugger CLI for AI Agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "bun run src/main.ts",
|
|
11
11
|
"build": "bun build src/main.ts --outdir dist --target=bun",
|
|
12
|
+
"publish": "bun publish",
|
|
12
13
|
"test": "bun test",
|
|
13
14
|
"lint": "biome check .",
|
|
14
15
|
"format": "biome check --write .",
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
"devDependencies": {
|
|
18
19
|
"@biomejs/biome": "^2.3.14",
|
|
19
20
|
"@types/bun": "latest",
|
|
21
|
+
"@vscode/debugprotocol": "^1.68.0",
|
|
20
22
|
"devtools-protocol": "^0.0.1581282"
|
|
21
23
|
},
|
|
22
24
|
"peerDependencies": {
|
package/src/commands/attach.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { ensureDaemon } from "../daemon/spawn.ts";
|
|
|
5
5
|
registerCommand("attach", async (args) => {
|
|
6
6
|
const session = args.global.session;
|
|
7
7
|
const target = args.subcommand ?? args.positionals[0];
|
|
8
|
+
const runtime = typeof args.flags.runtime === "string" ? args.flags.runtime : undefined;
|
|
8
9
|
|
|
9
10
|
if (!target) {
|
|
10
11
|
console.error("No target specified");
|
|
@@ -26,7 +27,7 @@ registerCommand("attach", async (args) => {
|
|
|
26
27
|
|
|
27
28
|
// Send attach command
|
|
28
29
|
const client = new DaemonClient(session);
|
|
29
|
-
const response = await client.request("attach", { target });
|
|
30
|
+
const response = await client.request("attach", { target, runtime });
|
|
30
31
|
|
|
31
32
|
if (!response.ok) {
|
|
32
33
|
console.error(`${response.error}`);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { registerCommand } from "../cli/registry.ts";
|
|
2
|
+
import { DaemonClient } from "../daemon/client.ts";
|
|
3
|
+
|
|
4
|
+
registerCommand("break-fn", async (args) => {
|
|
5
|
+
const session = args.global.session;
|
|
6
|
+
|
|
7
|
+
if (!DaemonClient.isRunning(session)) {
|
|
8
|
+
console.error(`No active session "${session}"`);
|
|
9
|
+
console.error(" -> Try: agent-dbg launch --brk --runtime lldb ./program");
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const name = args.subcommand;
|
|
14
|
+
if (!name) {
|
|
15
|
+
console.error("Usage: agent-dbg break-fn <function-name>");
|
|
16
|
+
console.error(" Example: agent-dbg break-fn __assert_rtn");
|
|
17
|
+
console.error(" Example: agent-dbg break-fn 'yoga::Style::operator=='");
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const condition = typeof args.flags.condition === "string" ? args.flags.condition : undefined;
|
|
22
|
+
|
|
23
|
+
const client = new DaemonClient(session);
|
|
24
|
+
const response = await client.request("break-fn", { name, condition });
|
|
25
|
+
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
console.error(`${response.error}`);
|
|
28
|
+
if (response.suggestion) console.error(` ${response.suggestion}`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const data = response.data as { ref: string };
|
|
33
|
+
|
|
34
|
+
if (args.global.json) {
|
|
35
|
+
console.log(JSON.stringify(data, null, 2));
|
|
36
|
+
} else {
|
|
37
|
+
console.log(`${data.ref} fn:${name}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return 0;
|
|
41
|
+
});
|
package/src/commands/launch.ts
CHANGED
|
@@ -9,6 +9,7 @@ registerCommand("launch", async (args) => {
|
|
|
9
9
|
const port = typeof args.flags.port === "string" ? parseInt(args.flags.port, 10) : undefined;
|
|
10
10
|
const timeout =
|
|
11
11
|
typeof args.flags.timeout === "string" ? parseInt(args.flags.timeout, 10) : undefined;
|
|
12
|
+
const runtime = typeof args.flags.runtime === "string" ? args.flags.runtime : undefined;
|
|
12
13
|
|
|
13
14
|
// Reconstruct the full command from subcommand + positionals.
|
|
14
15
|
// The parser treats the second non-flag word as subcommand, but for launch
|
|
@@ -28,7 +29,7 @@ registerCommand("launch", async (args) => {
|
|
|
28
29
|
|
|
29
30
|
// Send launch command to daemon
|
|
30
31
|
const client = new DaemonClient(session);
|
|
31
|
-
const response = await client.request("launch", { command, brk, port });
|
|
32
|
+
const response = await client.request("launch", { command, brk, port, runtime });
|
|
32
33
|
|
|
33
34
|
if (!response.ok) {
|
|
34
35
|
console.error(`${response.error}`);
|
package/src/daemon/client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { type DaemonResponse, DaemonResponseSchema } from "../protocol/messages.ts";
|
|
3
3
|
import { getLockPath, getSocketDir, getSocketPath } from "./paths.ts";
|
|
4
4
|
|
package/src/daemon/entry.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DapSession } from "../dap/session.ts";
|
|
1
2
|
import type { DaemonRequest, DaemonResponse } from "../protocol/messages.ts";
|
|
2
3
|
import { DaemonLogger } from "./logger.ts";
|
|
3
4
|
import { ensureSocketDir, getDaemonLogPath } from "./paths.ts";
|
|
@@ -33,7 +34,17 @@ daemonLogger.info("daemon.start", `Daemon starting for session "${session}"`, {
|
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
const server = new DaemonServer(session, { idleTimeout: timeout, logger: daemonLogger });
|
|
36
|
-
const
|
|
37
|
+
const cdpSession = new DebugSession(session, { daemonLogger });
|
|
38
|
+
let dapSession: DapSession | null = null;
|
|
39
|
+
|
|
40
|
+
function isDapRuntime(runtime: string | undefined): runtime is string {
|
|
41
|
+
return runtime !== undefined && runtime !== "node";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Return the active session — DapSession if one was launched, otherwise the CDP session. */
|
|
45
|
+
function activeSession(): DebugSession | DapSession {
|
|
46
|
+
return dapSession ?? cdpSession;
|
|
47
|
+
}
|
|
37
48
|
|
|
38
49
|
server.onRequest(async (req: DaemonRequest): Promise<DaemonResponse> => {
|
|
39
50
|
switch (req.cmd) {
|
|
@@ -41,54 +52,64 @@ server.onRequest(async (req: DaemonRequest): Promise<DaemonResponse> => {
|
|
|
41
52
|
return { ok: true, data: "pong" };
|
|
42
53
|
|
|
43
54
|
case "launch": {
|
|
44
|
-
const { command, brk = true, port } = req.args;
|
|
45
|
-
|
|
55
|
+
const { command, brk = true, port, runtime } = req.args;
|
|
56
|
+
if (isDapRuntime(runtime)) {
|
|
57
|
+
dapSession = new DapSession(session, runtime);
|
|
58
|
+
const result = await dapSession.launch(command, { brk });
|
|
59
|
+
return { ok: true, data: result };
|
|
60
|
+
}
|
|
61
|
+
const result = await cdpSession.launch(command, { brk, port });
|
|
46
62
|
return { ok: true, data: result };
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
case "attach": {
|
|
50
|
-
const { target } = req.args;
|
|
51
|
-
|
|
66
|
+
const { target, runtime } = req.args;
|
|
67
|
+
if (isDapRuntime(runtime)) {
|
|
68
|
+
dapSession = new DapSession(session, runtime);
|
|
69
|
+
const result = await dapSession.attach(target);
|
|
70
|
+
return { ok: true, data: result };
|
|
71
|
+
}
|
|
72
|
+
const result = await cdpSession.attach(target);
|
|
52
73
|
return { ok: true, data: result };
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
case "status":
|
|
56
|
-
return { ok: true, data:
|
|
77
|
+
return { ok: true, data: activeSession().getStatus() };
|
|
57
78
|
|
|
58
79
|
case "state": {
|
|
59
|
-
const stateResult = await
|
|
80
|
+
const stateResult = await activeSession().buildState(req.args);
|
|
60
81
|
return { ok: true, data: stateResult };
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
case "continue": {
|
|
64
|
-
await
|
|
65
|
-
const stateAfter = await
|
|
85
|
+
await activeSession().continue();
|
|
86
|
+
const stateAfter = await activeSession().buildState();
|
|
66
87
|
return { ok: true, data: stateAfter };
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
case "step": {
|
|
70
91
|
const { mode = "over" } = req.args;
|
|
71
|
-
await
|
|
72
|
-
const stateAfter = await
|
|
92
|
+
await activeSession().step(mode);
|
|
93
|
+
const stateAfter = await activeSession().buildState();
|
|
73
94
|
return { ok: true, data: stateAfter };
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
case "pause": {
|
|
77
|
-
await
|
|
78
|
-
const stateAfter = await
|
|
98
|
+
await activeSession().pause();
|
|
99
|
+
const stateAfter = await activeSession().buildState();
|
|
79
100
|
return { ok: true, data: stateAfter };
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
case "run-to": {
|
|
83
104
|
const { file, line } = req.args;
|
|
84
|
-
await
|
|
85
|
-
const stateAfter = await
|
|
105
|
+
await activeSession().runTo(file, line);
|
|
106
|
+
const stateAfter = await activeSession().buildState();
|
|
86
107
|
return { ok: true, data: stateAfter };
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
case "break": {
|
|
90
111
|
const { file, line, condition, hitCount, urlRegex, column } = req.args;
|
|
91
|
-
const bpResult = await
|
|
112
|
+
const bpResult = await activeSession().setBreakpoint(file, line, {
|
|
92
113
|
condition,
|
|
93
114
|
hitCount,
|
|
94
115
|
urlRegex,
|
|
@@ -97,22 +118,38 @@ server.onRequest(async (req: DaemonRequest): Promise<DaemonResponse> => {
|
|
|
97
118
|
return { ok: true, data: bpResult };
|
|
98
119
|
}
|
|
99
120
|
|
|
121
|
+
case "break-fn": {
|
|
122
|
+
const session = activeSession();
|
|
123
|
+
if (!("setFunctionBreakpoint" in session)) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
error: "Function breakpoints are only supported with DAP runtimes (e.g. --runtime lldb)",
|
|
127
|
+
suggestion: "Use 'break <file>:<line>' for CDP sessions",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const { name, condition } = req.args;
|
|
131
|
+
const bpResult = await (session as DapSession).setFunctionBreakpoint(name, {
|
|
132
|
+
condition,
|
|
133
|
+
});
|
|
134
|
+
return { ok: true, data: bpResult };
|
|
135
|
+
}
|
|
136
|
+
|
|
100
137
|
case "break-rm": {
|
|
101
138
|
const { ref } = req.args;
|
|
102
139
|
if (ref === "all") {
|
|
103
|
-
await
|
|
140
|
+
await activeSession().removeAllBreakpoints();
|
|
104
141
|
return { ok: true, data: "all removed" };
|
|
105
142
|
}
|
|
106
|
-
await
|
|
143
|
+
await activeSession().removeBreakpoint(ref);
|
|
107
144
|
return { ok: true, data: "removed" };
|
|
108
145
|
}
|
|
109
146
|
|
|
110
147
|
case "break-ls":
|
|
111
|
-
return { ok: true, data:
|
|
148
|
+
return { ok: true, data: activeSession().listBreakpoints() };
|
|
112
149
|
|
|
113
150
|
case "logpoint": {
|
|
114
151
|
const { file, line, template, condition, maxEmissions } = req.args;
|
|
115
|
-
const lpResult = await
|
|
152
|
+
const lpResult = await activeSession().setLogpoint(file, line, template, {
|
|
116
153
|
condition,
|
|
117
154
|
maxEmissions,
|
|
118
155
|
});
|
|
@@ -121,136 +158,137 @@ server.onRequest(async (req: DaemonRequest): Promise<DaemonResponse> => {
|
|
|
121
158
|
|
|
122
159
|
case "catch": {
|
|
123
160
|
const { mode } = req.args;
|
|
124
|
-
await
|
|
161
|
+
await activeSession().setExceptionPause(mode);
|
|
125
162
|
return { ok: true, data: mode };
|
|
126
163
|
}
|
|
127
164
|
|
|
128
165
|
case "source": {
|
|
129
|
-
const sourceResult = await
|
|
166
|
+
const sourceResult = await activeSession().getSource(req.args);
|
|
130
167
|
return { ok: true, data: sourceResult };
|
|
131
168
|
}
|
|
132
169
|
|
|
133
170
|
case "scripts": {
|
|
134
171
|
const { filter } = req.args;
|
|
135
|
-
const scriptsResult =
|
|
172
|
+
const scriptsResult = activeSession().getScripts(filter);
|
|
136
173
|
return { ok: true, data: scriptsResult };
|
|
137
174
|
}
|
|
138
175
|
|
|
139
176
|
case "stack": {
|
|
140
|
-
const stackResult =
|
|
177
|
+
const stackResult = activeSession().getStack(req.args);
|
|
141
178
|
return { ok: true, data: stackResult };
|
|
142
179
|
}
|
|
143
180
|
|
|
144
181
|
case "search": {
|
|
145
182
|
const { query, ...searchOptions } = req.args;
|
|
146
|
-
const searchResult = await
|
|
183
|
+
const searchResult = await activeSession().searchInScripts(query, searchOptions);
|
|
147
184
|
return { ok: true, data: searchResult };
|
|
148
185
|
}
|
|
149
186
|
|
|
150
187
|
case "console": {
|
|
151
|
-
const consoleResult =
|
|
188
|
+
const consoleResult = activeSession().getConsoleMessages(req.args);
|
|
152
189
|
return { ok: true, data: consoleResult };
|
|
153
190
|
}
|
|
154
191
|
|
|
155
192
|
case "exceptions": {
|
|
156
|
-
const exceptionsResult =
|
|
193
|
+
const exceptionsResult = activeSession().getExceptions(req.args);
|
|
157
194
|
return { ok: true, data: exceptionsResult };
|
|
158
195
|
}
|
|
159
196
|
|
|
160
197
|
case "eval": {
|
|
161
198
|
const { expression, ...evalOptions } = req.args;
|
|
162
|
-
const evalResult = await
|
|
199
|
+
const evalResult = await activeSession().eval(expression, evalOptions);
|
|
163
200
|
return { ok: true, data: evalResult };
|
|
164
201
|
}
|
|
165
202
|
|
|
166
203
|
case "vars": {
|
|
167
|
-
const varsResult = await
|
|
204
|
+
const varsResult = await activeSession().getVars(req.args);
|
|
168
205
|
return { ok: true, data: varsResult };
|
|
169
206
|
}
|
|
170
207
|
|
|
171
208
|
case "props": {
|
|
172
209
|
const { ref, ...propsOptions } = req.args;
|
|
173
|
-
const propsResult = await
|
|
210
|
+
const propsResult = await activeSession().getProps(ref, propsOptions);
|
|
174
211
|
return { ok: true, data: propsResult };
|
|
175
212
|
}
|
|
176
213
|
|
|
177
214
|
case "blackbox": {
|
|
178
215
|
const { patterns } = req.args;
|
|
179
|
-
const result = await
|
|
216
|
+
const result = await activeSession().addBlackbox(patterns);
|
|
180
217
|
return { ok: true, data: result };
|
|
181
218
|
}
|
|
182
219
|
|
|
183
220
|
case "blackbox-ls": {
|
|
184
|
-
return { ok: true, data:
|
|
221
|
+
return { ok: true, data: activeSession().listBlackbox() };
|
|
185
222
|
}
|
|
186
223
|
|
|
187
224
|
case "blackbox-rm": {
|
|
188
225
|
const { patterns } = req.args;
|
|
189
|
-
const result = await
|
|
226
|
+
const result = await activeSession().removeBlackbox(patterns);
|
|
190
227
|
return { ok: true, data: result };
|
|
191
228
|
}
|
|
192
229
|
|
|
193
230
|
case "set": {
|
|
194
231
|
const { name, value, frame } = req.args;
|
|
195
|
-
const result = await
|
|
232
|
+
const result = await activeSession().setVariable(name, value, { frame });
|
|
196
233
|
return { ok: true, data: result };
|
|
197
234
|
}
|
|
198
235
|
|
|
199
236
|
case "set-return": {
|
|
200
237
|
const { value } = req.args;
|
|
201
|
-
const result = await
|
|
238
|
+
const result = await activeSession().setReturnValue(value);
|
|
202
239
|
return { ok: true, data: result };
|
|
203
240
|
}
|
|
204
241
|
|
|
205
242
|
case "hotpatch": {
|
|
206
243
|
const { file, source, dryRun } = req.args;
|
|
207
|
-
const result = await
|
|
244
|
+
const result = await activeSession().hotpatch(file, source, { dryRun });
|
|
208
245
|
return { ok: true, data: result };
|
|
209
246
|
}
|
|
210
247
|
|
|
211
248
|
case "break-toggle": {
|
|
212
249
|
const { ref } = req.args;
|
|
213
|
-
const toggleResult = await
|
|
250
|
+
const toggleResult = await activeSession().toggleBreakpoint(ref);
|
|
214
251
|
return { ok: true, data: toggleResult };
|
|
215
252
|
}
|
|
216
253
|
|
|
217
254
|
case "breakable": {
|
|
218
255
|
const { file, startLine, endLine } = req.args;
|
|
219
|
-
const breakableResult = await
|
|
256
|
+
const breakableResult = await activeSession().getBreakableLocations(file, startLine, endLine);
|
|
220
257
|
return { ok: true, data: breakableResult };
|
|
221
258
|
}
|
|
222
259
|
|
|
223
260
|
case "restart-frame": {
|
|
224
261
|
const { frameRef } = req.args;
|
|
225
|
-
const restartResult = await
|
|
262
|
+
const restartResult = await activeSession().restartFrame(frameRef);
|
|
226
263
|
return { ok: true, data: restartResult };
|
|
227
264
|
}
|
|
228
265
|
|
|
229
266
|
case "sourcemap": {
|
|
230
267
|
const { file: smFile } = req.args;
|
|
231
268
|
if (smFile) {
|
|
232
|
-
const match =
|
|
269
|
+
const match = activeSession().sourceMapResolver.findScriptForSource(smFile);
|
|
233
270
|
if (match) {
|
|
234
|
-
const info =
|
|
271
|
+
const info = activeSession().sourceMapResolver.getInfo(match.scriptId);
|
|
235
272
|
return { ok: true, data: info ? [info] : [] };
|
|
236
273
|
}
|
|
237
274
|
return { ok: true, data: [] };
|
|
238
275
|
}
|
|
239
|
-
return { ok: true, data:
|
|
276
|
+
return { ok: true, data: activeSession().sourceMapResolver.getAllInfos() };
|
|
240
277
|
}
|
|
241
278
|
|
|
242
279
|
case "sourcemap-disable": {
|
|
243
|
-
|
|
280
|
+
activeSession().sourceMapResolver.setDisabled(true);
|
|
244
281
|
return { ok: true, data: "disabled" };
|
|
245
282
|
}
|
|
246
283
|
|
|
247
284
|
case "restart": {
|
|
248
|
-
const result = await
|
|
285
|
+
const result = await activeSession().restart();
|
|
249
286
|
return { ok: true, data: result };
|
|
250
287
|
}
|
|
251
288
|
|
|
252
289
|
case "stop":
|
|
253
|
-
await
|
|
290
|
+
await activeSession().stop();
|
|
291
|
+
dapSession = null;
|
|
254
292
|
setTimeout(() => {
|
|
255
293
|
server.stop();
|
|
256
294
|
process.exit(0);
|
package/src/daemon/server.ts
CHANGED
|
@@ -56,11 +56,15 @@ export class DaemonServer {
|
|
|
56
56
|
|
|
57
57
|
const server = this;
|
|
58
58
|
|
|
59
|
-
this.listener = Bun.listen<{
|
|
59
|
+
this.listener = Bun.listen<{
|
|
60
|
+
buffer: string;
|
|
61
|
+
pendingWrite: Buffer | null;
|
|
62
|
+
pendingOffset: number;
|
|
63
|
+
}>({
|
|
60
64
|
unix: this.socketPath,
|
|
61
65
|
socket: {
|
|
62
66
|
open(socket) {
|
|
63
|
-
socket.data = { buffer: "" };
|
|
67
|
+
socket.data = { buffer: "", pendingWrite: null, pendingOffset: 0 };
|
|
64
68
|
server.resetIdleTimer();
|
|
65
69
|
},
|
|
66
70
|
data(socket, data) {
|
|
@@ -73,6 +77,10 @@ export class DaemonServer {
|
|
|
73
77
|
|
|
74
78
|
server.handleMessage(socket, line);
|
|
75
79
|
},
|
|
80
|
+
drain(socket) {
|
|
81
|
+
// Continue writing any pending data
|
|
82
|
+
server.flushPending(socket);
|
|
83
|
+
},
|
|
76
84
|
close() {},
|
|
77
85
|
error(_socket, error) {
|
|
78
86
|
server.logger?.error("socket.error", error.message);
|
|
@@ -84,20 +92,49 @@ export class DaemonServer {
|
|
|
84
92
|
this.resetIdleTimer();
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun socket type
|
|
96
|
+
private flushPending(socket: any): void {
|
|
97
|
+
const data = socket.data as {
|
|
98
|
+
pendingWrite: Buffer | null;
|
|
99
|
+
pendingOffset: number;
|
|
100
|
+
};
|
|
101
|
+
if (!data.pendingWrite) return;
|
|
102
|
+
|
|
103
|
+
while (data.pendingOffset < data.pendingWrite.length) {
|
|
104
|
+
const written = socket.write(data.pendingWrite.subarray(data.pendingOffset));
|
|
105
|
+
if (written === 0) {
|
|
106
|
+
// Still full — drain will call us again
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
data.pendingOffset += written;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// All data flushed
|
|
113
|
+
data.pendingWrite = null;
|
|
114
|
+
data.pendingOffset = 0;
|
|
115
|
+
socket.end();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun socket type
|
|
119
|
+
private sendResponse(socket: any, response: DaemonResponse): void {
|
|
120
|
+
const payload = Buffer.from(`${JSON.stringify(response)}\n`);
|
|
121
|
+
const written = socket.write(payload);
|
|
122
|
+
if (written < payload.length) {
|
|
123
|
+
// Partial write — store remainder for drain
|
|
124
|
+
socket.data.pendingWrite = payload;
|
|
125
|
+
socket.data.pendingOffset = written;
|
|
126
|
+
} else {
|
|
127
|
+
socket.end();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun socket type
|
|
132
|
+
private handleMessage(socket: any, line: string): void {
|
|
91
133
|
let json: unknown;
|
|
92
134
|
try {
|
|
93
135
|
json = JSON.parse(line);
|
|
94
136
|
} catch {
|
|
95
|
-
|
|
96
|
-
ok: false,
|
|
97
|
-
error: "Invalid JSON",
|
|
98
|
-
};
|
|
99
|
-
socket.write(`${JSON.stringify(errResponse)}\n`);
|
|
100
|
-
socket.end();
|
|
137
|
+
this.sendResponse(socket, { ok: false, error: "Invalid JSON" });
|
|
101
138
|
return;
|
|
102
139
|
}
|
|
103
140
|
|
|
@@ -106,44 +143,40 @@ export class DaemonServer {
|
|
|
106
143
|
const obj = json as Record<string, unknown> | null;
|
|
107
144
|
const cmd =
|
|
108
145
|
obj && typeof obj === "object" && typeof obj.cmd === "string" ? obj.cmd : undefined;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
146
|
+
this.sendResponse(
|
|
147
|
+
socket,
|
|
148
|
+
cmd
|
|
149
|
+
? {
|
|
150
|
+
ok: false,
|
|
151
|
+
error: `Unknown command: ${cmd}`,
|
|
152
|
+
suggestion: "-> Try: agent-dbg --help",
|
|
153
|
+
}
|
|
154
|
+
: {
|
|
155
|
+
ok: false,
|
|
156
|
+
error: "Invalid request: must have { cmd: string, args: object }",
|
|
157
|
+
},
|
|
158
|
+
);
|
|
121
159
|
return;
|
|
122
160
|
}
|
|
123
161
|
const request: DaemonRequest = parsed.data;
|
|
124
162
|
|
|
125
163
|
if (!this.handler) {
|
|
126
|
-
|
|
164
|
+
this.sendResponse(socket, {
|
|
127
165
|
ok: false,
|
|
128
166
|
error: "No request handler registered",
|
|
129
|
-
};
|
|
130
|
-
socket.write(`${JSON.stringify(errResponse)}\n`);
|
|
131
|
-
socket.end();
|
|
167
|
+
});
|
|
132
168
|
return;
|
|
133
169
|
}
|
|
134
170
|
|
|
135
171
|
this.handler(request)
|
|
136
172
|
.then((response) => {
|
|
137
|
-
|
|
138
|
-
socket.end();
|
|
173
|
+
this.sendResponse(socket, response);
|
|
139
174
|
})
|
|
140
175
|
.catch((err) => {
|
|
141
|
-
|
|
176
|
+
this.sendResponse(socket, {
|
|
142
177
|
ok: false,
|
|
143
178
|
error: err instanceof Error ? err.message : String(err),
|
|
144
|
-
};
|
|
145
|
-
socket.write(`${JSON.stringify(errResponse)}\n`);
|
|
146
|
-
socket.end();
|
|
179
|
+
});
|
|
147
180
|
});
|
|
148
181
|
}
|
|
149
182
|
|
|
@@ -17,7 +17,8 @@ export async function setBreakpoint(
|
|
|
17
17
|
let originalFile: string | null = null;
|
|
18
18
|
let originalLine: number | null = null;
|
|
19
19
|
let actualLine = line;
|
|
20
|
-
let actualColumn: number | undefined =
|
|
20
|
+
let actualColumn: number | undefined =
|
|
21
|
+
options?.column !== undefined ? options.column - 1 : undefined; // user column is 1-based
|
|
21
22
|
let actualFile = file;
|
|
22
23
|
|
|
23
24
|
if (!options?.urlRegex) {
|
package/src/daemon/session.ts
CHANGED
|
@@ -164,8 +164,7 @@ export class DebugSession {
|
|
|
164
164
|
this.session = session;
|
|
165
165
|
ensureSocketDir();
|
|
166
166
|
this.cdpLogger = new CdpLogger(getLogPath(session));
|
|
167
|
-
this.daemonLogger =
|
|
168
|
-
options?.daemonLogger ?? new DaemonLogger(getDaemonLogPath(session));
|
|
167
|
+
this.daemonLogger = options?.daemonLogger ?? new DaemonLogger(getDaemonLogPath(session));
|
|
169
168
|
}
|
|
170
169
|
|
|
171
170
|
// ── Session lifecycle ─────────────────────────────────────────────
|
|
@@ -987,9 +986,12 @@ export class DebugSession {
|
|
|
987
986
|
|
|
988
987
|
private drainReader(reader: { read(): Promise<{ done: boolean; value?: Uint8Array }> }): void {
|
|
989
988
|
const pump = (): void => {
|
|
990
|
-
reader
|
|
991
|
-
|
|
992
|
-
|
|
989
|
+
reader
|
|
990
|
+
.read()
|
|
991
|
+
.then(({ done }) => {
|
|
992
|
+
if (!done) pump();
|
|
993
|
+
})
|
|
994
|
+
.catch(() => {});
|
|
993
995
|
};
|
|
994
996
|
pump();
|
|
995
997
|
}
|
package/src/daemon/spawn.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, openSync } from "node:fs";
|
|
1
|
+
import { existsSync, openSync, readFileSync } from "node:fs";
|
|
2
2
|
import { DaemonClient } from "./client.ts";
|
|
3
3
|
import { ensureSocketDir, getDaemonLogPath, getSocketPath } from "./paths.ts";
|
|
4
4
|
|
|
@@ -18,9 +18,9 @@ export async function spawnDaemon(
|
|
|
18
18
|
const execPath = process.execPath;
|
|
19
19
|
const scriptPath = process.argv[1];
|
|
20
20
|
|
|
21
|
-
// If argv[1] exists and is a .ts
|
|
22
|
-
// Otherwise we're
|
|
23
|
-
if (scriptPath && scriptPath.endsWith(".ts")) {
|
|
21
|
+
// If argv[1] exists and is a script file (.ts or .js), we're running via
|
|
22
|
+
// `bun run src/main.ts` or `bun dist/main.js`. Otherwise we're a compiled binary.
|
|
23
|
+
if (scriptPath && (scriptPath.endsWith(".ts") || scriptPath.endsWith(".js"))) {
|
|
24
24
|
spawnArgs.push(execPath, "run", scriptPath);
|
|
25
25
|
} else {
|
|
26
26
|
spawnArgs.push(execPath);
|
|
@@ -55,7 +55,23 @@ export async function spawnDaemon(
|
|
|
55
55
|
await Bun.sleep(POLL_INTERVAL_MS);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
// Read daemon log to surface the actual error
|
|
59
|
+
const logPath = getDaemonLogPath(session);
|
|
60
|
+
let logTail = "";
|
|
61
|
+
try {
|
|
62
|
+
const log = readFileSync(logPath, "utf-8");
|
|
63
|
+
const lines = log.trimEnd().split("\n");
|
|
64
|
+
logTail = lines.slice(-20).join("\n");
|
|
65
|
+
} catch {}
|
|
66
|
+
|
|
67
|
+
const details = [
|
|
68
|
+
`Daemon for session "${session}" failed to start within ${SPAWN_TIMEOUT_MS}ms`,
|
|
69
|
+
`Spawn command: ${spawnArgs.join(" ")}`,
|
|
70
|
+
`Socket path: ${socketPath}`,
|
|
71
|
+
logTail ? `Daemon log (last 20 lines):\n${logTail}` : `No daemon log at ${logPath}`,
|
|
72
|
+
].join("\n");
|
|
73
|
+
|
|
74
|
+
throw new Error(details);
|
|
59
75
|
}
|
|
60
76
|
|
|
61
77
|
/**
|