@agentshouse/kit 0.2.1-cb6679d048e60 → 0.2.1-cc988e43abeeb

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,15 +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
+ import { relayed } from './relay.js';
16
17
  const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
17
- const ATTACHMENTS = '/kit/attachments/upload';
18
18
  export async function received(request) {
19
19
  let text = '';
20
20
  request.setEncoding('utf8');
@@ -22,7 +22,8 @@ export async function received(request) {
22
22
  text += chunk;
23
23
  return text;
24
24
  }
25
- function relay(request, response, target, authorization) {
25
+ function relay(request, response, house, authorization) {
26
+ const target = relayed(house);
26
27
  const send = target.protocol === 'https:' ? httpsRequest : httpRequest;
27
28
  const forwarded = send(target, {
28
29
  method: request.method,
@@ -38,8 +39,6 @@ function refusalIn(text, status) {
38
39
  return refusalOf(text) ?? { code: '', text: text === '' ? `House refused it with ${status}` : text, conflicts: [] };
39
40
  }
40
41
  function answered(forwarded) {
41
- if (forwarded.status >= 500)
42
- throw new HouseRefusal('/', forwarded.status, forwarded.text);
43
42
  if (forwarded.status !== 200)
44
43
  return { refused: refusalIn(forwarded.text, forwarded.status) };
45
44
  const answer = JSON.parse(forwarded.text);
@@ -63,16 +62,11 @@ function pendingSeconds(forwarded) {
63
62
  function resendAfter(forwarded, attempt) {
64
63
  if (forwarded === null)
65
64
  return retryDelay(attempt);
66
- if (forwarded.status >= 500)
67
- return refusalOf(forwarded.text) === null ? retryDelay(attempt) : null;
65
+ if (forwarded.status !== 200)
66
+ return retryable(forwarded.text) === false ? null : retryDelay(attempt);
68
67
  const seconds = pendingSeconds(forwarded);
69
68
  return seconds === null ? null : seconds * 1000;
70
69
  }
71
- function versioned(conversationId, text) {
72
- const file = JSON.parse(text);
73
- const identity = JSON.stringify([conversationId, file.room_ref ?? null, file.name, file.sha256]);
74
- return JSON.stringify({ ...file, version: uuidOf(createHash('sha256').update(identity).digest(), 8) });
75
- }
76
70
  function editAnswer(forwarded) {
77
71
  const answer = answered(forwarded);
78
72
  if ('refused' in answer)
@@ -103,6 +97,7 @@ export async function openBridge(house, copies, conversationId, signal) {
103
97
  const after = resendAfter(forwarded, attempt);
104
98
  if (after === null)
105
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.
106
101
  await delay(after, undefined, { signal: closed.signal });
107
102
  }
108
103
  };
@@ -117,46 +112,32 @@ export async function openBridge(house, copies, conversationId, signal) {
117
112
  if ('refused' in prepared)
118
113
  return prepared;
119
114
  const { transfer } = parse(prepared.text);
120
- const posted = await fetch(transfer.url, {
121
- method: 'POST',
122
- headers: { 'content-type': 'text/plain; charset=utf-8' },
123
- body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
124
- signal: closed.signal,
125
- });
126
- const text = await posted.text();
127
- if (posted.status >= 500)
128
- throw new HouseRefusal('/', posted.status, text);
129
- if (!posted.ok)
130
- return { refused: refusalIn(text, posted.status) };
131
- const held = JSON.parse(text).transfer;
132
- return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
133
- };
134
- let mutations = null;
135
- const writes = async (tool) => {
136
- mutations ??= house
137
- .post('/kit/tools/mutations', {}, closed.signal)
138
- .then(({ tools }) => new Set(tools.filter((listed) => listed.writes).map((listed) => listed.name)));
115
+ let text;
139
116
  try {
140
- 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);
141
129
  }
142
130
  catch (error) {
143
- mutations = null;
144
- throw error;
131
+ if (!finalRefusal(error))
132
+ throw error;
133
+ return { refused: refusalIn(error.text, error.status) };
145
134
  }
135
+ const held = JSON.parse(text).transfer;
136
+ return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
146
137
  };
147
- const forward = async (path, text) => {
148
- if (path === '/') {
149
- const message = JSON.parse(text);
150
- const tool = message.method === 'tools/call' ? String(message.params.name) : null;
151
- return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
152
- }
153
- const answer = await fetch(new URL(path, origin), {
154
- method: 'POST',
155
- headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
156
- body: versioned(conversationId, text),
157
- signal: closed.signal,
158
- });
159
- 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);
160
141
  };
161
142
  const caller = {
162
143
  key: `conversation:${conversationId}`,
@@ -178,11 +159,13 @@ export async function openBridge(house, copies, conversationId, signal) {
178
159
  const { cwd, commit } = JSON.parse(text);
179
160
  return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
180
161
  }
162
+ if (path === '/git/fetch')
163
+ return { status: 200, text: JSON.stringify(await copies.fetch()) };
181
164
  if (path === '/git/observed') {
182
165
  await copies.observed(JSON.parse(text));
183
166
  return { status: 200, text: '{}' };
184
167
  }
185
- return path === '/' || path === ATTACHMENTS ? forward(path, text) : { status: 404, text: '' };
168
+ return path === '/' ? forward(text) : { status: 404, text: '' };
186
169
  };
187
170
  const sockets = join(kitHome(), 'bridges');
188
171
  await mkdir(sockets, { recursive: true, mode: 0o700 });
@@ -202,7 +185,8 @@ export async function openBridge(house, copies, conversationId, signal) {
202
185
  response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
203
186
  return;
204
187
  }
205
- 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);
206
190
  }
207
191
  });
208
192
  bridge.listen(socketPath);
@@ -229,7 +213,7 @@ export async function openBridge(house, copies, conversationId, signal) {
229
213
  GIT_CONFIG_VALUE_0: app,
230
214
  },
231
215
  tool: async (name, args) => {
232
- const forwarded = await forward('/', JSON.stringify(call(name, args)));
216
+ const forwarded = await forward(JSON.stringify(call(name, args)));
233
217
  if (forwarded.status !== 200)
234
218
  throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
235
219
  const answer = JSON.parse(forwarded.text);
package/dist/clis.js CHANGED
@@ -1,48 +1,60 @@
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, 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 RAW_SUBTYPES = [
8
+ 'background_tasks_changed',
9
+ 'task_started',
10
+ 'task_updated',
11
+ 'task_notification',
12
+ 'session_state_changed',
13
+ 'hook_started',
14
+ 'hook_response',
15
+ ];
16
+ async function executable(path) {
17
+ try {
18
+ await access(path, constants.X_OK);
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
21
23
  }
22
- return null;
23
24
  }
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;
25
+ async function allowHouseInCodex(path) {
26
+ const candidates = [...new Set(path.split(delimiter).filter((directory) => isAbsolute(directory)))].map((directory) => join(directory, 'house'));
27
+ const houses = [];
28
+ for (const candidate of candidates)
29
+ if (await executable(candidate))
30
+ houses.push(candidate);
31
+ const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
32
+ await mkdir(rules, { recursive: true });
33
+ await writeFile(join(rules, 'house.rules'), `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`);
34
+ }
35
+ async function nothing() { }
36
+ function bridgeInClaudeSandbox(bridge) {
37
+ return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
31
38
  }
32
39
  export const CLIS = {
33
40
  'codex-acp': {
34
41
  bin: 'codex',
35
42
  minimum: '0.159.1',
36
43
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
37
- adapter: { package: '@agentclientprotocol/codex-acp', bin: 'codex-acp', executable: 'CODEX_PATH', env: {} },
44
+ adapter: {
45
+ package: '@agentclientprotocol/codex-acp',
46
+ bin: 'codex-acp',
47
+ executable: 'CODEX_PATH',
48
+ env: { CODEX_CONFIG: JSON.stringify({ features: { shell_zsh_fork: true } }) },
49
+ },
38
50
  args: [],
51
+ fullAccess: 'agent-full-access',
52
+ sessionMeta: () => ({}),
53
+ renames: true,
54
+ allowHouse: allowHouseInCodex,
39
55
  signedIn: { command: ['login', 'status'] },
40
56
  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,
57
+ air: ['asyncTasks', 'nativeSubagentSessions'],
46
58
  },
47
59
  'claude-agent-acp': {
48
60
  bin: 'claude',
@@ -52,45 +64,49 @@ export const CLIS = {
52
64
  package: '@agentclientprotocol/claude-agent-acp',
53
65
  bin: 'claude-agent-acp',
54
66
  executable: 'CLAUDE_CODE_EXECUTABLE',
55
- env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp' },
67
+ env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp', IS_SANDBOX: '1' },
56
68
  },
57
69
  args: [],
70
+ fullAccess: 'bypassPermissions',
71
+ sessionMeta: (bridge, title) => ({
72
+ claudeCode: {
73
+ options: {
74
+ allowedTools: ['Bash(house:*)'],
75
+ includeHookEvents: true,
76
+ title,
77
+ settings: { sandbox: { network: bridgeInClaudeSandbox(bridge) } },
78
+ },
79
+ emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
80
+ },
81
+ }),
82
+ renames: false,
83
+ allowHouse: nothing,
58
84
  signedIn: { command: ['auth', 'status'] },
59
85
  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,
86
+ air: ['asyncTasks', 'sessionFailure'],
67
87
  },
68
88
  'grok-build': {
69
89
  bin: 'grok',
70
90
  minimum: '1.0.46',
71
91
  install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
72
92
  adapter: null,
73
- args: ['agent', '--no-leader', 'stdio'],
93
+ args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
94
+ fullAccess: null,
95
+ sessionMeta: () => ({}),
96
+ renames: false,
97
+ allowHouse: nothing,
74
98
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
75
99
  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,
100
+ air: ['asyncTasks'],
87
101
  },
88
102
  };
89
103
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
90
104
  export const KIT_VERSION = KIT_PACKAGE.version;
91
105
  const PROXY = /^https?_proxy$/i;
92
106
  const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
107
+ // An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
93
108
  const INSTALL_MS = 15 * 60_000;
109
+ // 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
110
  const READ_MS = 30_000;
95
111
  const CAUSE_CHARACTERS = 4000;
96
112
  function prefix(kind) {
@@ -118,8 +134,22 @@ export function run(command, args, timeoutMs, detached = false, signal) {
118
134
  timeout: timeoutMs,
119
135
  env: withoutProxy(),
120
136
  detached,
121
- signal,
137
+ signal: detached ? undefined : signal,
122
138
  });
139
+ const stop = () => {
140
+ try {
141
+ process.kill(-child.pid, 'SIGKILL');
142
+ }
143
+ catch { }
144
+ };
145
+ if (detached && signal?.aborted)
146
+ stop();
147
+ if (detached)
148
+ signal?.addEventListener('abort', stop, { once: true });
149
+ const ended = (ran) => {
150
+ signal?.removeEventListener('abort', stop);
151
+ resolve(ran);
152
+ };
123
153
  let stdout = '';
124
154
  let output = '';
125
155
  child.stdout.setEncoding('utf8').on('data', (chunk) => {
@@ -129,25 +159,25 @@ export function run(command, args, timeoutMs, detached = false, signal) {
129
159
  child.stderr.setEncoding('utf8').on('data', (chunk) => {
130
160
  output += chunk;
131
161
  });
132
- child.on('error', (error) => resolve({ status: null, stdout, output: error.message }));
133
- child.on('close', (status) => resolve({ status, stdout, output }));
162
+ child.on('error', (error) => ended({ status: null, stdout, output: error.message }));
163
+ child.on('close', (status) => ended({ status, stdout, output }));
134
164
  });
135
165
  }
136
166
  export function failureOf(ran, program) {
137
167
  return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
138
168
  }
139
- export async function installAdapter(kind) {
169
+ export async function installAdapter(kind, signal) {
140
170
  const adapter = CLIS[kind].adapter;
141
171
  if (adapter === null)
142
172
  return null;
143
173
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
144
174
  if ((await installedAdapter(kind)) === release)
145
175
  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);
176
+ 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
177
  return ran.status === 0 ? null : failureOf(ran, 'npm');
148
178
  }
149
- export async function installCli(kind) {
150
- const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true);
179
+ export async function installCli(kind, signal) {
180
+ const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
151
181
  return ran.status === 0 ? null : failureOf(ran, 'the install');
152
182
  }
153
183
  function loginShell() {
@@ -156,11 +186,11 @@ function loginShell() {
156
186
  const FOUND = 'house-kit-cli ';
157
187
  const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
158
188
  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);
189
+ function inLoginShell(script, argument = '', signal) {
190
+ return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
161
191
  }
162
- export async function locate(kind) {
163
- const ran = await inLoginShell(LOOKUP, CLIS[kind].bin);
192
+ export async function locate(kind, signal) {
193
+ const ran = await inLoginShell(LOOKUP, CLIS[kind].bin, signal);
164
194
  return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
165
195
  }
166
196
  export async function loginPath() {
@@ -168,8 +198,8 @@ export async function loginPath() {
168
198
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
169
199
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
170
200
  }
171
- export async function versionOf(path) {
172
- return VERSION.exec((await run(path, ['--version'], READ_MS)).stdout)?.[0] ?? null;
201
+ export async function versionOf(path, signal) {
202
+ return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
173
203
  }
174
204
  function numbers(version) {
175
205
  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
+ });