@browserbridge/bbx 1.0.1 → 1.2.0

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 (70) hide show
  1. package/README.md +4 -4
  2. package/package.json +11 -13
  3. package/packages/agent-client/src/cli-helpers.js +33 -0
  4. package/packages/agent-client/src/cli.js +122 -45
  5. package/packages/agent-client/src/client.js +134 -8
  6. package/packages/agent-client/src/command-registry.js +4 -1
  7. package/packages/agent-client/src/detect.js +159 -48
  8. package/packages/agent-client/src/install.js +24 -1
  9. package/packages/agent-client/src/mcp-config.js +29 -10
  10. package/packages/agent-client/src/setup-status.js +12 -4
  11. package/packages/mcp-server/src/bin.js +57 -5
  12. package/packages/mcp-server/src/handlers-capture.js +279 -0
  13. package/packages/mcp-server/src/handlers-dom.js +196 -0
  14. package/packages/mcp-server/src/handlers-navigation.js +79 -0
  15. package/packages/mcp-server/src/handlers-page.js +365 -0
  16. package/packages/mcp-server/src/handlers-utils.js +296 -0
  17. package/packages/mcp-server/src/handlers.js +63 -1159
  18. package/packages/mcp-server/src/server.js +13 -3
  19. package/packages/native-host/bin/bridge-daemon.js +34 -4
  20. package/packages/native-host/bin/install-manifest.js +32 -2
  21. package/packages/native-host/bin/postinstall.js +16 -0
  22. package/packages/native-host/src/config.js +131 -6
  23. package/packages/native-host/src/daemon-logger.js +157 -0
  24. package/packages/native-host/src/daemon-process.js +422 -0
  25. package/packages/native-host/src/daemon.js +322 -77
  26. package/packages/native-host/src/framing.js +131 -11
  27. package/packages/native-host/src/install-manifest.js +121 -7
  28. package/packages/native-host/src/native-host.js +110 -73
  29. package/packages/protocol/src/capabilities.js +4 -0
  30. package/packages/protocol/src/defaults.js +1 -0
  31. package/packages/protocol/src/errors.js +4 -0
  32. package/packages/protocol/src/payload-cost.js +19 -6
  33. package/packages/protocol/src/protocol.js +143 -7
  34. package/packages/protocol/src/registry.js +13 -0
  35. package/packages/protocol/src/summary.js +18 -10
  36. package/packages/protocol/src/types.js +28 -3
  37. package/skills/browser-bridge/SKILL.md +2 -1
  38. package/skills/browser-bridge/references/interaction.md +1 -0
  39. package/skills/browser-bridge/references/protocol.md +2 -1
  40. package/CHANGELOG.md +0 -55
  41. package/assets/banner.jpg +0 -0
  42. package/assets/logo.png +0 -0
  43. package/assets/logo.svg +0 -65
  44. package/docs/api-reference.md +0 -157
  45. package/docs/cli-guide.md +0 -128
  46. package/docs/index.md +0 -25
  47. package/docs/manual-setup.md +0 -140
  48. package/docs/mcp-vs-cli.md +0 -258
  49. package/docs/publishing.md +0 -112
  50. package/docs/quickstart.md +0 -104
  51. package/docs/troubleshooting.md +0 -59
  52. package/docs/unpacked-extension.md +0 -72
  53. package/docs/usage-scenarios.md +0 -136
  54. package/manifest.json +0 -38
  55. package/packages/extension/assets/icon-128.png +0 -0
  56. package/packages/extension/assets/icon-16.png +0 -0
  57. package/packages/extension/assets/icon-32.png +0 -0
  58. package/packages/extension/assets/icon-48.png +0 -0
  59. package/packages/extension/src/background-helpers.js +0 -474
  60. package/packages/extension/src/background-routing.js +0 -89
  61. package/packages/extension/src/background.js +0 -3490
  62. package/packages/extension/src/content-script-helpers.js +0 -282
  63. package/packages/extension/src/content-script.js +0 -2043
  64. package/packages/extension/src/debugger-coordinator.js +0 -188
  65. package/packages/extension/src/sidepanel-helpers.js +0 -104
  66. package/packages/extension/ui/popup.html +0 -35
  67. package/packages/extension/ui/popup.js +0 -298
  68. package/packages/extension/ui/sidepanel.html +0 -102
  69. package/packages/extension/ui/sidepanel.js +0 -1771
  70. package/packages/extension/ui/ui.css +0 -1160
@@ -37,6 +37,7 @@ import {
37
37
  DEFAULT_WAIT_TIMEOUT_MS,
38
38
  getMethodsByMaxComplexity,
39
39
  } from '../../protocol/src/index.js';
40
+ import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
40
41
 
41
42
  export const BUDGET_PRESET_DESCRIPTION = `Budget preset: "quick", "normal", or "deep" (defaults: query ${BUDGET_PRESETS.normal.maxNodes} nodes / depth ${BUDGET_PRESETS.normal.maxDepth} / text ${BUDGET_PRESETS.normal.textBudget}). Numeric fields override the preset when both are provided.`;
42
43
  export const TAB_ID_DESCRIPTION =
@@ -369,7 +370,7 @@ export function createBridgeMcpServer() {
369
370
  {
370
371
  title: 'Browser Input',
371
372
  description:
372
- 'Simulate user input: click, focus, type, press keys, set checked, select options, hover, drag, or scroll into view. Reuse elementRef from prior queries.',
373
+ 'Simulate user input: click, focus, type, press keys, CDP key events, set checked, select options, hover, drag, or scroll into view. Reuse elementRef from prior queries.',
373
374
  inputSchema: {
374
375
  action: z
375
376
  .enum([
@@ -377,6 +378,7 @@ export function createBridgeMcpServer() {
377
378
  'focus',
378
379
  'type',
379
380
  'press_key',
381
+ 'cdp_press_key',
380
382
  'set_checked',
381
383
  'select_option',
382
384
  'hover',
@@ -399,10 +401,17 @@ export function createBridgeMcpServer() {
399
401
  .optional()
400
402
  .describe('Mouse button for click (default: left)'),
401
403
  clickCount: z.number().optional().describe('Click count (1=single, 2=double)'),
402
- text: z.string().optional().describe('Text to type (for type action)'),
404
+ text: z.string().max(100000).optional().describe('Text to type (for type action)'),
403
405
  clear: z.boolean().optional().describe('Clear field before typing (default: false)'),
404
406
  submit: z.boolean().optional().describe('Press Enter after typing (default: false)'),
405
- key: z.string().optional().describe('Key to press (e.g., "Enter", "Tab", "ArrowDown")'),
407
+ key: z
408
+ .string()
409
+ .optional()
410
+ .describe('Key to press (e.g., "Escape", "Enter", "Tab", "ArrowDown")'),
411
+ code: z
412
+ .string()
413
+ .optional()
414
+ .describe('Optional physical key code for cdp_press_key (e.g., "Escape", "KeyA")'),
406
415
  modifiers: z
407
416
  .array(z.enum(['Alt', 'Control', 'Meta', 'Shift']))
408
417
  .optional()
@@ -652,6 +661,7 @@ export function createBridgeMcpServer() {
652
661
  * @returns {Promise<void>}
653
662
  */
654
663
  export async function startBridgeMcpServer() {
664
+ applyWindowsTcpTransportDefaults();
655
665
  const server = createBridgeMcpServer();
656
666
  const transport = new StdioServerTransport();
657
667
  await server.connect(transport);
@@ -1,12 +1,41 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-check
3
3
  import { BridgeDaemon } from '../src/daemon.js';
4
- import { getSocketPath } from '../src/config.js';
4
+ import {
5
+ applyWindowsTcpTransportDefaults,
6
+ formatBridgeTransport,
7
+ getBridgeTransport,
8
+ } from '../src/config.js';
9
+ import { DaemonLogger } from '../src/daemon-logger.js';
10
+ import { clearDaemonPidFile, writeDaemonPidFile } from '../src/daemon-process.js';
5
11
 
6
- const daemon = new BridgeDaemon({ socketPath: getSocketPath() });
7
- await daemon.start();
12
+ applyWindowsTcpTransportDefaults();
13
+ const transport = getBridgeTransport();
14
+ const daemon = new BridgeDaemon({ transport, logger: new DaemonLogger() });
8
15
 
9
- process.stdout.write(`Browser Bridge daemon listening on ${getSocketPath()}\n`);
16
+ /**
17
+ * @param {unknown} error
18
+ * @returns {boolean}
19
+ */
20
+ function isExistingDaemonError(error) {
21
+ return (
22
+ error instanceof Error && error.message.startsWith('Another daemon is already running on ')
23
+ );
24
+ }
25
+
26
+ try {
27
+ await daemon.start();
28
+ await writeDaemonPidFile(process.pid);
29
+ } catch (error) {
30
+ if (isExistingDaemonError(error)) {
31
+ process.stdout.write(`${error.message}\n`);
32
+ process.exit(0);
33
+ }
34
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
35
+ process.exit(1);
36
+ }
37
+
38
+ process.stdout.write(`Browser Bridge daemon listening on ${formatBridgeTransport(transport)}\n`);
10
39
 
11
40
  let shuttingDown = false;
12
41
 
@@ -21,6 +50,7 @@ async function shutdown() {
21
50
  shuttingDown = true;
22
51
  try {
23
52
  await daemon.stop();
53
+ await clearDaemonPidFile({ pid: process.pid });
24
54
  process.exit(0);
25
55
  } catch (error) {
26
56
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
@@ -3,22 +3,32 @@
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
- import { installNativeManifest, parseExtensionId } from '../src/install-manifest.js';
6
+ import { restartBridgeDaemonIfRunning } from '../src/daemon-process.js';
7
+ import {
8
+ installNativeManifest,
9
+ parseExtensionId,
10
+ uninstallNativeManifest,
11
+ } from '../src/install-manifest.js';
7
12
  import { SUPPORTED_BROWSERS } from '../src/config.js';
8
13
 
9
14
  /** @typedef {import('../src/config.js').SupportedBrowser} SupportedBrowser */
10
15
 
16
+ const USAGE = 'Usage: bbx-install [<extension-id>] [--browser <browser>] [--all] [--uninstall]\n';
17
+
11
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
19
  const repoRoot = path.resolve(__dirname, '../../..');
13
20
 
14
21
  const args = process.argv.slice(2);
15
22
  let extensionIdArg = /** @type {string | undefined} */ (undefined);
16
23
  let installAll = false;
24
+ let uninstall = false;
17
25
  const browsers = /** @type {SupportedBrowser[]} */ ([]);
18
26
 
19
27
  for (let i = 0; i < args.length; i++) {
20
28
  if (args[i] === '--all') {
21
29
  installAll = true;
30
+ } else if (args[i] === '--uninstall') {
31
+ uninstall = true;
22
32
  } else if (args[i] === '--browser' && args[i + 1]) {
23
33
  const candidate = args[i + 1];
24
34
  if (!SUPPORTED_BROWSERS.includes(/** @type {SupportedBrowser} */ (candidate))) {
@@ -34,6 +44,11 @@ for (let i = 0; i < args.length; i++) {
34
44
  }
35
45
  }
36
46
 
47
+ if (uninstall && extensionIdArg) {
48
+ process.stderr.write(USAGE);
49
+ process.exit(1);
50
+ }
51
+
37
52
  if (extensionIdArg && !parseExtensionId(extensionIdArg)) {
38
53
  process.stderr.write(
39
54
  `Invalid extension ID: ${extensionIdArg}\n` +
@@ -48,10 +63,25 @@ const targets = installAll
48
63
  ? browsers
49
64
  : [/** @type {SupportedBrowser} */ ('chrome')];
50
65
 
51
- for (const target of targets) {
66
+ for (const [index, target] of targets.entries()) {
67
+ if (uninstall) {
68
+ await uninstallNativeManifest({
69
+ browser: target,
70
+ removeBridgeDir: index === targets.length - 1,
71
+ });
72
+ continue;
73
+ }
74
+
52
75
  await installNativeManifest({
53
76
  repoRoot,
54
77
  extensionIdArg,
55
78
  browser: target,
56
79
  });
57
80
  }
81
+
82
+ if (!uninstall) {
83
+ const restartResult = await restartBridgeDaemonIfRunning();
84
+ if (restartResult) {
85
+ process.stdout.write('Restarted Browser Bridge daemon to use the updated install.\n');
86
+ }
87
+ }
@@ -10,6 +10,7 @@
10
10
  import path from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
 
13
+ import { restartBridgeDaemonIfRunning } from '../src/daemon-process.js';
13
14
  import { installNativeManifest } from '../src/install-manifest.js';
14
15
 
15
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -24,4 +25,19 @@ try {
24
25
  process.stderr.write(
25
26
  `Browser Bridge: native host auto-install skipped (${message}).\nRun \`bbx install\` manually if needed.\n`
26
27
  );
28
+ process.exit(0);
29
+ }
30
+
31
+ try {
32
+ const restartResult = await restartBridgeDaemonIfRunning();
33
+ if (restartResult) {
34
+ process.stdout.write(
35
+ 'Browser Bridge: restarted the local daemon to use the updated install.\n'
36
+ );
37
+ }
38
+ } catch (err) {
39
+ const message = err instanceof Error ? err.message : String(err);
40
+ process.stderr.write(
41
+ `Browser Bridge: native host installed, but daemon restart failed (${message}).\nRun \`bbx restart\` if needed.\n`
42
+ );
27
43
  }
@@ -5,6 +5,10 @@ import path from 'node:path';
5
5
 
6
6
  export const APP_NAME = 'com.browserbridge.browser_bridge';
7
7
  export const BRIDGE_HOME_ENV = 'BROWSER_BRIDGE_HOME';
8
+ export const BRIDGE_TCP_PORT_ENV = 'BBX_TCP_PORT';
9
+ export const DEFAULT_WINDOWS_TCP_PORT = 9223;
10
+
11
+ /** @typedef {{ type: 'socket', socketPath: string, label: string } | { type: 'tcp', host: string, port: number, label: string }} BridgeTransport */
8
12
 
9
13
  /**
10
14
  * The official Chrome Web Store extension ID used when callers do not provide
@@ -15,10 +19,11 @@ export const BRIDGE_HOME_ENV = 'BROWSER_BRIDGE_HOME';
15
19
  export const PUBLISHED_EXTENSION_ID = 'jjjkmmcdkpcgamlopogicbnnhdgebhie';
16
20
 
17
21
  /**
22
+ * @param {NodeJS.ProcessEnv} [env=process.env]
18
23
  * @returns {string}
19
24
  */
20
- export function getBridgeDir() {
21
- const override = process.env[BRIDGE_HOME_ENV];
25
+ export function getBridgeDir(env = process.env) {
26
+ const override = env[BRIDGE_HOME_ENV];
22
27
  if (override) {
23
28
  return override;
24
29
  }
@@ -31,19 +36,139 @@ export function getBridgeDir() {
31
36
  }
32
37
 
33
38
  if (platform === 'win32') {
34
- const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
39
+ const localAppData = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
35
40
  return path.join(localAppData, 'Browser Bridge');
36
41
  }
37
42
 
38
- const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, '.local', 'share');
43
+ const xdgDataHome = env.XDG_DATA_HOME || path.join(home, '.local', 'share');
39
44
  return path.join(xdgDataHome, 'browser-bridge');
40
45
  }
41
46
 
42
47
  /**
48
+ * Resolve the IPC endpoint the daemon listens on and the CLI / native host
49
+ * connect to. On Windows we use a Named Pipe by default because Node's AF_UNIX
50
+ * support fails with EACCES on listen() under recent Node + Windows 11
51
+ * combinations, while Named Pipes are the historical and reliable Windows IPC
52
+ * mechanism. An explicit bridge-home override keeps the legacy socket path so
53
+ * callers can opt into custom test or compatibility setups.
54
+ *
55
+ * @param {NodeJS.ProcessEnv} [env=process.env]
56
+ * @returns {string}
57
+ */
58
+ export function getSocketPath(env = process.env) {
59
+ if (os.platform() === 'win32' && !env[BRIDGE_HOME_ENV]) {
60
+ return `\\\\.\\pipe\\${APP_NAME}`;
61
+ }
62
+ return path.join(getBridgeDir(env), 'bridge.sock');
63
+ }
64
+
65
+ /**
66
+ * @param {string} socketPath
67
+ * @returns {BridgeTransport}
68
+ */
69
+ export function createSocketBridgeTransport(socketPath) {
70
+ return {
71
+ type: 'socket',
72
+ socketPath,
73
+ label: socketPath,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * @param {number} port
79
+ * @param {string} [host='127.0.0.1']
80
+ * @returns {BridgeTransport}
81
+ */
82
+ export function createTcpBridgeTransport(port, host = '127.0.0.1') {
83
+ return {
84
+ type: 'tcp',
85
+ host,
86
+ port,
87
+ label: `${host}:${port}`,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * @param {NodeJS.ProcessEnv} [env=process.env]
93
+ * @returns {number | null}
94
+ */
95
+ export function getBridgeTcpPort(env = process.env) {
96
+ const raw = env[BRIDGE_TCP_PORT_ENV];
97
+ if (raw == null || raw === '') {
98
+ return null;
99
+ }
100
+
101
+ const port = Number.parseInt(raw, 10);
102
+ if (!Number.isInteger(port) || String(port) !== String(raw).trim() || port < 1 || port > 65535) {
103
+ throw new Error(
104
+ `${BRIDGE_TCP_PORT_ENV} must be an integer between 1 and 65535 (got ${JSON.stringify(raw)}).`
105
+ );
106
+ }
107
+
108
+ return port;
109
+ }
110
+
111
+ /**
112
+ * Align Windows CLI/daemon entrypoints with the installed native-host launcher,
113
+ * which uses TCP by default. Preserve explicit overrides and custom bridge-home
114
+ * test setups that rely on the socket transport.
115
+ *
116
+ * @param {NodeJS.ProcessEnv} [env=process.env]
117
+ * @returns {boolean}
118
+ */
119
+ export function applyWindowsTcpTransportDefaults(env = process.env) {
120
+ if (os.platform() !== 'win32') {
121
+ return false;
122
+ }
123
+ if (env[BRIDGE_TCP_PORT_ENV] != null && env[BRIDGE_TCP_PORT_ENV] !== '') {
124
+ return false;
125
+ }
126
+ if (env[BRIDGE_HOME_ENV]) {
127
+ return false;
128
+ }
129
+
130
+ env[BRIDGE_TCP_PORT_ENV] = String(DEFAULT_WINDOWS_TCP_PORT);
131
+ return true;
132
+ }
133
+
134
+ /**
135
+ * @param {NodeJS.ProcessEnv} [env=process.env]
136
+ * @returns {BridgeTransport}
137
+ */
138
+ export function getBridgeTransport(env = process.env) {
139
+ const tcpPort = getBridgeTcpPort(env);
140
+ if (tcpPort !== null) {
141
+ return createTcpBridgeTransport(tcpPort);
142
+ }
143
+
144
+ return createSocketBridgeTransport(getSocketPath(env));
145
+ }
146
+
147
+ /**
148
+ * @param {BridgeTransport} [transport=getBridgeTransport()]
149
+ * @returns {import('node:net').ListenOptions | string}
150
+ */
151
+ export function getBridgeListenTarget(transport = getBridgeTransport()) {
152
+ if (transport.type === 'tcp') {
153
+ return { host: transport.host, port: transport.port };
154
+ }
155
+ return transport.socketPath;
156
+ }
157
+
158
+ /**
159
+ * @param {BridgeTransport} [transport=getBridgeTransport()]
160
+ * @returns {string}
161
+ */
162
+ export function formatBridgeTransport(transport = getBridgeTransport()) {
163
+ return transport.label;
164
+ }
165
+
166
+ /**
167
+ * @param {NodeJS.ProcessEnv} [env=process.env]
43
168
  * @returns {string}
44
169
  */
45
- export function getSocketPath() {
46
- return path.join(getBridgeDir(), 'bridge.sock');
170
+ export function getDaemonPidPath(env = process.env) {
171
+ return path.join(getBridgeDir(env), 'daemon.pid');
47
172
  }
48
173
 
49
174
  /**
@@ -0,0 +1,157 @@
1
+ // @ts-check
2
+
3
+ /** @typedef {{ write: (chunk: string) => void }} LogStream */
4
+
5
+ /**
6
+ * @enum {number}
7
+ * @private
8
+ */
9
+ const LOG_LEVELS = /** @type {const} */ ({
10
+ debug: 0,
11
+ info: 1,
12
+ warn: 2,
13
+ error: 3,
14
+ });
15
+
16
+ /** @typedef {keyof typeof LOG_LEVELS} LogLevel */
17
+
18
+ /**
19
+ * @typedef {{
20
+ * debug(message: string, extra?: Record<string, unknown>): void;
21
+ * info(message: string, extra?: Record<string, unknown>): void;
22
+ * warn(message: string, extra?: Record<string, unknown>): void;
23
+ * error(message: string, extra?: Record<string, unknown>): void;
24
+ * }} DaemonLoggerLike
25
+ */
26
+
27
+ /**
28
+ * Structured JSON logger for the daemon. Each call emits a single NDJSON line
29
+ * with a timestamp, level, message, and optional structured fields.
30
+ */
31
+ export class DaemonLogger {
32
+ /** @type {LogStream} */
33
+ #stream;
34
+ /** @type {LogLevel} */
35
+ #minLevel;
36
+ /** @type {Record<string, unknown>} */
37
+ #defaults;
38
+
39
+ /**
40
+ * @param {{ stream?: LogStream, minLevel?: LogLevel, defaults?: Record<string, unknown> }} [options]
41
+ */
42
+ constructor({ stream, minLevel = 'info', defaults = {} } = {}) {
43
+ this.#stream = stream ?? /** @type {LogStream} */ (process.stderr);
44
+ this.#minLevel = minLevel;
45
+ this.#defaults = defaults;
46
+ }
47
+
48
+ /**
49
+ * @param {LogLevel} level
50
+ * @param {string} message
51
+ * @param {Record<string, unknown>} [extra]
52
+ * @returns {void}
53
+ */
54
+ #write(level, message, extra) {
55
+ if (LOG_LEVELS[level] < LOG_LEVELS[this.#minLevel]) {
56
+ return;
57
+ }
58
+ const entry = {
59
+ ...this.#defaults,
60
+ timestamp: new Date().toISOString(),
61
+ level,
62
+ message,
63
+ ...extra,
64
+ };
65
+ this.#stream.write(`${JSON.stringify(entry)}\n`);
66
+ }
67
+
68
+ /**
69
+ * @param {string} message
70
+ * @param {Record<string, unknown>} [extra]
71
+ * @returns {void}
72
+ */
73
+ debug(message, extra) {
74
+ this.#write('debug', message, extra);
75
+ }
76
+
77
+ /**
78
+ * @param {string} message
79
+ * @param {Record<string, unknown>} [extra]
80
+ * @returns {void}
81
+ */
82
+ info(message, extra) {
83
+ this.#write('info', message, extra);
84
+ }
85
+
86
+ /**
87
+ * @param {string} message
88
+ * @param {Record<string, unknown>} [extra]
89
+ * @returns {void}
90
+ */
91
+ warn(message, extra) {
92
+ this.#write('warn', message, extra);
93
+ }
94
+
95
+ /**
96
+ * @param {string} message
97
+ * @param {Record<string, unknown>} [extra]
98
+ * @returns {void}
99
+ */
100
+ error(message, extra) {
101
+ this.#write('error', message, extra);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * A silent logger that discards all output. Useful in tests.
107
+ * @type {DaemonLoggerLike}
108
+ */
109
+ export const silentLogger = {
110
+ debug() {},
111
+ info() {},
112
+ warn() {},
113
+ error() {},
114
+ };
115
+
116
+ /**
117
+ * Wrap a legacy `Pick<Console, 'log' | 'error'>` logger into the new
118
+ * `DaemonLoggerLike` interface. Used for backward compatibility with
119
+ * callers that still pass `{ log() {}, error() {} }`.
120
+ *
121
+ * @param {Pick<Console, 'log' | 'error'>} legacy
122
+ * @returns {DaemonLoggerLike}
123
+ */
124
+ export function wrapLegacyLogger(legacy) {
125
+ return {
126
+ debug(message, extra) {
127
+ legacy.log?.(`[debug] ${message}`, extra ?? '');
128
+ },
129
+ info(message, extra) {
130
+ legacy.log?.(`[info] ${message}`, extra ?? '');
131
+ },
132
+ warn(message, extra) {
133
+ legacy.log?.(`[warn] ${message}`, extra ?? '');
134
+ },
135
+ error(message, extra) {
136
+ legacy.error?.(`[error] ${message}`, extra ?? '');
137
+ },
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Accept either a `DaemonLoggerLike` or a legacy `Pick<Console, 'log' | 'error'>`
143
+ * and return a normalized `DaemonLoggerLike`. If the input already has an `info`
144
+ * method it is returned as-is; otherwise it is wrapped via `wrapLegacyLogger`.
145
+ *
146
+ * @param {DaemonLoggerLike | Pick<Console, 'log' | 'error'> | undefined} logger
147
+ * @returns {DaemonLoggerLike}
148
+ */
149
+ export function normalizeDaemonLogger(logger) {
150
+ if (logger === undefined) {
151
+ return new DaemonLogger();
152
+ }
153
+ if (typeof (/** @type {any} */ (logger).info) === 'function') {
154
+ return /** @type {DaemonLoggerLike} */ (logger);
155
+ }
156
+ return wrapLegacyLogger(/** @type {Pick<Console, 'log' | 'error'>} */ (logger));
157
+ }