@agentshouse/kit 0.2.1-c063e835a50ea → 0.2.1-c14f8446ba284
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/README.md +12 -13
- package/dist/acp.js +16 -93
- package/dist/agents.js +81 -39
- package/dist/api.js +30 -2
- package/dist/app-git.js +64 -0
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +32 -39
- package/dist/clis.js +95 -65
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +284 -236
- package/dist/copies.js +27 -35
- package/dist/end.js +8 -0
- package/dist/events.js +521 -0
- package/dist/files.js +14 -0
- package/dist/house-main.js +28 -10
- package/dist/instructions.js +2 -2
- package/dist/kit-main.js +3 -0
- package/dist/login.js +23 -98
- package/dist/logout.js +3 -0
- package/dist/parts.js +64 -0
- package/dist/refusals.js +12 -0
- package/dist/resident.js +12 -12
- package/dist/scope.js +203 -0
- package/dist/secret-input.js +4 -1
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +34 -9
- package/dist/skills.js +1 -1
- package/dist/stream.js +29 -4
- package/dist/transcription.js +54 -6
- package/dist/update.js +2 -0
- package/dist/work.js +348 -0
- package/package.json +1 -1
- package/dist/blocks.js +0 -51
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
|
|
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');
|
|
@@ -40,8 +39,6 @@ function refusalIn(text, status) {
|
|
|
40
39
|
return refusalOf(text) ?? { code: '', text: text === '' ? `House refused it with ${status}` : text, conflicts: [] };
|
|
41
40
|
}
|
|
42
41
|
function answered(forwarded) {
|
|
43
|
-
if (forwarded.status >= 500)
|
|
44
|
-
throw new HouseRefusal('/', forwarded.status, forwarded.text);
|
|
45
42
|
if (forwarded.status !== 200)
|
|
46
43
|
return { refused: refusalIn(forwarded.text, forwarded.status) };
|
|
47
44
|
const answer = JSON.parse(forwarded.text);
|
|
@@ -65,16 +62,11 @@ function pendingSeconds(forwarded) {
|
|
|
65
62
|
function resendAfter(forwarded, attempt) {
|
|
66
63
|
if (forwarded === null)
|
|
67
64
|
return retryDelay(attempt);
|
|
68
|
-
if (forwarded.status
|
|
69
|
-
return
|
|
65
|
+
if (forwarded.status !== 200)
|
|
66
|
+
return retryable(forwarded.text) === false ? null : retryDelay(attempt);
|
|
70
67
|
const seconds = pendingSeconds(forwarded);
|
|
71
68
|
return seconds === null ? null : seconds * 1000;
|
|
72
69
|
}
|
|
73
|
-
function versioned(conversationId, text) {
|
|
74
|
-
const file = JSON.parse(text);
|
|
75
|
-
const identity = JSON.stringify([conversationId, file.room_ref ?? null, file.name, file.sha256]);
|
|
76
|
-
return JSON.stringify({ ...file, version: uuidOf(createHash('sha256').update(identity).digest(), 8) });
|
|
77
|
-
}
|
|
78
70
|
function editAnswer(forwarded) {
|
|
79
71
|
const answer = answered(forwarded);
|
|
80
72
|
if ('refused' in answer)
|
|
@@ -105,6 +97,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
105
97
|
const after = resendAfter(forwarded, attempt);
|
|
106
98
|
if (after === null)
|
|
107
99
|
return forwarded;
|
|
100
|
+
// A write House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
|
|
108
101
|
await delay(after, undefined, { signal: closed.signal });
|
|
109
102
|
}
|
|
110
103
|
};
|
|
@@ -119,17 +112,26 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
119
112
|
if ('refused' in prepared)
|
|
120
113
|
return prepared;
|
|
121
114
|
const { transfer } = parse(prepared.text);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
115
|
+
let text;
|
|
116
|
+
try {
|
|
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);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (!finalRefusal(error))
|
|
132
|
+
throw error;
|
|
133
|
+
return { refused: refusalIn(error.text, error.status) };
|
|
134
|
+
}
|
|
133
135
|
const held = JSON.parse(text).transfer;
|
|
134
136
|
return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
|
|
135
137
|
};
|
|
@@ -146,19 +148,10 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
146
148
|
throw error;
|
|
147
149
|
}
|
|
148
150
|
};
|
|
149
|
-
const forward = async (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
|
|
154
|
-
}
|
|
155
|
-
const answer = await fetch(relayed(new URL(path, origin)), {
|
|
156
|
-
method: 'POST',
|
|
157
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
|
|
158
|
-
body: versioned(conversationId, text),
|
|
159
|
-
signal: closed.signal,
|
|
160
|
-
});
|
|
161
|
-
return { status: answer.status, text: await answer.text() };
|
|
151
|
+
const forward = async (text) => {
|
|
152
|
+
const message = JSON.parse(text);
|
|
153
|
+
const tool = message.method === 'tools/call' ? String(message.params.name) : null;
|
|
154
|
+
return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
|
|
162
155
|
};
|
|
163
156
|
const caller = {
|
|
164
157
|
key: `conversation:${conversationId}`,
|
|
@@ -184,7 +177,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
184
177
|
await copies.observed(JSON.parse(text));
|
|
185
178
|
return { status: 200, text: '{}' };
|
|
186
179
|
}
|
|
187
|
-
return path === '/'
|
|
180
|
+
return path === '/' ? forward(text) : { status: 404, text: '' };
|
|
188
181
|
};
|
|
189
182
|
const sockets = join(kitHome(), 'bridges');
|
|
190
183
|
await mkdir(sockets, { recursive: true, mode: 0o700 });
|
|
@@ -231,7 +224,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
231
224
|
GIT_CONFIG_VALUE_0: app,
|
|
232
225
|
},
|
|
233
226
|
tool: async (name, args) => {
|
|
234
|
-
const forwarded = await forward(
|
|
227
|
+
const forwarded = await forward(JSON.stringify(call(name, args)));
|
|
235
228
|
if (forwarded.status !== 200)
|
|
236
229
|
throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
|
|
237
230
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
133
|
-
child.on('close', (status) =>
|
|
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
|
+
});
|