@agentshouse/kit 0.2.1-cd9cbd8b6428c → 0.2.1-cf110d15f69c8

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/dist/bridge.js CHANGED
@@ -6,16 +6,15 @@ import { createServer, request as httpRequest } from 'node:http';
6
6
  import { request as httpsRequest } from 'node:https';
7
7
  import { delimiter, dirname, join } from 'node:path';
8
8
  import { parse } from 'yaml';
9
- import { HouseRefusal, retryDelay } from './api.js';
9
+ import { finalRefusal, HouseRefusal, retryDelay, retrying } from './api.js';
10
10
  import { loginPath } from './clis.js';
11
11
  import { changedPaths, } from './copies.js';
12
12
  import { kitHome, readEnrolment } from './home.js';
13
13
  import { callHouse, mcpBody, rpc, UNREACHABLE } from './mcp.js';
14
- import { operationId, uuidOf } from './operation.js';
15
- import { refusalOf } from './refusals.js';
14
+ import { operationId } from './operation.js';
15
+ import { refusalOf, retryable } from './refusals.js';
16
16
  import { relayed } from './relay.js';
17
17
  const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
18
- const ATTACHMENTS = '/kit/attachments/upload';
19
18
  export async function received(request) {
20
19
  let text = '';
21
20
  request.setEncoding('utf8');
@@ -23,7 +22,8 @@ export async function received(request) {
23
22
  text += chunk;
24
23
  return text;
25
24
  }
26
- function relay(request, response, target, authorization) {
25
+ function relay(request, response, house, authorization) {
26
+ const target = relayed(house);
27
27
  const send = target.protocol === 'https:' ? httpsRequest : httpRequest;
28
28
  const forwarded = send(target, {
29
29
  method: request.method,
@@ -39,8 +39,6 @@ function refusalIn(text, status) {
39
39
  return refusalOf(text) ?? { code: '', text: text === '' ? `House refused it with ${status}` : text, conflicts: [] };
40
40
  }
41
41
  function answered(forwarded) {
42
- if (forwarded.status >= 500)
43
- throw new HouseRefusal('/', forwarded.status, forwarded.text);
44
42
  if (forwarded.status !== 200)
45
43
  return { refused: refusalIn(forwarded.text, forwarded.status) };
46
44
  const answer = JSON.parse(forwarded.text);
@@ -64,16 +62,11 @@ function pendingSeconds(forwarded) {
64
62
  function resendAfter(forwarded, attempt) {
65
63
  if (forwarded === null)
66
64
  return retryDelay(attempt);
67
- if (forwarded.status >= 500)
68
- return refusalOf(forwarded.text) === null ? retryDelay(attempt) : null;
65
+ if (forwarded.status !== 200)
66
+ return retryable(forwarded.text) === false ? null : retryDelay(attempt);
69
67
  const seconds = pendingSeconds(forwarded);
70
68
  return seconds === null ? null : seconds * 1000;
71
69
  }
72
- function versioned(conversationId, text) {
73
- const file = JSON.parse(text);
74
- const identity = JSON.stringify([conversationId, file.room_ref ?? null, file.name, file.sha256]);
75
- return JSON.stringify({ ...file, version: uuidOf(createHash('sha256').update(identity).digest(), 8) });
76
- }
77
70
  function editAnswer(forwarded) {
78
71
  const answer = answered(forwarded);
79
72
  if ('refused' in answer)
@@ -104,6 +97,7 @@ export async function openBridge(house, copies, conversationId, signal) {
104
97
  const after = resendAfter(forwarded, attempt);
105
98
  if (after === null)
106
99
  return forwarded;
100
+ // A Tool call House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
107
101
  await delay(after, undefined, { signal: closed.signal });
108
102
  }
109
103
  };
@@ -118,46 +112,32 @@ export async function openBridge(house, copies, conversationId, signal) {
118
112
  if ('refused' in prepared)
119
113
  return prepared;
120
114
  const { transfer } = parse(prepared.text);
121
- const posted = await fetch(relayed(transfer.url), {
122
- method: 'POST',
123
- headers: { 'content-type': 'text/plain; charset=utf-8' },
124
- body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
125
- signal: closed.signal,
126
- });
127
- const text = await posted.text();
128
- if (posted.status >= 500)
129
- throw new HouseRefusal('/', posted.status, text);
130
- if (!posted.ok)
131
- return { refused: refusalIn(text, posted.status) };
132
- const held = JSON.parse(text).transfer;
133
- return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
134
- };
135
- let mutations = null;
136
- const writes = async (tool) => {
137
- mutations ??= house
138
- .post('/kit/tools/mutations', {}, closed.signal)
139
- .then(({ tools }) => new Set(tools.filter((listed) => listed.writes).map((listed) => listed.name)));
115
+ let text;
140
116
  try {
141
- return (await mutations).has(tool);
117
+ text = await retrying(async () => {
118
+ const posted = await fetch(relayed(transfer.url), {
119
+ method: 'POST',
120
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
121
+ body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
122
+ signal: closed.signal,
123
+ });
124
+ const answer = await posted.text();
125
+ if (!posted.ok)
126
+ throw new HouseRefusal('/', posted.status, answer);
127
+ return answer;
128
+ }, closed.signal);
142
129
  }
143
130
  catch (error) {
144
- mutations = null;
145
- throw error;
131
+ if (!finalRefusal(error))
132
+ throw error;
133
+ return { refused: refusalIn(error.text, error.status) };
146
134
  }
135
+ const held = JSON.parse(text).transfer;
136
+ return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
147
137
  };
148
- const forward = async (path, text) => {
149
- if (path === '/') {
150
- const message = JSON.parse(text);
151
- const tool = message.method === 'tools/call' ? String(message.params.name) : null;
152
- return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
153
- }
154
- const answer = await fetch(relayed(new URL(path, origin)), {
155
- method: 'POST',
156
- headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
157
- body: versioned(conversationId, text),
158
- signal: closed.signal,
159
- });
160
- return { status: answer.status, text: await answer.text() };
138
+ const forward = (text) => {
139
+ const message = JSON.parse(text);
140
+ return send(message, message.method === 'tools/call' ? operationId() : null);
161
141
  };
162
142
  const caller = {
163
143
  key: `conversation:${conversationId}`,
@@ -179,11 +159,13 @@ export async function openBridge(house, copies, conversationId, signal) {
179
159
  const { cwd, commit } = JSON.parse(text);
180
160
  return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
181
161
  }
162
+ if (path === '/git/fetch')
163
+ return { status: 200, text: JSON.stringify(await copies.fetch()) };
182
164
  if (path === '/git/observed') {
183
165
  await copies.observed(JSON.parse(text));
184
166
  return { status: 200, text: '{}' };
185
167
  }
186
- return path === '/' || path === ATTACHMENTS ? forward(path, text) : { status: 404, text: '' };
168
+ return path === '/' ? forward(text) : { status: 404, text: '' };
187
169
  };
188
170
  const sockets = join(kitHome(), 'bridges');
189
171
  await mkdir(sockets, { recursive: true, mode: 0o700 });
@@ -203,7 +185,8 @@ export async function openBridge(house, copies, conversationId, signal) {
203
185
  response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
204
186
  return;
205
187
  }
206
- response.writeHead(502).end(request.url === '/git/push' && error instanceof Error ? error.message : UNREACHABLE);
188
+ const verb = request.url === '/git/push' || request.url === '/git/fetch';
189
+ response.writeHead(502).end(verb && error instanceof Error ? error.message : UNREACHABLE);
207
190
  }
208
191
  });
209
192
  bridge.listen(socketPath);
@@ -230,7 +213,7 @@ export async function openBridge(house, copies, conversationId, signal) {
230
213
  GIT_CONFIG_VALUE_0: app,
231
214
  },
232
215
  tool: async (name, args) => {
233
- const forwarded = await forward('/', JSON.stringify(call(name, args)));
216
+ const forwarded = await forward(JSON.stringify(call(name, args)));
234
217
  if (forwarded.status !== 200)
235
218
  throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
236
219
  const answer = JSON.parse(forwarded.text);
package/dist/clis.js CHANGED
@@ -1,48 +1,86 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
3
- import { readFile } from 'node:fs/promises';
4
- import { userInfo } from 'node:os';
5
- import { join } from 'node:path';
2
+ import { constants, readFileSync } from 'node:fs';
3
+ import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
+ import { homedir, userInfo } from 'node:os';
5
+ import { delimiter, isAbsolute, join } from 'node:path';
6
6
  import { kitHome } from './home.js';
7
- const RUNNING_JOB = new Set(['running', 'paused']);
8
- function threadStatus({ method, params }) {
9
- if (method !== 'session/update' || params?.update?.sessionUpdate !== 'session_info_update')
10
- return undefined;
11
- return params.update._meta?.codex?.threadStatus?.type;
12
- }
13
- function asyncTask({ method, params }) {
14
- const update = params?.update;
15
- if (method !== 'session/update' || update?.asyncTaskId === undefined)
16
- return null;
17
- if (update.sessionUpdate === 'async_task_spawned')
18
- return { id: update.asyncTaskId, running: true };
19
- if (update.sessionUpdate === 'async_task_state_update' && !RUNNING_JOB.has(update.state ?? '')) {
20
- return { id: update.asyncTaskId, running: false };
7
+ const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
8
+ function houseRule(houses) {
9
+ return `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`;
10
+ }
11
+ const RAW_SUBTYPES = [
12
+ 'background_tasks_changed',
13
+ 'task_started',
14
+ 'task_updated',
15
+ 'task_notification',
16
+ 'session_state_changed',
17
+ 'hook_started',
18
+ 'hook_response',
19
+ ];
20
+ async function executable(path) {
21
+ try {
22
+ await access(path, constants.X_OK);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
21
27
  }
22
- return null;
23
28
  }
24
- function airPhase({ _meta }) {
25
- const phase = _meta?.jetbrains?.air?.phase;
26
- if (phase === 'commentary')
27
- return 'note';
28
- if (phase === 'final_answer')
29
- return 'answer';
30
- return null;
29
+ async function listedHouses(file) {
30
+ let text;
31
+ try {
32
+ text = await readFile(file, 'utf8');
33
+ }
34
+ catch (error) {
35
+ if (error.code === 'ENOENT')
36
+ return [];
37
+ throw error;
38
+ }
39
+ try {
40
+ const listed = JSON.parse(HOUSE_EXECUTABLES.exec(text)?.[1] ?? '[]');
41
+ return Array.isArray(listed) ? listed.filter((house) => typeof house === 'string') : [];
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ }
47
+ export async function allowHouseInCodex(path) {
48
+ const candidates = [...new Set(path.split(delimiter).filter((directory) => isAbsolute(directory)))].map((directory) => join(directory, 'house'));
49
+ const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
50
+ const file = join(rules, 'house.rules');
51
+ const houses = [];
52
+ for (const house of [...(await listedHouses(file)), ...candidates]) {
53
+ if (!houses.includes(house) && (await executable(house)))
54
+ houses.push(house);
55
+ }
56
+ await mkdir(rules, { recursive: true });
57
+ const temporary = `${file}.${process.pid}`;
58
+ await writeFile(temporary, houseRule(houses));
59
+ await rename(temporary, file);
60
+ }
61
+ async function nothing() { }
62
+ function bridgeInClaudeSandbox(bridge) {
63
+ return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
31
64
  }
32
65
  export const CLIS = {
33
66
  'codex-acp': {
34
67
  bin: 'codex',
35
68
  minimum: '0.159.1',
36
69
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
37
- adapter: { package: '@agentclientprotocol/codex-acp', bin: 'codex-acp', executable: 'CODEX_PATH', env: {} },
70
+ adapter: {
71
+ package: '@agentclientprotocol/codex-acp',
72
+ bin: 'codex-acp',
73
+ executable: 'CODEX_PATH',
74
+ env: { CODEX_CONFIG: JSON.stringify({ features: { shell_zsh_fork: true } }) },
75
+ },
38
76
  args: [],
77
+ fullAccess: 'agent-full-access',
78
+ sessionMeta: () => ({}),
79
+ renames: true,
80
+ allowHouse: allowHouseInCodex,
39
81
  signedIn: { command: ['login', 'status'] },
40
82
  login: { args: ['login', '--device-auth'], code: 'show' },
41
- queues: false,
42
- turnStarted: (notice) => threadStatus(notice) === 'active',
43
- turnEnded: (notice) => threadStatus(notice) === 'idle',
44
- job: asyncTask,
45
- phase: airPhase,
83
+ air: ['asyncTasks', 'nativeSubagentSessions'],
46
84
  },
47
85
  'claude-agent-acp': {
48
86
  bin: 'claude',
@@ -52,45 +90,49 @@ export const CLIS = {
52
90
  package: '@agentclientprotocol/claude-agent-acp',
53
91
  bin: 'claude-agent-acp',
54
92
  executable: 'CLAUDE_CODE_EXECUTABLE',
55
- env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp' },
93
+ env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp', IS_SANDBOX: '1' },
56
94
  },
57
95
  args: [],
96
+ fullAccess: 'bypassPermissions',
97
+ sessionMeta: (bridge, title) => ({
98
+ claudeCode: {
99
+ options: {
100
+ allowedTools: ['Bash(house:*)'],
101
+ includeHookEvents: true,
102
+ title,
103
+ settings: { sandbox: { network: bridgeInClaudeSandbox(bridge) } },
104
+ },
105
+ emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
106
+ },
107
+ }),
108
+ renames: false,
109
+ allowHouse: nothing,
58
110
  signedIn: { command: ['auth', 'status'] },
59
111
  login: { args: ['auth', 'login', '--claudeai'], code: 'collect', rejected: 'Invalid code' },
60
- queues: false,
61
- turnStarted: () => false,
62
- turnEnded: ({ method, params }) => method === 'session/update' &&
63
- params?.update?.sessionUpdate === 'usage_update' &&
64
- params.update._meta?.['_claude/origin'] !== undefined,
65
- job: asyncTask,
66
- phase: () => null,
112
+ air: ['asyncTasks', 'sessionFailure'],
67
113
  },
68
114
  'grok-build': {
69
115
  bin: 'grok',
70
116
  minimum: '1.0.46',
71
117
  install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
72
118
  adapter: null,
73
- args: ['agent', '--no-leader', 'stdio'],
119
+ args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
120
+ fullAccess: null,
121
+ sessionMeta: () => ({}),
122
+ renames: false,
123
+ allowHouse: nothing,
74
124
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
75
125
  login: { args: ['login', '--device-auth'], code: 'show' },
76
- queues: true,
77
- turnStarted: ({ method, params }) => method === '_x.ai/queue/changed' && params?.runningPromptId !== undefined,
78
- turnEnded: ({ method, params }) => method === '_x.ai/session_notification' && params?.update?.sessionUpdate === 'turn_completed',
79
- job: ({ method, params }) => {
80
- if (method === '_x.ai/task_backgrounded')
81
- return { id: params.update.task_id, running: true };
82
- if (method === '_x.ai/task_completed')
83
- return { id: params.update.task_snapshot.task_id, running: false };
84
- return null;
85
- },
86
- phase: () => null,
126
+ air: ['asyncTasks'],
87
127
  },
88
128
  };
89
129
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
90
130
  export const KIT_VERSION = KIT_PACKAGE.version;
91
131
  const PROXY = /^https?_proxy$/i;
92
132
  const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
133
+ // An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
93
134
  const INSTALL_MS = 15 * 60_000;
135
+ // A login shell or a version read is ended after half a minute, so a profile that waits for input cannot stall the report.
94
136
  const READ_MS = 30_000;
95
137
  const CAUSE_CHARACTERS = 4000;
96
138
  function prefix(kind) {
@@ -118,8 +160,22 @@ export function run(command, args, timeoutMs, detached = false, signal) {
118
160
  timeout: timeoutMs,
119
161
  env: withoutProxy(),
120
162
  detached,
121
- signal,
163
+ signal: detached ? undefined : signal,
122
164
  });
165
+ const stop = () => {
166
+ try {
167
+ process.kill(-child.pid, 'SIGKILL');
168
+ }
169
+ catch { }
170
+ };
171
+ if (detached && signal?.aborted)
172
+ stop();
173
+ if (detached)
174
+ signal?.addEventListener('abort', stop, { once: true });
175
+ const ended = (ran) => {
176
+ signal?.removeEventListener('abort', stop);
177
+ resolve(ran);
178
+ };
123
179
  let stdout = '';
124
180
  let output = '';
125
181
  child.stdout.setEncoding('utf8').on('data', (chunk) => {
@@ -129,25 +185,25 @@ export function run(command, args, timeoutMs, detached = false, signal) {
129
185
  child.stderr.setEncoding('utf8').on('data', (chunk) => {
130
186
  output += chunk;
131
187
  });
132
- child.on('error', (error) => resolve({ status: null, stdout, output: error.message }));
133
- child.on('close', (status) => resolve({ status, stdout, output }));
188
+ child.on('error', (error) => ended({ status: null, stdout, output: error.message }));
189
+ child.on('close', (status) => ended({ status, stdout, output }));
134
190
  });
135
191
  }
136
192
  export function failureOf(ran, program) {
137
193
  return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
138
194
  }
139
- export async function installAdapter(kind) {
195
+ export async function installAdapter(kind, signal) {
140
196
  const adapter = CLIS[kind].adapter;
141
197
  if (adapter === null)
142
198
  return null;
143
199
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
144
200
  if ((await installedAdapter(kind)) === release)
145
201
  return null;
146
- const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS);
202
+ const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
147
203
  return ran.status === 0 ? null : failureOf(ran, 'npm');
148
204
  }
149
- export async function installCli(kind) {
150
- const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true);
205
+ export async function installCli(kind, signal) {
206
+ const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
151
207
  return ran.status === 0 ? null : failureOf(ran, 'the install');
152
208
  }
153
209
  function loginShell() {
@@ -156,11 +212,11 @@ function loginShell() {
156
212
  const FOUND = 'house-kit-cli ';
157
213
  const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
158
214
  const USER_BIN = 'case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) PATH="$PATH:$HOME/.local/bin" ;; esac; export PATH';
159
- function inLoginShell(script, argument = '') {
160
- return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true);
215
+ function inLoginShell(script, argument = '', signal) {
216
+ return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
161
217
  }
162
- export async function locate(kind) {
163
- const ran = await inLoginShell(LOOKUP, CLIS[kind].bin);
218
+ export async function locate(kind, signal) {
219
+ const ran = await inLoginShell(LOOKUP, CLIS[kind].bin, signal);
164
220
  return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
165
221
  }
166
222
  export async function loginPath() {
@@ -168,8 +224,8 @@ export async function loginPath() {
168
224
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
169
225
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
170
226
  }
171
- export async function versionOf(path) {
172
- return VERSION.exec((await run(path, ['--version'], READ_MS)).stdout)?.[0] ?? null;
227
+ export async function versionOf(path, signal) {
228
+ return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
173
229
  }
174
230
  function numbers(version) {
175
231
  return version.split(/[.-]/, 3).map(Number);
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { connect } from 'node:net';
4
+ const KIT_ANSWER = '"house-kit-';
5
+ const SYNC = 'house-kit/sync';
6
+ function lines(each) {
7
+ let buffer = '';
8
+ return (chunk) => {
9
+ buffer += chunk;
10
+ for (let index = buffer.indexOf('\n'); index >= 0; index = buffer.indexOf('\n')) {
11
+ each(buffer.slice(0, index + 1));
12
+ buffer = buffer.slice(index + 1);
13
+ }
14
+ };
15
+ }
16
+ function answersKit(line) {
17
+ if (!line.includes(KIT_ANSWER))
18
+ return false;
19
+ try {
20
+ const message = JSON.parse(line);
21
+ return message.method === undefined && typeof message.id === 'string' && message.id.startsWith(KIT_ANSWER.slice(1));
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ const args = process.argv.slice(2);
28
+ const serving = args[0] === 'app-server';
29
+ if (process.env.HOUSE_KIT_SANDBOX === '1' && serving)
30
+ args.push('--disable', 'memories');
31
+ const codex = spawn(process.env.HOUSE_KIT_CODEX, args, { stdio: ['pipe', 'pipe', 'inherit'] });
32
+ const stream = serving ? connect(process.env.HOUSE_KIT_CODEX_STREAM) : null;
33
+ stream?.on('error', () => undefined);
34
+ process.stdin.setEncoding('utf8').on('data', lines((line) => codex.stdin.write(line)));
35
+ process.stdin.on('end', () => codex.stdin.end());
36
+ stream?.setEncoding('utf8').on('data', lines((line) => {
37
+ const message = JSON.parse(line);
38
+ if (message.method === SYNC)
39
+ stream.write(`${JSON.stringify({ jsonrpc: '2.0', id: message.id, result: {} })}\n`);
40
+ else
41
+ codex.stdin.write(line);
42
+ }));
43
+ codex.stdout.setEncoding('utf8').on('data', lines((line) => {
44
+ stream?.write(line);
45
+ if (answersKit(line) || process.stdout.write(line))
46
+ return;
47
+ codex.stdout.pause();
48
+ process.stdout.once('drain', () => codex.stdout.resume());
49
+ }));
50
+ codex.stdin.on('error', () => undefined);
51
+ for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP'])
52
+ process.on(signal, () => codex.kill(signal));
53
+ codex.on('exit', (code, signal) => {
54
+ if (signal === null)
55
+ process.exit(code ?? 1);
56
+ process.removeAllListeners(signal);
57
+ process.kill(process.pid, signal);
58
+ });