@bbigbang/runtime-acp 0.1.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.
@@ -0,0 +1,31 @@
1
+ export type JsonRpcVersion = '2.0';
2
+ export type JsonRpcId = number | string;
3
+ export type JsonRpcError = {
4
+ code: number;
5
+ message: string;
6
+ data?: unknown;
7
+ };
8
+ export type JsonRpcRequest = {
9
+ jsonrpc: JsonRpcVersion;
10
+ id: JsonRpcId;
11
+ method: string;
12
+ params?: unknown;
13
+ };
14
+ export type JsonRpcNotification = {
15
+ jsonrpc: JsonRpcVersion;
16
+ method: string;
17
+ params?: unknown;
18
+ };
19
+ export type JsonRpcResponse = {
20
+ jsonrpc: JsonRpcVersion;
21
+ id: JsonRpcId;
22
+ result: unknown;
23
+ } | {
24
+ jsonrpc: JsonRpcVersion;
25
+ id: JsonRpcId;
26
+ error: JsonRpcError;
27
+ };
28
+ export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;
29
+ export declare function isRequest(message: any): message is JsonRpcRequest;
30
+ export declare function isNotification(message: any): message is JsonRpcNotification;
31
+ export declare function isResponse(message: any): message is JsonRpcResponse;
@@ -0,0 +1,18 @@
1
+ export function isRequest(message) {
2
+ return (message &&
3
+ message.jsonrpc === '2.0' &&
4
+ typeof message.method === 'string' &&
5
+ 'id' in message);
6
+ }
7
+ export function isNotification(message) {
8
+ return (message &&
9
+ message.jsonrpc === '2.0' &&
10
+ typeof message.method === 'string' &&
11
+ !('id' in message));
12
+ }
13
+ export function isResponse(message) {
14
+ return (message &&
15
+ message.jsonrpc === '2.0' &&
16
+ 'id' in message &&
17
+ ('result' in message || 'error' in message));
18
+ }
@@ -0,0 +1,15 @@
1
+ import type { JsonRpcMessage } from './jsonrpc.js';
2
+ type StdioExitInfo = {
3
+ code: number | null;
4
+ signal: NodeJS.Signals | null;
5
+ error?: string;
6
+ };
7
+ export type StdioProcess = {
8
+ write: (message: JsonRpcMessage) => void;
9
+ onMessage: (cb: (message: JsonRpcMessage) => void) => void;
10
+ onStderr: (cb: (line: string) => void) => void;
11
+ onExit?: (cb: (info: StdioExitInfo) => void) => void;
12
+ kill: () => void;
13
+ };
14
+ export declare function spawnAcpAgent(command: string, args: string[], env?: NodeJS.ProcessEnv, cwd?: string): StdioProcess;
15
+ export {};
@@ -0,0 +1,201 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import readline from 'node:readline';
5
+ import { log } from '../logging.js';
6
+ const ACP_AGENT_COMMAND_FALLBACKS = {
7
+ 'claude-code-acp': {
8
+ command: 'npx',
9
+ args: ['-y', '@zed-industries/claude-code-acp@latest'],
10
+ },
11
+ 'codex-acp': {
12
+ command: 'npx',
13
+ args: ['-y', '@zed-industries/codex-acp@latest'],
14
+ },
15
+ };
16
+ const CODEX_RUNTIME_ENV_DENYLIST = [
17
+ 'CODEX_SANDBOX_NETWORK_DISABLED',
18
+ 'CODEX_THREAD_ID',
19
+ ];
20
+ export function spawnAcpAgent(command, args, env, cwd) {
21
+ const childEnv = {
22
+ ...process.env,
23
+ // Unset vars that would prevent Claude Code from starting inside an existing Claude session
24
+ CLAUDECODE: undefined,
25
+ CLAUDE_CODE_ENTRYPOINT: undefined,
26
+ ...env,
27
+ };
28
+ if (isCodexAcpAgent(command, args)) {
29
+ for (const key of CODEX_RUNTIME_ENV_DENYLIST) {
30
+ delete childEnv[key];
31
+ }
32
+ }
33
+ const spawnPlan = resolveSpawnPlan(command, args, childEnv);
34
+ if (spawnPlan.fallbackFrom) {
35
+ log.warn('ACP agent command not found on PATH; falling back to npx package execution', {
36
+ requestedCommand: spawnPlan.fallbackFrom,
37
+ fallbackCommand: spawnPlan.command,
38
+ cwd,
39
+ });
40
+ }
41
+ const child = spawn(spawnPlan.command, spawnPlan.args, {
42
+ stdio: ['pipe', 'pipe', 'pipe'],
43
+ ...(cwd ? { cwd } : {}),
44
+ env: childEnv,
45
+ });
46
+ const stdoutRl = child.stdout ? readline.createInterface({
47
+ input: child.stdout,
48
+ crlfDelay: Infinity,
49
+ }) : null;
50
+ const stderrRl = child.stderr ? readline.createInterface({
51
+ input: child.stderr,
52
+ crlfDelay: Infinity,
53
+ }) : null;
54
+ const messageHandlers = [];
55
+ const stderrHandlers = [];
56
+ const exitHandlers = [];
57
+ let exitInfo = null;
58
+ let stdinError = null;
59
+ stdoutRl?.on('line', (line) => {
60
+ if (!line.trim())
61
+ return;
62
+ try {
63
+ const msg = JSON.parse(line);
64
+ messageHandlers.forEach((h) => h(msg));
65
+ }
66
+ catch (error) {
67
+ log.error('ACP stdout non-JSON line (fatal):', line);
68
+ log.error(error);
69
+ child.kill('SIGKILL');
70
+ }
71
+ });
72
+ stderrRl?.on('line', (line) => {
73
+ stderrHandlers.forEach((h) => h(line));
74
+ });
75
+ const handleExit = (code, signal, error) => {
76
+ if (!exitInfo) {
77
+ exitInfo = { code, signal, ...(error ? { error } : {}) };
78
+ }
79
+ log.warn('ACP agent exited', exitInfo);
80
+ exitHandlers.forEach((h) => h(exitInfo));
81
+ };
82
+ child.on('error', (error) => {
83
+ log.error('ACP agent process error', error);
84
+ handleExit(null, null, error instanceof Error ? error.message : String(error));
85
+ });
86
+ child.stdin && 'on' in child.stdin && child.stdin.on('error', (error) => {
87
+ stdinError = error instanceof Error ? error : new Error(String(error));
88
+ log.warn('ACP agent stdin error', stdinError);
89
+ handleExit(null, null, stdinError.message);
90
+ });
91
+ child.stdout && 'on' in child.stdout && child.stdout.on('error', (error) => {
92
+ log.warn('ACP agent stdout error', error);
93
+ handleExit(null, null, error instanceof Error ? error.message : String(error));
94
+ });
95
+ child.stderr && 'on' in child.stderr && child.stderr.on('error', (error) => {
96
+ log.warn('ACP agent stderr error', error);
97
+ handleExit(null, null, error instanceof Error ? error.message : String(error));
98
+ });
99
+ child.on('exit', (code, signal) => {
100
+ handleExit(code, signal);
101
+ });
102
+ function write(message) {
103
+ if (exitInfo) {
104
+ const errorSuffix = exitInfo.error ? `, error=${exitInfo.error}` : '';
105
+ throw new Error(`ACP process is not running (code=${String(exitInfo.code)}, signal=${String(exitInfo.signal)}${errorSuffix})`);
106
+ }
107
+ if (stdinError) {
108
+ throw new Error(`ACP process stdin is not writable: ${stdinError.message}`);
109
+ }
110
+ if (!child.stdin || child.stdin.destroyed || !child.stdin.writable) {
111
+ throw new Error('ACP process stdin is not writable.');
112
+ }
113
+ const payload = JSON.stringify(message);
114
+ if (payload.includes('\n')) {
115
+ // JSON itself must be newline-delimited; embedded newlines here are a bug.
116
+ throw new Error('ACP message serialization produced newline');
117
+ }
118
+ child.stdin.write(payload + '\n', (error) => {
119
+ if (!error)
120
+ return;
121
+ stdinError = error instanceof Error ? error : new Error(String(error));
122
+ log.warn('ACP agent stdin write failed', stdinError);
123
+ handleExit(null, null, stdinError.message);
124
+ });
125
+ }
126
+ return {
127
+ write,
128
+ onMessage: (cb) => {
129
+ messageHandlers.push(cb);
130
+ },
131
+ onStderr: (cb) => {
132
+ stderrHandlers.push(cb);
133
+ },
134
+ onExit: (cb) => {
135
+ exitHandlers.push(cb);
136
+ if (exitInfo)
137
+ cb(exitInfo);
138
+ },
139
+ kill: () => {
140
+ child.kill('SIGTERM');
141
+ },
142
+ };
143
+ }
144
+ function isCodexAcpAgent(command, args) {
145
+ const basename = path.basename(command);
146
+ if (basename === 'codex-acp')
147
+ return true;
148
+ if (basename === 'codex' && args[0] === 'acp')
149
+ return true;
150
+ return args.some((arg) => arg.includes('codex-acp'));
151
+ }
152
+ function resolveSpawnPlan(command, args, env) {
153
+ if (hasPathSeparator(command) || isCommandOnPath(command, env)) {
154
+ return { command, args };
155
+ }
156
+ const fallback = ACP_AGENT_COMMAND_FALLBACKS[command];
157
+ if (!fallback)
158
+ return { command, args };
159
+ return {
160
+ command: fallback.command,
161
+ args: [...fallback.args, ...args],
162
+ fallbackFrom: command,
163
+ };
164
+ }
165
+ function isCommandOnPath(command, env) {
166
+ const rawPath = env.PATH ?? process.env.PATH ?? '';
167
+ if (!rawPath)
168
+ return false;
169
+ const candidates = rawPath
170
+ .split(path.delimiter)
171
+ .map((entry) => entry || '.')
172
+ .filter(Boolean);
173
+ const extensions = process.platform === 'win32'
174
+ ? (env.PATHEXT ?? process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM')
175
+ .split(';')
176
+ .filter(Boolean)
177
+ : [''];
178
+ for (const dir of candidates) {
179
+ for (const extension of extensions) {
180
+ const fileName = process.platform === 'win32' && hasKnownExtension(command)
181
+ ? command
182
+ : `${command}${extension}`;
183
+ const candidate = path.join(dir, fileName);
184
+ try {
185
+ fs.accessSync(candidate, fs.constants.X_OK);
186
+ return true;
187
+ }
188
+ catch {
189
+ // keep probing PATH entries
190
+ }
191
+ }
192
+ }
193
+ return false;
194
+ }
195
+ function hasPathSeparator(command) {
196
+ return command.includes('/') || command.includes('\\');
197
+ }
198
+ function hasKnownExtension(command) {
199
+ const ext = path.extname(command);
200
+ return ext.length > 0;
201
+ }
@@ -0,0 +1,171 @@
1
+ export type InitializeParams = {
2
+ protocolVersion: number;
3
+ clientCapabilities: {
4
+ fs?: {
5
+ readTextFile?: boolean;
6
+ writeTextFile?: boolean;
7
+ };
8
+ terminal?: boolean;
9
+ };
10
+ clientInfo?: {
11
+ name: string;
12
+ title?: string;
13
+ version?: string;
14
+ };
15
+ };
16
+ export type InitializeResult = {
17
+ protocolVersion: number;
18
+ agentCapabilities: {
19
+ loadSession?: boolean;
20
+ promptCapabilities?: {
21
+ image?: boolean;
22
+ audio?: boolean;
23
+ embeddedContext?: boolean;
24
+ };
25
+ mcpCapabilities?: {
26
+ http?: boolean;
27
+ sse?: boolean;
28
+ };
29
+ };
30
+ agentInfo?: {
31
+ name: string;
32
+ title?: string;
33
+ version?: string;
34
+ };
35
+ authMethods?: Array<{
36
+ id: string;
37
+ name: string;
38
+ description?: string;
39
+ }>;
40
+ };
41
+ export type McpServerEntry = {
42
+ name: string;
43
+ command: string;
44
+ args: string[];
45
+ env?: Array<{
46
+ name: string;
47
+ value: string;
48
+ }>;
49
+ } | {
50
+ name: string;
51
+ type: string;
52
+ url: string;
53
+ headers?: Array<{
54
+ name: string;
55
+ value: string;
56
+ }>;
57
+ };
58
+ export type NewSessionParams = {
59
+ cwd: string;
60
+ mcpServers: McpServerEntry[];
61
+ _meta?: {
62
+ systemPrompt?: string | {
63
+ append: string;
64
+ };
65
+ };
66
+ };
67
+ export type NewSessionResult = {
68
+ sessionId: string;
69
+ availableModes?: Array<{
70
+ id: string;
71
+ name: string;
72
+ description?: string;
73
+ }>;
74
+ configOptions?: unknown[];
75
+ };
76
+ export type ContentBlock = {
77
+ type: 'text';
78
+ text: string;
79
+ } | {
80
+ type: 'resource_link';
81
+ uri: string;
82
+ name: string;
83
+ mimeType?: string;
84
+ };
85
+ export type PromptParams = {
86
+ sessionId: string;
87
+ prompt: ContentBlock[];
88
+ };
89
+ export type PromptResult = {
90
+ stopReason: string;
91
+ };
92
+ export type SessionUpdateNotification = {
93
+ sessionId: string;
94
+ update: any;
95
+ };
96
+ export type PermissionOption = {
97
+ optionId: string;
98
+ name: string;
99
+ kind: string;
100
+ };
101
+ export type RequestPermissionParams = {
102
+ sessionId: string;
103
+ toolCall: any;
104
+ options: PermissionOption[];
105
+ };
106
+ export type RequestPermissionOutcome = {
107
+ outcome: 'cancelled';
108
+ } | {
109
+ outcome: 'selected';
110
+ optionId: string;
111
+ };
112
+ export type RequestPermissionResult = {
113
+ outcome: RequestPermissionOutcome;
114
+ };
115
+ export type FsReadTextFileParams = {
116
+ sessionId: string;
117
+ path: string;
118
+ line?: number;
119
+ limit?: number;
120
+ };
121
+ export type FsReadTextFileResult = {
122
+ content: string;
123
+ };
124
+ export type FsWriteTextFileParams = {
125
+ sessionId: string;
126
+ path: string;
127
+ content: string;
128
+ };
129
+ export type TerminalCreateParams = {
130
+ sessionId: string;
131
+ command: string;
132
+ args?: string[];
133
+ cwd?: string | null;
134
+ env?: Array<{
135
+ name: string;
136
+ value: string;
137
+ }>;
138
+ outputByteLimit?: number | null;
139
+ };
140
+ export type TerminalCreateResult = {
141
+ terminalId: string;
142
+ };
143
+ export type TerminalOutputParams = {
144
+ sessionId: string;
145
+ terminalId: string;
146
+ };
147
+ export type TerminalExitStatus = {
148
+ exitCode?: number | null;
149
+ signal?: string | null;
150
+ };
151
+ export type TerminalOutputResult = {
152
+ output: string;
153
+ truncated: boolean;
154
+ exitStatus?: TerminalExitStatus | null;
155
+ };
156
+ export type TerminalWaitForExitParams = {
157
+ sessionId: string;
158
+ terminalId: string;
159
+ };
160
+ export type TerminalWaitForExitResult = {
161
+ exitCode?: number | null;
162
+ signal?: string | null;
163
+ };
164
+ export type TerminalKillParams = {
165
+ sessionId: string;
166
+ terminalId: string;
167
+ };
168
+ export type TerminalReleaseParams = {
169
+ sessionId: string;
170
+ terminalId: string;
171
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import Database from 'better-sqlite3';
2
+ export type Db = Database.Database;
3
+ export declare function openDb(dbPath: string): Db;
package/dist/db/db.js ADDED
@@ -0,0 +1,10 @@
1
+ import Database from 'better-sqlite3';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ export function openDb(dbPath) {
5
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
6
+ const db = new Database(dbPath);
7
+ db.pragma('journal_mode = WAL');
8
+ db.pragma('foreign_keys = ON');
9
+ return db;
10
+ }
@@ -0,0 +1,21 @@
1
+ import type { Db } from './db.js';
2
+ export type DeliveryCheckpointRow = {
3
+ bindingKey: string;
4
+ runId: string;
5
+ lastSeq: number;
6
+ messageId: string | null;
7
+ text: string;
8
+ createdAt: number;
9
+ updatedAt: number;
10
+ };
11
+ export declare function getDeliveryCheckpoint(db: Db, params: {
12
+ bindingKey: string;
13
+ runId: string;
14
+ }): DeliveryCheckpointRow | null;
15
+ export declare function upsertDeliveryCheckpoint(db: Db, params: {
16
+ bindingKey: string;
17
+ runId: string;
18
+ lastSeq: number;
19
+ messageId: string | null;
20
+ text: string;
21
+ }): void;
@@ -0,0 +1,28 @@
1
+ export function getDeliveryCheckpoint(db, params) {
2
+ const row = db
3
+ .prepare(`
4
+ SELECT binding_key as bindingKey,
5
+ run_id as runId,
6
+ last_seq as lastSeq,
7
+ message_id as messageId,
8
+ text,
9
+ created_at as createdAt,
10
+ updated_at as updatedAt
11
+ FROM delivery_checkpoints
12
+ WHERE binding_key = ? AND run_id = ?
13
+ `)
14
+ .get(params.bindingKey, params.runId);
15
+ return row ?? null;
16
+ }
17
+ export function upsertDeliveryCheckpoint(db, params) {
18
+ const now = Date.now();
19
+ db.prepare(`
20
+ INSERT INTO delivery_checkpoints(binding_key, run_id, last_seq, message_id, text, created_at, updated_at)
21
+ VALUES(?, ?, ?, ?, ?, ?, ?)
22
+ ON CONFLICT(binding_key, run_id) DO UPDATE SET
23
+ last_seq = excluded.last_seq,
24
+ message_id = excluded.message_id,
25
+ text = excluded.text,
26
+ updated_at = excluded.updated_at
27
+ `).run(params.bindingKey, params.runId, params.lastSeq, params.messageId, params.text, now, now);
28
+ }
@@ -0,0 +1,2 @@
1
+ import type { Db } from './db.js';
2
+ export declare function migrate(db: Db): void;