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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dbg",
3
- "version": "0.1.3",
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": {
@@ -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
+ });
@@ -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}`);
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
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
 
@@ -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 debugSession = new DebugSession(session, { daemonLogger });
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
- const result = await debugSession.launch(command, { brk, port });
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
- const result = await debugSession.attach(target);
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: debugSession.getStatus() };
77
+ return { ok: true, data: activeSession().getStatus() };
57
78
 
58
79
  case "state": {
59
- const stateResult = await debugSession.buildState(req.args);
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 debugSession.continue();
65
- const stateAfter = await debugSession.buildState();
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 debugSession.step(mode);
72
- const stateAfter = await debugSession.buildState();
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 debugSession.pause();
78
- const stateAfter = await debugSession.buildState();
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 debugSession.runTo(file, line);
85
- const stateAfter = await debugSession.buildState();
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 debugSession.setBreakpoint(file, line, {
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 debugSession.removeAllBreakpoints();
140
+ await activeSession().removeAllBreakpoints();
104
141
  return { ok: true, data: "all removed" };
105
142
  }
106
- await debugSession.removeBreakpoint(ref);
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: debugSession.listBreakpoints() };
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 debugSession.setLogpoint(file, line, template, {
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 debugSession.setExceptionPause(mode);
161
+ await activeSession().setExceptionPause(mode);
125
162
  return { ok: true, data: mode };
126
163
  }
127
164
 
128
165
  case "source": {
129
- const sourceResult = await debugSession.getSource(req.args);
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 = debugSession.getScripts(filter);
172
+ const scriptsResult = activeSession().getScripts(filter);
136
173
  return { ok: true, data: scriptsResult };
137
174
  }
138
175
 
139
176
  case "stack": {
140
- const stackResult = debugSession.getStack(req.args);
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 debugSession.searchInScripts(query, searchOptions);
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 = debugSession.getConsoleMessages(req.args);
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 = debugSession.getExceptions(req.args);
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 debugSession.eval(expression, evalOptions);
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 debugSession.getVars(req.args);
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 debugSession.getProps(ref, propsOptions);
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 debugSession.addBlackbox(patterns);
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: debugSession.listBlackbox() };
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 debugSession.removeBlackbox(patterns);
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 debugSession.setVariable(name, value, { frame });
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 debugSession.setReturnValue(value);
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 debugSession.hotpatch(file, source, { dryRun });
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 debugSession.toggleBreakpoint(ref);
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 debugSession.getBreakableLocations(file, startLine, endLine);
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 debugSession.restartFrame(frameRef);
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 = debugSession.sourceMapResolver.findScriptForSource(smFile);
269
+ const match = activeSession().sourceMapResolver.findScriptForSource(smFile);
233
270
  if (match) {
234
- const info = debugSession.sourceMapResolver.getInfo(match.scriptId);
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: debugSession.sourceMapResolver.getAllInfos() };
276
+ return { ok: true, data: activeSession().sourceMapResolver.getAllInfos() };
240
277
  }
241
278
 
242
279
  case "sourcemap-disable": {
243
- debugSession.sourceMapResolver.setDisabled(true);
280
+ activeSession().sourceMapResolver.setDisabled(true);
244
281
  return { ok: true, data: "disabled" };
245
282
  }
246
283
 
247
284
  case "restart": {
248
- const result = await debugSession.restart();
285
+ const result = await activeSession().restart();
249
286
  return { ok: true, data: result };
250
287
  }
251
288
 
252
289
  case "stop":
253
- await debugSession.stop();
290
+ await activeSession().stop();
291
+ dapSession = null;
254
292
  setTimeout(() => {
255
293
  server.stop();
256
294
  process.exit(0);
@@ -56,11 +56,15 @@ export class DaemonServer {
56
56
 
57
57
  const server = this;
58
58
 
59
- this.listener = Bun.listen<{ buffer: string }>({
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
- private handleMessage(
88
- socket: { write(data: string | Buffer | Uint8Array): number; end(): void },
89
- line: string,
90
- ): void {
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
- const errResponse: DaemonResponse = {
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
- const errResponse: DaemonResponse = cmd
110
- ? {
111
- ok: false,
112
- error: `Unknown command: ${cmd}`,
113
- suggestion: "-> Try: agent-dbg --help",
114
- }
115
- : {
116
- ok: false,
117
- error: "Invalid request: must have { cmd: string, args: object }",
118
- };
119
- socket.write(`${JSON.stringify(errResponse)}\n`);
120
- socket.end();
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
- const errResponse: DaemonResponse = {
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
- socket.write(`${JSON.stringify(response)}\n`);
138
- socket.end();
173
+ this.sendResponse(socket, response);
139
174
  })
140
175
  .catch((err) => {
141
- const errResponse: DaemonResponse = {
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 = options?.column !== undefined ? options.column - 1 : undefined; // user column is 1-based
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) {
@@ -176,6 +176,7 @@ export async function hotpatch(
176
176
  const setSourceParams: Protocol.Debugger.SetScriptSourceRequest = {
177
177
  scriptId,
178
178
  scriptSource: newSource,
179
+ allowTopFrameEditing: true,
179
180
  };
180
181
  if (options.dryRun) {
181
182
  setSourceParams.dryRun = true;
@@ -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.read().then(({ done }) => {
991
- if (!done) pump();
992
- }).catch(() => {});
989
+ reader
990
+ .read()
991
+ .then(({ done }) => {
992
+ if (!done) pump();
993
+ })
994
+ .catch(() => {});
993
995
  };
994
996
  pump();
995
997
  }
@@ -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 file, we're running via `bun run src/main.ts`
22
- // Otherwise we're running as a compiled binary
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
- throw new Error(`Daemon for session "${session}" failed to start within ${SPAWN_TIMEOUT_MS}ms`);
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
  /**