agent-dbg 0.1.2 → 0.1.3

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.2",
3
+ "version": "0.1.3",
4
4
  "description": "Node.js Debugger CLI for AI Agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  import { registerCommand } from "../cli/registry.ts";
2
2
  import { DaemonClient } from "../daemon/client.ts";
3
- import { spawnDaemon } from "../daemon/spawn.ts";
3
+ import { ensureDaemon } from "../daemon/spawn.ts";
4
4
 
5
5
  registerCommand("attach", async (args) => {
6
6
  const session = args.global.session;
@@ -12,17 +12,17 @@ registerCommand("attach", async (args) => {
12
12
  return 1;
13
13
  }
14
14
 
15
- // Check if daemon already running
15
+ // Check if daemon already running (PID-aware — stale sockets won't block)
16
16
  if (DaemonClient.isRunning(session)) {
17
17
  console.error(`Session "${session}" is already active`);
18
18
  console.error(` -> Try: agent-dbg stop --session ${session}`);
19
19
  return 1;
20
20
  }
21
21
 
22
- // Spawn daemon
22
+ // Ensure daemon is running — auto-cleans stale sockets if daemon is dead
23
23
  const timeout =
24
24
  typeof args.flags.timeout === "string" ? parseInt(args.flags.timeout, 10) : undefined;
25
- await spawnDaemon(session, { timeout });
25
+ await ensureDaemon(session, { timeout });
26
26
 
27
27
  // Send attach command
28
28
  const client = new DaemonClient(session);
@@ -1,6 +1,6 @@
1
1
  import { registerCommand } from "../cli/registry.ts";
2
2
  import { DaemonClient } from "../daemon/client.ts";
3
- import { spawnDaemon } from "../daemon/spawn.ts";
3
+ import { ensureDaemon } from "../daemon/spawn.ts";
4
4
  import { shortPath } from "../formatter/path.ts";
5
5
 
6
6
  registerCommand("launch", async (args) => {
@@ -23,10 +23,8 @@ registerCommand("launch", async (args) => {
23
23
  return 1;
24
24
  }
25
25
 
26
- // Spawn daemon if not already running (e.g. started externally for debugging)
27
- if (!DaemonClient.isRunning(session)) {
28
- await spawnDaemon(session, { timeout });
29
- }
26
+ // Ensure daemon is running auto-cleans stale sockets if daemon is dead
27
+ await ensureDaemon(session, { timeout });
30
28
 
31
29
  // Send launch command to daemon
32
30
  const client = new DaemonClient(session);
@@ -10,10 +10,11 @@ import {
10
10
  } from "node:fs";
11
11
  import type { CdpLogEntry } from "../cdp/logger.ts";
12
12
  import { registerCommand } from "../cli/registry.ts";
13
- import { getLogPath } from "../daemon/paths.ts";
14
- import { formatLogEntry } from "../formatter/logs.ts";
13
+ import type { DaemonLogEntry } from "../daemon/logger.ts";
14
+ import { getDaemonLogPath, getLogPath } from "../daemon/paths.ts";
15
+ import { formatDaemonLogEntry, formatLogEntry } from "../formatter/logs.ts";
15
16
 
16
- function parseEntries(text: string): CdpLogEntry[] {
17
+ function parseCdpEntries(text: string): CdpLogEntry[] {
17
18
  const entries: CdpLogEntry[] = [];
18
19
  for (const line of text.split("\n")) {
19
20
  if (!line.trim()) continue;
@@ -26,11 +27,28 @@ function parseEntries(text: string): CdpLogEntry[] {
26
27
  return entries;
27
28
  }
28
29
 
30
+ function parseDaemonEntries(text: string): DaemonLogEntry[] {
31
+ const entries: DaemonLogEntry[] = [];
32
+ for (const line of text.split("\n")) {
33
+ if (!line.trim()) continue;
34
+ try {
35
+ entries.push(JSON.parse(line) as DaemonLogEntry);
36
+ } catch {
37
+ // skip malformed lines
38
+ }
39
+ }
40
+ return entries;
41
+ }
42
+
29
43
  function filterByDomain(entries: CdpLogEntry[], domain: string): CdpLogEntry[] {
30
44
  return entries.filter((e) => e.method.startsWith(`${domain}.`));
31
45
  }
32
46
 
33
- function printEntries(entries: CdpLogEntry[], json: boolean): void {
47
+ function filterByLevel(entries: DaemonLogEntry[], level: string): DaemonLogEntry[] {
48
+ return entries.filter((e) => e.level === level);
49
+ }
50
+
51
+ function printCdpEntries(entries: CdpLogEntry[], json: boolean): void {
34
52
  for (const entry of entries) {
35
53
  if (json) {
36
54
  console.log(JSON.stringify(entry));
@@ -40,40 +58,58 @@ function printEntries(entries: CdpLogEntry[], json: boolean): void {
40
58
  }
41
59
  }
42
60
 
61
+ function printDaemonEntries(entries: DaemonLogEntry[], json: boolean): void {
62
+ for (const entry of entries) {
63
+ if (json) {
64
+ console.log(JSON.stringify(entry));
65
+ } else {
66
+ console.log(formatDaemonLogEntry(entry));
67
+ }
68
+ }
69
+ }
70
+
43
71
  registerCommand("logs", async (args) => {
44
72
  const session = args.global.session;
45
- const logPath = getLogPath(session);
73
+ const isDaemon = args.flags.daemon === true;
74
+ const logPath = isDaemon ? getDaemonLogPath(session) : getLogPath(session);
46
75
 
47
76
  // --clear: truncate log file
48
77
  if (args.flags.clear === true) {
49
78
  if (existsSync(logPath)) {
50
79
  writeFileSync(logPath, "");
51
- console.log("Log cleared");
80
+ console.log(`${isDaemon ? "Daemon log" : "Log"} cleared`);
52
81
  } else {
53
- console.log("No log file to clear");
82
+ console.log(`No ${isDaemon ? "daemon " : ""}log file to clear`);
54
83
  }
55
84
  return 0;
56
85
  }
57
86
 
58
87
  if (!existsSync(logPath)) {
59
- console.error(`No log file for session "${session}"`);
88
+ console.error(`No ${isDaemon ? "daemon " : ""}log file for session "${session}"`);
60
89
  console.error(" -> Try: agent-dbg launch --brk node app.js");
61
90
  return 1;
62
91
  }
63
92
 
64
93
  const isJson = args.global.json;
65
94
  const domain = typeof args.flags.domain === "string" ? args.flags.domain : undefined;
95
+ const level = typeof args.flags.level === "string" ? args.flags.level : undefined;
66
96
  const limit = typeof args.flags.limit === "string" ? parseInt(args.flags.limit, 10) : 50;
67
97
  const follow = args.flags.follow === true;
68
98
 
69
99
  // Read existing entries
70
100
  const content = readFileSync(logPath, "utf-8");
71
- let entries = parseEntries(content);
72
- if (domain) entries = filterByDomain(entries, domain);
73
101
 
74
- // Apply limit (show last N) — in follow mode, show all existing
75
- const sliced = follow ? entries : entries.slice(-limit);
76
- printEntries(sliced, isJson);
102
+ if (isDaemon) {
103
+ let entries = parseDaemonEntries(content);
104
+ if (level) entries = filterByLevel(entries, level);
105
+ const sliced = follow ? entries : entries.slice(-limit);
106
+ printDaemonEntries(sliced, isJson);
107
+ } else {
108
+ let entries = parseCdpEntries(content);
109
+ if (domain) entries = filterByDomain(entries, domain);
110
+ const sliced = follow ? entries : entries.slice(-limit);
111
+ printCdpEntries(sliced, isJson);
112
+ }
77
113
 
78
114
  if (!follow) return 0;
79
115
 
@@ -92,9 +128,15 @@ registerCommand("logs", async (args) => {
92
128
  closeSync(fd);
93
129
  offset = size;
94
130
 
95
- let newEntries = parseEntries(buf.toString("utf-8"));
96
- if (domain) newEntries = filterByDomain(newEntries, domain);
97
- printEntries(newEntries, isJson);
131
+ if (isDaemon) {
132
+ let newEntries = parseDaemonEntries(buf.toString("utf-8"));
133
+ if (level) newEntries = filterByLevel(newEntries, level);
134
+ printDaemonEntries(newEntries, isJson);
135
+ } else {
136
+ let newEntries = parseCdpEntries(buf.toString("utf-8"));
137
+ if (domain) newEntries = filterByDomain(newEntries, domain);
138
+ printCdpEntries(newEntries, isJson);
139
+ }
98
140
  } catch {
99
141
  // File may have been truncated or removed
100
142
  }
@@ -1,6 +1,6 @@
1
- import { existsSync, readdirSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
2
2
  import { type DaemonResponse, DaemonResponseSchema } from "../protocol/messages.ts";
3
- import { getSocketDir, getSocketPath } from "./paths.ts";
3
+ import { getLockPath, getSocketDir, getSocketPath } from "./paths.ts";
4
4
 
5
5
  const DEFAULT_TIMEOUT_MS = 30_000;
6
6
 
@@ -107,21 +107,43 @@ export class DaemonClient {
107
107
  });
108
108
  }
109
109
 
110
+ /**
111
+ * Check if a daemon is running for the given session.
112
+ * Uses PID liveness check (Docker-style): reads the lock file PID
113
+ * and verifies the process is actually alive via kill(pid, 0).
114
+ */
110
115
  static isRunning(session: string): boolean {
111
116
  const socketPath = getSocketPath(session);
112
117
  if (!existsSync(socketPath)) {
113
118
  return false;
114
119
  }
115
- // Try connecting to verify the daemon is actually alive
120
+
121
+ const lockPath = getLockPath(session);
122
+ if (!existsSync(lockPath)) {
123
+ // Socket exists but no lock file — stale
124
+ return false;
125
+ }
126
+
116
127
  try {
117
- // Use a sync approach: check if socket file exists
118
- // A true liveness check requires async connection, so we check the file
128
+ const pid = parseInt(readFileSync(lockPath, "utf-8"), 10);
129
+ if (Number.isNaN(pid)) return false;
130
+ process.kill(pid, 0); // signal 0 = liveness check
119
131
  return true;
120
132
  } catch {
121
133
  return false;
122
134
  }
123
135
  }
124
136
 
137
+ /**
138
+ * Remove stale socket and lock files for a session whose daemon is no longer alive.
139
+ */
140
+ static cleanStaleFiles(session: string): void {
141
+ const socketPath = getSocketPath(session);
142
+ const lockPath = getLockPath(session);
143
+ if (existsSync(socketPath)) unlinkSync(socketPath);
144
+ if (existsSync(lockPath)) unlinkSync(lockPath);
145
+ }
146
+
125
147
  static async isAlive(session: string): Promise<boolean> {
126
148
  const socketPath = getSocketPath(session);
127
149
  if (!existsSync(socketPath)) {
@@ -1,4 +1,6 @@
1
1
  import type { DaemonRequest, DaemonResponse } from "../protocol/messages.ts";
2
+ import { DaemonLogger } from "./logger.ts";
3
+ import { ensureSocketDir, getDaemonLogPath } from "./paths.ts";
2
4
  import { DaemonServer } from "./server.ts";
3
5
  import { DebugSession } from "./session.ts";
4
6
 
@@ -22,8 +24,16 @@ if (timeoutIdx !== -1) {
22
24
  }
23
25
  }
24
26
 
25
- const server = new DaemonServer(session, { idleTimeout: timeout });
26
- const debugSession = new DebugSession(session);
27
+ ensureSocketDir();
28
+ const daemonLogger = new DaemonLogger(getDaemonLogPath(session));
29
+ daemonLogger.info("daemon.start", `Daemon starting for session "${session}"`, {
30
+ pid: process.pid,
31
+ session,
32
+ timeout,
33
+ });
34
+
35
+ const server = new DaemonServer(session, { idleTimeout: timeout, logger: daemonLogger });
36
+ const debugSession = new DebugSession(session, { daemonLogger });
27
37
 
28
38
  server.onRequest(async (req: DaemonRequest): Promise<DaemonResponse> => {
29
39
  switch (req.cmd) {
@@ -0,0 +1,51 @@
1
+ import { appendFileSync, writeFileSync } from "node:fs";
2
+
3
+ export interface DaemonLogEntry {
4
+ ts: number;
5
+ level: "info" | "warn" | "error" | "debug";
6
+ event: string;
7
+ message: string;
8
+ data?: Record<string, unknown>;
9
+ }
10
+
11
+ export class DaemonLogger {
12
+ private logPath: string;
13
+
14
+ constructor(logPath: string) {
15
+ this.logPath = logPath;
16
+ writeFileSync(logPath, "");
17
+ }
18
+
19
+ info(event: string, message: string, data?: Record<string, unknown>): void {
20
+ this.write("info", event, message, data);
21
+ }
22
+
23
+ warn(event: string, message: string, data?: Record<string, unknown>): void {
24
+ this.write("warn", event, message, data);
25
+ }
26
+
27
+ error(event: string, message: string, data?: Record<string, unknown>): void {
28
+ this.write("error", event, message, data);
29
+ }
30
+
31
+ debug(event: string, message: string, data?: Record<string, unknown>): void {
32
+ this.write("debug", event, message, data);
33
+ }
34
+
35
+ clear(): void {
36
+ writeFileSync(this.logPath, "");
37
+ }
38
+
39
+ private write(
40
+ level: DaemonLogEntry["level"],
41
+ event: string,
42
+ message: string,
43
+ data?: Record<string, unknown>,
44
+ ): void {
45
+ const entry: DaemonLogEntry = { ts: Date.now(), level, event, message };
46
+ if (data !== undefined) {
47
+ entry.data = data;
48
+ }
49
+ appendFileSync(this.logPath, `${JSON.stringify(entry)}\n`);
50
+ }
51
+ }
@@ -22,6 +22,10 @@ export function getLogPath(session: string): string {
22
22
  return join(getSocketDir(), `${session}.cdp.log`);
23
23
  }
24
24
 
25
+ export function getDaemonLogPath(session: string): string {
26
+ return join(getSocketDir(), `${session}.daemon.log`);
27
+ }
28
+
25
29
  export function ensureSocketDir(): void {
26
30
  const dir = getSocketDir();
27
31
  if (!existsSync(dir)) {
@@ -4,6 +4,7 @@ import {
4
4
  DaemonRequestSchema,
5
5
  type DaemonResponse,
6
6
  } from "../protocol/messages.ts";
7
+ import type { DaemonLogger } from "./logger.ts";
7
8
  import { ensureSocketDir, getLockPath, getSocketPath } from "./paths.ts";
8
9
 
9
10
  type RequestHandler = (req: DaemonRequest) => Promise<DaemonResponse>;
@@ -16,12 +17,14 @@ export class DaemonServer {
16
17
  private listener: ReturnType<typeof Bun.listen> | null = null;
17
18
  private socketPath: string;
18
19
  private lockPath: string;
20
+ private logger: DaemonLogger | null;
19
21
 
20
- constructor(session: string, options: { idleTimeout: number }) {
22
+ constructor(session: string, options: { idleTimeout: number; logger?: DaemonLogger }) {
21
23
  this.session = session;
22
24
  this.idleTimeout = options.idleTimeout;
23
25
  this.socketPath = getSocketPath(session);
24
26
  this.lockPath = getLockPath(session);
27
+ this.logger = options.logger ?? null;
25
28
  }
26
29
 
27
30
  onRequest(handler: RequestHandler): void {
@@ -72,6 +75,7 @@ export class DaemonServer {
72
75
  },
73
76
  close() {},
74
77
  error(_socket, error) {
78
+ server.logger?.error("socket.error", error.message);
75
79
  console.error(`[daemon] socket error: ${error.message}`);
76
80
  },
77
81
  },
@@ -149,6 +153,10 @@ export class DaemonServer {
149
153
  }
150
154
  if (this.idleTimeout > 0) {
151
155
  this.idleTimer = setTimeout(() => {
156
+ this.logger?.info(
157
+ "daemon.idle",
158
+ `Idle timeout reached (${this.idleTimeout}s), shutting down`,
159
+ );
152
160
  this.stop();
153
161
  }, this.idleTimeout * 1000);
154
162
  }
@@ -6,7 +6,8 @@ import type { RemoteObject } from "../formatter/values.ts";
6
6
  import { formatValue } from "../formatter/values.ts";
7
7
  import { RefTable } from "../refs/ref-table.ts";
8
8
  import { SourceMapResolver } from "../sourcemap/resolver.ts";
9
- import { ensureSocketDir, getLogPath } from "./paths.ts";
9
+ import { DaemonLogger } from "./logger.ts";
10
+ import { ensureSocketDir, getDaemonLogPath, getLogPath } from "./paths.ts";
10
11
  import {
11
12
  addBlackbox as addBlackboxImpl,
12
13
  listBlackbox as listBlackboxImpl,
@@ -140,7 +141,7 @@ export class DebugSession {
140
141
  cdp: CdpClient | null = null;
141
142
  refs: RefTable = new RefTable();
142
143
  sourceMapResolver: SourceMapResolver = new SourceMapResolver();
143
- childProcess: Subprocess<"ignore", "pipe", "pipe"> | null = null;
144
+ childProcess: Subprocess<"ignore", "ignore", "pipe"> | null = null;
144
145
  state: "idle" | "running" | "paused" = "idle";
145
146
  pauseInfo: PauseInfo | null = null;
146
147
  pausedCallFrames: Protocol.Debugger.CallFrame[] = [];
@@ -157,11 +158,14 @@ export class DebugSession {
157
158
  launchCommand: string[] | null = null;
158
159
  launchOptions: { brk?: boolean; port?: number } | null = null;
159
160
  cdpLogger: CdpLogger;
161
+ daemonLogger: DaemonLogger;
160
162
 
161
- constructor(session: string) {
163
+ constructor(session: string, options?: { daemonLogger?: DaemonLogger }) {
162
164
  this.session = session;
163
165
  ensureSocketDir();
164
166
  this.cdpLogger = new CdpLogger(getLogPath(session));
167
+ this.daemonLogger =
168
+ options?.daemonLogger ?? new DaemonLogger(getDaemonLogPath(session));
165
169
  }
166
170
 
167
171
  // ── Session lifecycle ─────────────────────────────────────────────
@@ -192,11 +196,16 @@ export class DebugSession {
192
196
 
193
197
  const proc = Bun.spawn(spawnArgs, {
194
198
  stdin: "ignore",
195
- stdout: "pipe",
199
+ stdout: "ignore",
196
200
  stderr: "pipe",
197
201
  });
198
202
  this.childProcess = proc;
199
203
 
204
+ this.daemonLogger.info("child.spawn", `Spawned process pid=${proc.pid}`, {
205
+ pid: proc.pid,
206
+ command: spawnArgs,
207
+ });
208
+
200
209
  // Monitor child process exit in the background
201
210
  this.monitorProcessExit(proc);
202
211
 
@@ -204,6 +213,10 @@ export class DebugSession {
204
213
  const wsUrl = await this.readInspectorUrl(proc.stderr);
205
214
  this.wsUrl = wsUrl;
206
215
 
216
+ this.daemonLogger.info("inspector.detected", `Inspector URL: ${wsUrl}`, {
217
+ wsUrl,
218
+ });
219
+
207
220
  // Connect CDP
208
221
  await this.connectCdp(wsUrl);
209
222
 
@@ -741,8 +754,10 @@ export class DebugSession {
741
754
  }
742
755
 
743
756
  private async connectCdp(wsUrl: string): Promise<void> {
757
+ this.daemonLogger.debug("cdp.connecting", `Connecting to ${wsUrl}`);
744
758
  const cdp = await CdpClient.connect(wsUrl, this.cdpLogger);
745
759
  this.cdp = cdp;
760
+ this.daemonLogger.info("cdp.connected", `CDP connected to ${wsUrl}`);
746
761
 
747
762
  // Set up event handlers before enabling domains so we don't miss any events
748
763
  this.setupCdpEventHandlers(cdp);
@@ -871,9 +886,13 @@ export class DebugSession {
871
886
  });
872
887
  }
873
888
 
874
- private monitorProcessExit(proc: Subprocess<"ignore", "pipe", "pipe">): void {
889
+ private monitorProcessExit(proc: Subprocess<"ignore", "ignore", "pipe">): void {
875
890
  proc.exited
876
- .then(() => {
891
+ .then((exitCode) => {
892
+ this.daemonLogger.info("child.exit", `Process exited with code ${exitCode ?? "unknown"}`, {
893
+ pid: proc.pid,
894
+ exitCode: exitCode ?? null,
895
+ });
877
896
  // Child process has exited
878
897
  this.childProcess = null;
879
898
  if (this.cdp) {
@@ -884,7 +903,10 @@ export class DebugSession {
884
903
  this.pauseInfo = null;
885
904
  this.onProcessExit?.();
886
905
  })
887
- .catch(() => {
906
+ .catch((err) => {
907
+ this.daemonLogger.error("child.exit.error", `Error waiting for process exit: ${err}`, {
908
+ pid: proc.pid,
909
+ });
888
910
  // Error waiting for exit, treat as exited
889
911
  this.childProcess = null;
890
912
  this.state = "idle";
@@ -907,13 +929,16 @@ export class DebugSession {
907
929
  if (done) {
908
930
  break;
909
931
  }
910
- accumulated += decoder.decode(value, { stream: true });
932
+ const chunk = decoder.decode(value, { stream: true });
933
+ accumulated += chunk;
934
+ this.daemonLogger.debug("child.stderr", chunk.trimEnd());
911
935
 
912
936
  const match = INSPECTOR_URL_REGEX.exec(accumulated);
913
937
  if (match?.[1]) {
914
938
  clearTimeout(timeout);
915
- // Release the reader so the stream is not locked
916
- reader.releaseLock();
939
+ // Continue draining stderr in the background so proc.exited
940
+ // can resolve (Bun requires all piped streams to be consumed).
941
+ this.drainReader(reader);
917
942
  return match[1];
918
943
  }
919
944
  }
@@ -922,6 +947,10 @@ export class DebugSession {
922
947
  }
923
948
 
924
949
  clearTimeout(timeout);
950
+ this.daemonLogger.error("inspector.failed", "Failed to detect inspector URL", {
951
+ stderr: accumulated.slice(0, 2000),
952
+ timeoutMs: INSPECTOR_TIMEOUT_MS,
953
+ });
925
954
  throw new Error(
926
955
  `Failed to detect inspector URL within ${INSPECTOR_TIMEOUT_MS}ms. Stderr: ${accumulated.slice(0, 500)}`,
927
956
  );
@@ -955,4 +984,13 @@ export class DebugSession {
955
984
 
956
985
  return wsUrl;
957
986
  }
987
+
988
+ private drainReader(reader: { read(): Promise<{ done: boolean; value?: Uint8Array }> }): void {
989
+ const pump = (): void => {
990
+ reader.read().then(({ done }) => {
991
+ if (!done) pump();
992
+ }).catch(() => {});
993
+ };
994
+ pump();
995
+ }
958
996
  }
@@ -1,5 +1,6 @@
1
- import { existsSync } from "node:fs";
2
- import { getSocketPath } from "./paths.ts";
1
+ import { existsSync, openSync } from "node:fs";
2
+ import { DaemonClient } from "./client.ts";
3
+ import { ensureSocketDir, getDaemonLogPath, getSocketPath } from "./paths.ts";
3
4
 
4
5
  const POLL_INTERVAL_MS = 50;
5
6
  const SPAWN_TIMEOUT_MS = 5000;
@@ -30,11 +31,16 @@ export async function spawnDaemon(
30
31
  spawnArgs.push("--timeout", String(options.timeout));
31
32
  }
32
33
 
34
+ // Redirect daemon stdout/stderr to daemon log file so crashes are captured
35
+ // even before the DaemonLogger initializes inside the child process.
36
+ ensureSocketDir();
37
+ const logFd = openSync(getDaemonLogPath(session), "a");
38
+
33
39
  const proc = Bun.spawn(spawnArgs, {
34
40
  detached: true,
35
41
  stdin: "ignore",
36
- stdout: "ignore",
37
- stderr: "ignore",
42
+ stdout: logFd,
43
+ stderr: logFd,
38
44
  });
39
45
 
40
46
  // Unref so the parent process can exit
@@ -51,3 +57,20 @@ export async function spawnDaemon(
51
57
 
52
58
  throw new Error(`Daemon for session "${session}" failed to start within ${SPAWN_TIMEOUT_MS}ms`);
53
59
  }
60
+
61
+ /**
62
+ * Ensure a daemon is running for the session. If the socket exists but the
63
+ * daemon process is dead (stale), cleans up and respawns automatically.
64
+ */
65
+ export async function ensureDaemon(
66
+ session: string,
67
+ options?: { port?: number; timeout?: number },
68
+ ): Promise<void> {
69
+ if (DaemonClient.isRunning(session)) return;
70
+
71
+ // Clean up stale socket/lock files before spawning, otherwise
72
+ // spawnDaemon's poll loop would see the old socket and return immediately.
73
+ DaemonClient.cleanStaleFiles(session);
74
+
75
+ await spawnDaemon(session, options);
76
+ }
@@ -1,4 +1,5 @@
1
1
  import type { CdpLogEntry } from "../cdp/logger.ts";
2
+ import type { DaemonLogEntry } from "../daemon/logger.ts";
2
3
 
3
4
  function formatTime(ts: number): string {
4
5
  const d = new Date(ts);
@@ -108,3 +109,17 @@ export function formatLogEntry(entry: CdpLogEntry): string {
108
109
  const summary = summarizer ? summarizer(entry) : summarizeParams(entry);
109
110
  return `${time} <- ${entry.method}${summary ? ` ${summary}` : ""}`;
110
111
  }
112
+
113
+ const levelColors: Record<string, string> = {
114
+ info: "INFO ",
115
+ warn: "WARN ",
116
+ error: "ERROR",
117
+ debug: "DEBUG",
118
+ };
119
+
120
+ export function formatDaemonLogEntry(entry: DaemonLogEntry): string {
121
+ const time = `[${formatTime(entry.ts)}]`;
122
+ const level = levelColors[entry.level] ?? entry.level.toUpperCase();
123
+ const data = entry.data ? ` ${truncate(JSON.stringify(entry.data), 120)}` : "";
124
+ return `${time} ${level} ${entry.event}: ${entry.message}${data}`;
125
+ }