@openclaw-cloud/agent-controller 2.0.0 → 2.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 (62) hide show
  1. package/bin/agent-controller.js +11 -2
  2. package/dist/commands/bootstrap.js +3 -28
  3. package/dist/commands/bootstrap.js.map +1 -1
  4. package/dist/commands/channel-server.d.ts +17 -0
  5. package/dist/commands/channel-server.js +71 -0
  6. package/dist/commands/channel-server.js.map +1 -0
  7. package/dist/config-file.js +6 -0
  8. package/dist/config-file.js.map +1 -1
  9. package/dist/config.d.ts +10 -0
  10. package/dist/config.js +20 -0
  11. package/dist/config.js.map +1 -1
  12. package/dist/connection.js +11 -0
  13. package/dist/connection.js.map +1 -1
  14. package/dist/handlers/board-handler.js +56 -20
  15. package/dist/handlers/board-handler.js.map +1 -1
  16. package/dist/handlers/chat.d.ts +17 -0
  17. package/dist/handlers/chat.js +93 -0
  18. package/dist/handlers/chat.js.map +1 -1
  19. package/dist/handlers/memory.d.ts +6 -0
  20. package/dist/handlers/memory.js +100 -13
  21. package/dist/handlers/memory.js.map +1 -1
  22. package/dist/index.js +47 -4
  23. package/dist/index.js.map +1 -1
  24. package/dist/providers/claude-code/channel-server.d.ts +60 -0
  25. package/dist/providers/claude-code/channel-server.js +155 -0
  26. package/dist/providers/claude-code/channel-server.js.map +1 -0
  27. package/dist/providers/claude-code/index.d.ts +68 -0
  28. package/dist/providers/claude-code/index.js +280 -0
  29. package/dist/providers/claude-code/index.js.map +1 -0
  30. package/dist/providers/claude-code/login-flow.d.ts +26 -0
  31. package/dist/providers/claude-code/login-flow.js +135 -0
  32. package/dist/providers/claude-code/login-flow.js.map +1 -0
  33. package/dist/providers/claude-code/settings-writer.d.ts +29 -0
  34. package/dist/providers/claude-code/settings-writer.js +94 -0
  35. package/dist/providers/claude-code/settings-writer.js.map +1 -0
  36. package/dist/providers/claude-code/socket-bridge.d.ts +98 -0
  37. package/dist/providers/claude-code/socket-bridge.js +301 -0
  38. package/dist/providers/claude-code/socket-bridge.js.map +1 -0
  39. package/dist/providers/claude-code/spawn-claude.d.ts +48 -0
  40. package/dist/providers/claude-code/spawn-claude.js +108 -0
  41. package/dist/providers/claude-code/spawn-claude.js.map +1 -0
  42. package/dist/providers/index.d.ts +3 -1
  43. package/dist/providers/index.js +16 -0
  44. package/dist/providers/index.js.map +1 -1
  45. package/dist/utils/agent-controller-bin.d.ts +8 -0
  46. package/dist/utils/agent-controller-bin.js +58 -0
  47. package/dist/utils/agent-controller-bin.js.map +1 -0
  48. package/dist/utils/anthropic-auth.d.ts +25 -0
  49. package/dist/utils/anthropic-auth.js +13 -0
  50. package/dist/utils/anthropic-auth.js.map +1 -0
  51. package/dist/utils/apply-config.d.ts +28 -0
  52. package/dist/utils/apply-config.js +226 -0
  53. package/dist/utils/apply-config.js.map +1 -1
  54. package/dist/utils/claude-env.js +9 -4
  55. package/dist/utils/claude-env.js.map +1 -1
  56. package/dist/utils/config-merge-paths.d.ts +21 -0
  57. package/dist/utils/config-merge-paths.js +27 -0
  58. package/dist/utils/config-merge-paths.js.map +1 -0
  59. package/dist/utils/write-workspace-files.d.ts +15 -0
  60. package/dist/utils/write-workspace-files.js +48 -0
  61. package/dist/utils/write-workspace-files.js.map +1 -0
  62. package/package.json +1 -1
@@ -0,0 +1,135 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { logCollector } from '../../connection.js';
6
+ import { toErrorMessage } from '../../utils/response.js';
7
+ const AUTH_URL_RE = /https:\/\/claude\.ai\/oauth\/authorize\?\S+/;
8
+ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
9
+ /**
10
+ * Drive the `claude setup-token` subscription login flow.
11
+ *
12
+ * Flow:
13
+ * 1. Spawn `claude setup-token` and watch stdout for the `claude.ai/oauth/authorize?...` URL.
14
+ * 2. Push the URL to chat via `deps.publishLoginUrl`.
15
+ * 3. Await `deps.waitForCode()` — resolves with the code the user submitted.
16
+ * 4. Write `code + "\n"` to subprocess stdin, wait for exit.
17
+ * 5. Verify `~/.claude/.credentials.json` was created/updated → success.
18
+ *
19
+ * Emits structured logCollector events at each milestone.
20
+ */
21
+ export async function runSetupTokenFlow(deps) {
22
+ const spawnFn = deps.spawnFn ?? spawn;
23
+ const credPath = deps.credentialsPath ?? path.join(os.homedir(), '.claude', '.credentials.json');
24
+ const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
25
+ const startedAt = Date.now();
26
+ logCollector?.push('claude_login_start', 'info', 'Starting claude setup-token flow');
27
+ const credMtimeBefore = safeMtime(credPath);
28
+ const child = spawnFn('claude', ['setup-token'], {
29
+ stdio: ['pipe', 'pipe', 'pipe'],
30
+ });
31
+ let stdoutBuf = '';
32
+ let stderrBuf = '';
33
+ let urlPublished = false;
34
+ let codeWritten = false;
35
+ return new Promise((resolve, reject) => {
36
+ const timeoutHandle = setTimeout(() => {
37
+ cleanup();
38
+ logCollector?.push('claude_login_failed', 'error', 'Claude login timed out before completion', { durationMs: Date.now() - startedAt });
39
+ try {
40
+ child.kill('SIGKILL');
41
+ }
42
+ catch {
43
+ /* ignore */
44
+ }
45
+ reject(new Error('claude setup-token timed out'));
46
+ }, timeoutMs);
47
+ const onStdout = (chunk) => {
48
+ const text = chunk.toString();
49
+ stdoutBuf += text;
50
+ if (!urlPublished) {
51
+ const match = stdoutBuf.match(AUTH_URL_RE);
52
+ if (match) {
53
+ urlPublished = true;
54
+ const url = match[0];
55
+ logCollector?.push('claude_login_url_published', 'info', 'Captured OAuth URL', { url });
56
+ Promise.resolve(deps.publishLoginUrl(url))
57
+ .then(() => deps.waitForCode())
58
+ .then((code) => {
59
+ if (!code || typeof code !== 'string') {
60
+ throw new Error('waitForCode returned empty value');
61
+ }
62
+ codeWritten = true;
63
+ logCollector?.push('claude_login_code_received', 'info', 'User submitted code');
64
+ child.stdin.write(code.trim() + '\n');
65
+ child.stdin.end();
66
+ })
67
+ .catch((err) => {
68
+ cleanup();
69
+ logCollector?.push('claude_login_failed', 'error', `Login pre-exit step failed: ${toErrorMessage(err)}`);
70
+ try {
71
+ child.kill('SIGKILL');
72
+ }
73
+ catch {
74
+ /* ignore */
75
+ }
76
+ reject(err instanceof Error ? err : new Error(String(err)));
77
+ });
78
+ }
79
+ }
80
+ };
81
+ const onStderr = (chunk) => {
82
+ // We only need the last ~500 chars on failure — cap the buffer at 1000
83
+ // so a chatty subprocess can't grow memory unbounded over a long
84
+ // login wait (10-minute default timeout).
85
+ stderrBuf += chunk.toString();
86
+ if (stderrBuf.length > 1000) {
87
+ stderrBuf = stderrBuf.slice(-1000);
88
+ }
89
+ };
90
+ const onError = (err) => {
91
+ cleanup();
92
+ logCollector?.push('claude_login_failed', 'error', `spawn error: ${err.message}`);
93
+ reject(err);
94
+ };
95
+ const onExit = (code) => {
96
+ cleanup();
97
+ if (code === 0 && codeWritten) {
98
+ const credMtimeAfter = safeMtime(credPath);
99
+ const credChanged = credMtimeAfter !== null && credMtimeAfter !== credMtimeBefore;
100
+ if (credChanged) {
101
+ logCollector?.push('claude_login_completed', 'info', 'Claude login succeeded', {
102
+ durationMs: Date.now() - startedAt,
103
+ });
104
+ resolve();
105
+ return;
106
+ }
107
+ logCollector?.push('claude_login_failed', 'error', 'Login exited 0 but credentials file was not updated', { credPath });
108
+ reject(new Error('credentials file not updated after setup-token'));
109
+ return;
110
+ }
111
+ logCollector?.push('claude_login_failed', 'error', `claude setup-token exited with code ${code ?? 'null'}`, { stderrTail: stderrBuf.slice(-500) });
112
+ reject(new Error(`claude setup-token exited with code ${code ?? 'null'}`));
113
+ };
114
+ function cleanup() {
115
+ clearTimeout(timeoutHandle);
116
+ child.stdout.off('data', onStdout);
117
+ child.stderr.off('data', onStderr);
118
+ child.off('error', onError);
119
+ child.off('exit', onExit);
120
+ }
121
+ child.stdout.on('data', onStdout);
122
+ child.stderr.on('data', onStderr);
123
+ child.on('error', onError);
124
+ child.on('exit', onExit);
125
+ });
126
+ }
127
+ function safeMtime(p) {
128
+ try {
129
+ return fs.statSync(p).mtimeMs;
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ }
135
+ //# sourceMappingURL=login-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login-flow.js","sourceRoot":"","sources":["../../../src/providers/claude-code/login-flow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ export interface ClaudeCodeSettingsContext {
2
+ /** Absolute path to the Claude workspace (cwd of `claude` process) */
3
+ workspaceDir: string;
4
+ /** Anthropic API key. If prefixed with `sk-ant-oat` → treated as OAuth token. */
5
+ anthropicApiKey?: string;
6
+ /** Alternative: explicit auth token (takes precedence over anthropicApiKey) */
7
+ anthropicAuthToken?: string;
8
+ /** Backend URL for the openclaw-backend MCP server entry */
9
+ backendUrl?: string;
10
+ /** Bearer token for the openclaw-backend MCP server */
11
+ agentToken?: string;
12
+ /** Content of CLAUDE.md to write into the workspace (persona / bootstrap) */
13
+ claudeMdContent?: string;
14
+ /** Port of the local agent-memory MCP server (defaults to 3457). */
15
+ memoryMcpPort?: number;
16
+ /** Absolute path to the `agent-controller` bin. Defaults to `agent-controller` on PATH. */
17
+ agentControllerBin?: string;
18
+ /** Override the socket path (tests). */
19
+ channelSocketPath?: string;
20
+ }
21
+ /**
22
+ * Write `.claude/settings.json`, `.mcp.json`, and `CLAUDE.md` into the workspace.
23
+ *
24
+ * Idempotent — overwrites each call. Mirrors the logic in
25
+ * `src/utils/claude-env.ts` for OAuth vs API-key auth.
26
+ */
27
+ export declare function writeClaudeCodeSettings(ctx: ClaudeCodeSettingsContext): Promise<void>;
28
+ export declare function buildSettings(ctx: ClaudeCodeSettingsContext): Record<string, unknown>;
29
+ export declare function buildMcpJson(ctx: ClaudeCodeSettingsContext): Record<string, unknown>;
@@ -0,0 +1,94 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { config } from '../../config.js';
4
+ import { getAgentControllerBinPath } from '../../utils/agent-controller-bin.js';
5
+ import { classifyAnthropicAuth } from '../../utils/anthropic-auth.js';
6
+ import { logCollector } from '../../connection.js';
7
+ /**
8
+ * Write `.claude/settings.json`, `.mcp.json`, and `CLAUDE.md` into the workspace.
9
+ *
10
+ * Idempotent — overwrites each call. Mirrors the logic in
11
+ * `src/utils/claude-env.ts` for OAuth vs API-key auth.
12
+ */
13
+ export async function writeClaudeCodeSettings(ctx) {
14
+ const { workspaceDir } = ctx;
15
+ const dotClaude = path.join(workspaceDir, '.claude');
16
+ await fs.mkdir(dotClaude, { recursive: true });
17
+ // settings.json can embed ANTHROPIC_AUTH_TOKEN / ANTHROPIC_API_KEY — chmod
18
+ // 0o600 to limit exposure on multi-user VMs.
19
+ await fs.writeFile(path.join(dotClaude, 'settings.json'), JSON.stringify(buildSettings(ctx), null, 2) + '\n', { mode: 0o600 });
20
+ await fs.writeFile(path.join(workspaceDir, '.mcp.json'), JSON.stringify(buildMcpJson(ctx), null, 2) + '\n');
21
+ if (ctx.claudeMdContent !== undefined) {
22
+ await fs.writeFile(path.join(workspaceDir, 'CLAUDE.md'), ctx.claudeMdContent);
23
+ }
24
+ }
25
+ // ---------------------------------------------------------------------------
26
+ // Builders (exported for unit tests)
27
+ // ---------------------------------------------------------------------------
28
+ export function buildSettings(ctx) {
29
+ const permissions = { allow: ['Bash(*)', 'Read(*)', 'Write(*)', 'Edit(*)', 'mcp__*'] };
30
+ const auth = classifyAnthropicAuth({
31
+ anthropicAuthToken: ctx.anthropicAuthToken,
32
+ anthropicApiKey: ctx.anthropicApiKey,
33
+ });
34
+ switch (auth.kind) {
35
+ case 'auth_token':
36
+ return { permissions, env: { ANTHROPIC_AUTH_TOKEN: auth.value } };
37
+ case 'api_key':
38
+ return { permissions, env: { ANTHROPIC_API_KEY: auth.value } };
39
+ case 'none':
40
+ // No static credentials: rely on ~/.claude/.credentials.json (subscription login).
41
+ return { permissions };
42
+ }
43
+ }
44
+ export function buildMcpJson(ctx) {
45
+ const memoryPort = ctx.memoryMcpPort ?? 3457;
46
+ const servers = {
47
+ 'agent-memory': {
48
+ type: 'http',
49
+ url: `http://127.0.0.1:${memoryPort}/mcp`,
50
+ },
51
+ };
52
+ const backendUrl = ctx.backendUrl ?? config.backendUrl;
53
+ const token = ctx.agentToken ?? config.agentToken;
54
+ if (backendUrl && token) {
55
+ servers['openclaw-backend'] = {
56
+ type: 'http',
57
+ url: `${backendUrl.replace(/\/+$/, '')}/mcp`,
58
+ headers: { Authorization: `Bearer ${token}` },
59
+ };
60
+ }
61
+ // Channel server — spawned by Claude Code as a stdio MCP plugin. Declares
62
+ // the `experimental.claude/channel` capability, so Claude routes inbound
63
+ // chat traffic through it (and invokes the `reply`/`edit_message` tools
64
+ // when it wants to reply to the user).
65
+ //
66
+ // We use a plain stdio MCP entry (Option B) rather than Claude's `--channels
67
+ // plugin:` install mechanism — it avoids any out-of-band plugin install
68
+ // step and works with stock Claude Code.
69
+ const explicitBin = ctx.agentControllerBin?.trim();
70
+ let binary;
71
+ if (explicitBin) {
72
+ binary = explicitBin;
73
+ }
74
+ else {
75
+ const resolved = getAgentControllerBinPath();
76
+ if (resolved) {
77
+ binary = resolved;
78
+ }
79
+ else {
80
+ binary = 'agent-controller';
81
+ logCollector?.push('agent_controller_bin_unresolved', 'warn', 'Could not resolve absolute path to agent-controller bin — falling back to PATH lookup in .mcp.json', { argv1: process.argv[1] ?? null });
82
+ }
83
+ }
84
+ const channelArgs = ['channel-server'];
85
+ if (ctx.channelSocketPath) {
86
+ channelArgs.push('--socket-path', ctx.channelSocketPath);
87
+ }
88
+ servers['agent-controller-channel'] = {
89
+ command: binary,
90
+ args: channelArgs,
91
+ };
92
+ return { mcpServers: servers };
93
+ }
94
+ //# sourceMappingURL=settings-writer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-writer.js","sourceRoot":"","sources":["../../../src/providers/claude-code/settings-writer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { ChannelReply, ChannelMessageMeta, ChannelReplyMeta } from './channel-server.js';
3
+ /**
4
+ * Unix-domain-socket bridge between the main agent-controller process and the
5
+ * channel-server worker that holds Claude Code's stdio MCP channel.
6
+ *
7
+ * Protocol: newline-delimited JSON. Ops:
8
+ * { op: "hello", pid } — worker → main
9
+ * { op: "reply", content, meta } — worker → main (Claude reply tool)
10
+ * { op: "edit", content, meta } — worker → main (Claude edit_message tool)
11
+ * { op: "push", content, meta } — main → worker (inbound chat msg)
12
+ *
13
+ * Liberal parsing: malformed lines are logged + dropped (never crash the link).
14
+ */
15
+ export declare const DEFAULT_SOCKET_PATH: string;
16
+ /**
17
+ * Feed a chunk into a newline-delimited JSON accumulator. Returns the updated
18
+ * buffer (remaining, unterminated partial line) after invoking `onMessage`
19
+ * for each completed line that parses as JSON. Malformed lines invoke
20
+ * `onBadLine` (if provided) and are dropped.
21
+ *
22
+ * Extracted so the ChannelBridgeServer and ChannelBridgeClient can share
23
+ * the identical framing logic without duplication.
24
+ */
25
+ export declare function ingestNdjsonLines<T>(buf: string, chunk: string, onMessage: (msg: Partial<T>) => void, onBadLine?: () => void): string;
26
+ export type SocketOp = {
27
+ op: 'hello';
28
+ pid: number;
29
+ } | {
30
+ op: 'reply';
31
+ content: string;
32
+ meta: ChannelReplyMeta;
33
+ } | {
34
+ op: 'edit';
35
+ content: string;
36
+ meta: ChannelReplyMeta;
37
+ } | {
38
+ op: 'push';
39
+ content: string;
40
+ meta: ChannelMessageMeta;
41
+ };
42
+ export interface ChannelBridgeServerEvents {
43
+ connect: [pid: number | null];
44
+ reply: [content: string, meta: ChannelReplyMeta, tool: 'reply' | 'edit_message'];
45
+ close: [];
46
+ error: [err: Error];
47
+ }
48
+ /**
49
+ * Server side — lives in the main agent-controller process.
50
+ *
51
+ * Accepts exactly one worker connection (extras are rejected + closed). Emits
52
+ * reply/edit ops; callers invoke `push(content, meta)` to emit an inbound
53
+ * chat message to the worker.
54
+ */
55
+ export declare class ChannelBridgeServer extends EventEmitter {
56
+ private readonly opts;
57
+ private server;
58
+ private active;
59
+ private buf;
60
+ constructor(opts?: {
61
+ socketPath?: string;
62
+ });
63
+ get socketPath(): string;
64
+ listen(): Promise<void>;
65
+ private handleConnection;
66
+ private ingest;
67
+ private dispatch;
68
+ /** Push an inbound chat message to the worker (→ Claude MCP notification). */
69
+ push(content: string, meta: ChannelMessageMeta): void;
70
+ private write;
71
+ close(): Promise<void>;
72
+ isConnected(): boolean;
73
+ }
74
+ /**
75
+ * Client side — lives in the channel-server worker subprocess.
76
+ *
77
+ * Connects to the bridge socket with retry/backoff (up to 30s). Parses
78
+ * incoming `push` ops and forwards outgoing `reply`/`edit` ops.
79
+ */
80
+ export declare class ChannelBridgeClient extends EventEmitter {
81
+ private readonly opts;
82
+ private sock;
83
+ private buf;
84
+ private closed;
85
+ constructor(opts?: {
86
+ socketPath?: string;
87
+ maxWaitMs?: number;
88
+ });
89
+ get socketPath(): string;
90
+ connect(): Promise<void>;
91
+ private connectOnce;
92
+ sendHello(pid: number): void;
93
+ sendReply(content: string, meta: ChannelReplyMeta, tool: 'reply' | 'edit_message'): void;
94
+ private ingest;
95
+ private write;
96
+ close(): void;
97
+ }
98
+ export type { ChannelReply, ChannelMessageMeta, ChannelReplyMeta };
@@ -0,0 +1,301 @@
1
+ import net from 'node:net';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { EventEmitter } from 'node:events';
6
+ import { logCollector } from '../../connection.js';
7
+ import { toErrorMessage } from '../../utils/response.js';
8
+ /**
9
+ * Unix-domain-socket bridge between the main agent-controller process and the
10
+ * channel-server worker that holds Claude Code's stdio MCP channel.
11
+ *
12
+ * Protocol: newline-delimited JSON. Ops:
13
+ * { op: "hello", pid } — worker → main
14
+ * { op: "reply", content, meta } — worker → main (Claude reply tool)
15
+ * { op: "edit", content, meta } — worker → main (Claude edit_message tool)
16
+ * { op: "push", content, meta } — main → worker (inbound chat msg)
17
+ *
18
+ * Liberal parsing: malformed lines are logged + dropped (never crash the link).
19
+ */
20
+ export const DEFAULT_SOCKET_PATH = path.join(os.homedir(), '.openclaw', 'claude-channel.sock');
21
+ /**
22
+ * Feed a chunk into a newline-delimited JSON accumulator. Returns the updated
23
+ * buffer (remaining, unterminated partial line) after invoking `onMessage`
24
+ * for each completed line that parses as JSON. Malformed lines invoke
25
+ * `onBadLine` (if provided) and are dropped.
26
+ *
27
+ * Extracted so the ChannelBridgeServer and ChannelBridgeClient can share
28
+ * the identical framing logic without duplication.
29
+ */
30
+ export function ingestNdjsonLines(buf, chunk, onMessage, onBadLine) {
31
+ let updated = buf + chunk;
32
+ let idx;
33
+ while ((idx = updated.indexOf('\n')) >= 0) {
34
+ const line = updated.slice(0, idx).trim();
35
+ updated = updated.slice(idx + 1);
36
+ if (!line)
37
+ continue;
38
+ try {
39
+ onMessage(JSON.parse(line));
40
+ }
41
+ catch {
42
+ onBadLine?.();
43
+ }
44
+ }
45
+ return updated;
46
+ }
47
+ /**
48
+ * Server side — lives in the main agent-controller process.
49
+ *
50
+ * Accepts exactly one worker connection (extras are rejected + closed). Emits
51
+ * reply/edit ops; callers invoke `push(content, meta)` to emit an inbound
52
+ * chat message to the worker.
53
+ */
54
+ export class ChannelBridgeServer extends EventEmitter {
55
+ opts;
56
+ server = null;
57
+ active = null;
58
+ buf = '';
59
+ constructor(opts = {}) {
60
+ super();
61
+ this.opts = opts;
62
+ // Default no-op 'error' listener so re-emitting a server error never turns
63
+ // into an unhandled-exception process crash if no external listener is
64
+ // registered. External callers can still attach their own listener.
65
+ this.on('error', () => {
66
+ /* no-op */
67
+ });
68
+ }
69
+ get socketPath() {
70
+ return this.opts.socketPath ?? DEFAULT_SOCKET_PATH;
71
+ }
72
+ async listen() {
73
+ const p = this.socketPath;
74
+ await fs.promises.mkdir(path.dirname(p), { recursive: true });
75
+ // Best-effort: remove stale socket file from a previous crash.
76
+ try {
77
+ await fs.promises.unlink(p);
78
+ }
79
+ catch {
80
+ /* not present */
81
+ }
82
+ return new Promise((resolve, reject) => {
83
+ const srv = net.createServer((sock) => this.handleConnection(sock));
84
+ let bound = false;
85
+ // Single error handler — before bind completes, a listen error rejects
86
+ // the promise; after bind, it's a runtime error emitted to listeners.
87
+ // This removes the previous two-listener dance (once + on) and the
88
+ // brittle cross-removeListener cleanup on success.
89
+ const onError = (err) => {
90
+ if (!bound) {
91
+ reject(err);
92
+ return;
93
+ }
94
+ logCollector?.push('channel_bridge_error', 'error', `bridge server error: ${err.message}`);
95
+ this.emit('error', err);
96
+ };
97
+ srv.on('error', onError);
98
+ srv.listen(p, () => {
99
+ bound = true;
100
+ this.server = srv;
101
+ logCollector?.push('channel_bridge_listening', 'info', `channel bridge on ${p}`);
102
+ resolve();
103
+ });
104
+ });
105
+ }
106
+ handleConnection(sock) {
107
+ if (this.active) {
108
+ // Reject extras — we expect exactly one worker.
109
+ logCollector?.push('channel_bridge_reject_extra', 'warn', 'extra worker connection rejected — one worker already active');
110
+ try {
111
+ sock.end();
112
+ }
113
+ catch {
114
+ /* ignore */
115
+ }
116
+ return;
117
+ }
118
+ this.active = sock;
119
+ sock.setEncoding('utf8');
120
+ sock.on('data', (chunk) => this.ingest(String(chunk)));
121
+ sock.on('close', () => {
122
+ this.active = null;
123
+ this.buf = '';
124
+ logCollector?.push('channel_bridge_closed', 'info', 'worker connection closed');
125
+ this.emit('close');
126
+ });
127
+ sock.on('error', (err) => {
128
+ logCollector?.push('channel_bridge_sock_error', 'warn', `worker socket error: ${err.message}`);
129
+ });
130
+ }
131
+ ingest(chunk) {
132
+ this.buf = ingestNdjsonLines(this.buf, chunk, (msg) => this.dispatch(msg), () => logCollector?.push('channel_bridge_bad_line', 'warn', 'dropped malformed bridge line'));
133
+ }
134
+ dispatch(msg) {
135
+ if (!msg || typeof msg !== 'object' || !msg.op)
136
+ return;
137
+ switch (msg.op) {
138
+ case 'hello': {
139
+ const pid = typeof msg.pid === 'number' ? msg.pid : null;
140
+ logCollector?.push('channel_bridge_hello', 'info', `worker hello pid=${pid ?? 'unknown'}`);
141
+ this.emit('connect', pid);
142
+ break;
143
+ }
144
+ case 'reply': {
145
+ const m = msg;
146
+ const content = typeof m.content === 'string' ? m.content : '';
147
+ const meta = (m.meta ?? {});
148
+ this.emit('reply', content, meta, 'reply');
149
+ break;
150
+ }
151
+ case 'edit': {
152
+ const m = msg;
153
+ const content = typeof m.content === 'string' ? m.content : '';
154
+ const meta = (m.meta ?? {});
155
+ this.emit('reply', content, meta, 'edit_message');
156
+ break;
157
+ }
158
+ default:
159
+ logCollector?.push('channel_bridge_unknown_op', 'warn', `unknown op: ${String(msg.op)}`);
160
+ }
161
+ }
162
+ /** Push an inbound chat message to the worker (→ Claude MCP notification). */
163
+ push(content, meta) {
164
+ this.write({ op: 'push', content, meta });
165
+ }
166
+ write(op) {
167
+ if (!this.active) {
168
+ logCollector?.push('channel_bridge_no_worker', 'warn', `dropped op ${op.op} — no worker`);
169
+ return;
170
+ }
171
+ try {
172
+ this.active.write(JSON.stringify(op) + '\n');
173
+ }
174
+ catch (err) {
175
+ logCollector?.push('channel_bridge_write_error', 'warn', `write ${op.op} failed: ${toErrorMessage(err)}`);
176
+ }
177
+ }
178
+ async close() {
179
+ try {
180
+ this.active?.end();
181
+ }
182
+ catch {
183
+ /* ignore */
184
+ }
185
+ this.active = null;
186
+ if (!this.server)
187
+ return;
188
+ const srv = this.server;
189
+ this.server = null;
190
+ await new Promise((resolve) => srv.close(() => resolve()));
191
+ try {
192
+ await fs.promises.unlink(this.socketPath);
193
+ }
194
+ catch {
195
+ /* ignore */
196
+ }
197
+ }
198
+ isConnected() {
199
+ return !!this.active;
200
+ }
201
+ }
202
+ /**
203
+ * Client side — lives in the channel-server worker subprocess.
204
+ *
205
+ * Connects to the bridge socket with retry/backoff (up to 30s). Parses
206
+ * incoming `push` ops and forwards outgoing `reply`/`edit` ops.
207
+ */
208
+ export class ChannelBridgeClient extends EventEmitter {
209
+ opts;
210
+ sock = null;
211
+ buf = '';
212
+ closed = false;
213
+ constructor(opts = {}) {
214
+ super();
215
+ this.opts = opts;
216
+ }
217
+ get socketPath() {
218
+ return this.opts.socketPath ?? DEFAULT_SOCKET_PATH;
219
+ }
220
+ async connect() {
221
+ const maxWait = this.opts.maxWaitMs ?? 30_000;
222
+ const started = Date.now();
223
+ let attempt = 0;
224
+ // Retry with capped exponential backoff while the main process comes up.
225
+ while (true) {
226
+ try {
227
+ await this.connectOnce();
228
+ return;
229
+ }
230
+ catch (err) {
231
+ if (Date.now() - started > maxWait) {
232
+ throw err instanceof Error ? err : new Error(String(err));
233
+ }
234
+ attempt += 1;
235
+ const delay = Math.min(2000, 100 * 2 ** Math.min(attempt, 6));
236
+ await new Promise((r) => setTimeout(r, delay));
237
+ }
238
+ }
239
+ }
240
+ connectOnce() {
241
+ return new Promise((resolve, reject) => {
242
+ const s = net.createConnection({ path: this.socketPath });
243
+ const onError = (err) => {
244
+ s.removeAllListeners();
245
+ reject(err);
246
+ };
247
+ s.once('error', onError);
248
+ s.once('connect', () => {
249
+ s.removeListener('error', onError);
250
+ s.setEncoding('utf8');
251
+ this.sock = s;
252
+ s.on('data', (chunk) => this.ingest(String(chunk)));
253
+ s.on('close', () => {
254
+ this.sock = null;
255
+ this.buf = '';
256
+ if (!this.closed)
257
+ this.emit('close');
258
+ });
259
+ s.on('error', (err) => this.emit('error', err));
260
+ resolve();
261
+ });
262
+ });
263
+ }
264
+ sendHello(pid) {
265
+ this.write({ op: 'hello', pid });
266
+ }
267
+ sendReply(content, meta, tool) {
268
+ this.write({ op: tool === 'edit_message' ? 'edit' : 'reply', content, meta });
269
+ }
270
+ ingest(chunk) {
271
+ this.buf = ingestNdjsonLines(this.buf, chunk, (msg) => {
272
+ if (msg && msg.op === 'push') {
273
+ const m = msg;
274
+ const content = typeof m.content === 'string' ? m.content : '';
275
+ const meta = (m.meta ?? {});
276
+ this.emit('push', content, meta);
277
+ }
278
+ });
279
+ }
280
+ write(op) {
281
+ if (!this.sock)
282
+ return;
283
+ try {
284
+ this.sock.write(JSON.stringify(op) + '\n');
285
+ }
286
+ catch {
287
+ /* ignore — socket will emit close/error */
288
+ }
289
+ }
290
+ close() {
291
+ this.closed = true;
292
+ try {
293
+ this.sock?.end();
294
+ }
295
+ catch {
296
+ /* ignore */
297
+ }
298
+ this.sock = null;
299
+ }
300
+ }
301
+ //# sourceMappingURL=socket-bridge.js.map