@agentshouse/kit 0.2.1-ce145e9aadbf6 → 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/README.md +26 -26
- package/dist/acp.js +16 -93
- package/dist/agents.js +53 -30
- package/dist/api.js +30 -2
- package/dist/app-git.js +64 -0
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +33 -51
- package/dist/clis.js +111 -70
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +284 -236
- package/dist/copies.js +130 -107
- package/dist/end.js +18 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +521 -0
- package/dist/files.js +14 -0
- package/dist/home.js +41 -3
- package/dist/house-main.js +79 -32
- package/dist/instructions.js +7 -5
- package/dist/kit-main.js +3 -0
- package/dist/login.js +99 -102
- package/dist/logout.js +16 -13
- package/dist/parts.js +82 -0
- package/dist/refusals.js +12 -0
- package/dist/relay.js +12 -2
- package/dist/resident.js +30 -23
- package/dist/scope.js +203 -0
- package/dist/secret-input.js +3 -2
- package/dist/service-main.js +9 -0
- package/dist/service.js +33 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +34 -10
- package/dist/stream.js +31 -4
- package/dist/transcription.js +54 -6
- package/dist/update.js +2 -0
- package/dist/work.js +348 -0
- package/package.json +2 -2
- 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 Tool call 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,46 +112,32 @@ 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
|
-
method: 'POST',
|
|
124
|
-
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
125
|
-
body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
|
|
126
|
-
signal: closed.signal,
|
|
127
|
-
});
|
|
128
|
-
const text = await posted.text();
|
|
129
|
-
if (posted.status >= 500)
|
|
130
|
-
throw new HouseRefusal('/', posted.status, text);
|
|
131
|
-
if (!posted.ok)
|
|
132
|
-
return { refused: refusalIn(text, posted.status) };
|
|
133
|
-
const held = JSON.parse(text).transfer;
|
|
134
|
-
return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
|
|
135
|
-
};
|
|
136
|
-
let mutations = null;
|
|
137
|
-
const writes = async (tool) => {
|
|
138
|
-
mutations ??= house
|
|
139
|
-
.post('/kit/tools/mutations', {}, closed.signal)
|
|
140
|
-
.then(({ tools }) => new Set(tools.filter((listed) => listed.writes).map((listed) => listed.name)));
|
|
115
|
+
let text;
|
|
141
116
|
try {
|
|
142
|
-
|
|
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);
|
|
143
129
|
}
|
|
144
130
|
catch (error) {
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
if (!finalRefusal(error))
|
|
132
|
+
throw error;
|
|
133
|
+
return { refused: refusalIn(error.text, error.status) };
|
|
147
134
|
}
|
|
135
|
+
const held = JSON.parse(text).transfer;
|
|
136
|
+
return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
|
|
148
137
|
};
|
|
149
|
-
const forward =
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const tool = message.method === 'tools/call' ? String(message.params.name) : null;
|
|
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() };
|
|
138
|
+
const forward = (text) => {
|
|
139
|
+
const message = JSON.parse(text);
|
|
140
|
+
return send(message, message.method === 'tools/call' ? operationId() : null);
|
|
162
141
|
};
|
|
163
142
|
const caller = {
|
|
164
143
|
key: `conversation:${conversationId}`,
|
|
@@ -180,11 +159,13 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
180
159
|
const { cwd, commit } = JSON.parse(text);
|
|
181
160
|
return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
|
|
182
161
|
}
|
|
162
|
+
if (path === '/git/fetch')
|
|
163
|
+
return { status: 200, text: JSON.stringify(await copies.fetch()) };
|
|
183
164
|
if (path === '/git/observed') {
|
|
184
165
|
await copies.observed(JSON.parse(text));
|
|
185
166
|
return { status: 200, text: '{}' };
|
|
186
167
|
}
|
|
187
|
-
return path === '/'
|
|
168
|
+
return path === '/' ? forward(text) : { status: 404, text: '' };
|
|
188
169
|
};
|
|
189
170
|
const sockets = join(kitHome(), 'bridges');
|
|
190
171
|
await mkdir(sockets, { recursive: true, mode: 0o700 });
|
|
@@ -204,7 +185,8 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
204
185
|
response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
|
|
205
186
|
return;
|
|
206
187
|
}
|
|
207
|
-
|
|
188
|
+
const verb = request.url === '/git/push' || request.url === '/git/fetch';
|
|
189
|
+
response.writeHead(502).end(verb && error instanceof Error ? error.message : UNREACHABLE);
|
|
208
190
|
}
|
|
209
191
|
});
|
|
210
192
|
bridge.listen(socketPath);
|
|
@@ -231,7 +213,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
231
213
|
GIT_CONFIG_VALUE_0: app,
|
|
232
214
|
},
|
|
233
215
|
tool: async (name, args) => {
|
|
234
|
-
const forwarded = await forward(
|
|
216
|
+
const forwarded = await forward(JSON.stringify(call(name, args)));
|
|
235
217
|
if (forwarded.status !== 200)
|
|
236
218
|
throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
|
|
237
219
|
const answer = JSON.parse(forwarded.text);
|
package/dist/clis.js
CHANGED
|
@@ -1,57 +1,86 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { constants, readFileSync } from 'node:fs';
|
|
3
|
+
import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { homedir, userInfo } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
5
|
+
import { delimiter, isAbsolute, join } from 'node:path';
|
|
6
6
|
import { kitHome } from './home.js';
|
|
7
|
-
const
|
|
8
|
-
|
|
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;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
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'));
|
|
9
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
|
+
}
|
|
10
56
|
await mkdir(rules, { recursive: true });
|
|
11
|
-
|
|
57
|
+
const temporary = `${file}.${process.pid}`;
|
|
58
|
+
await writeFile(temporary, houseRule(houses));
|
|
59
|
+
await rename(temporary, file);
|
|
12
60
|
}
|
|
13
61
|
async function nothing() { }
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
return undefined;
|
|
17
|
-
return params.update._meta?.codex?.threadStatus?.type;
|
|
18
|
-
}
|
|
19
|
-
function asyncTask({ method, params }) {
|
|
20
|
-
const update = params?.update;
|
|
21
|
-
if (method !== 'session/update' || update?.asyncTaskId === undefined)
|
|
22
|
-
return null;
|
|
23
|
-
if (update.sessionUpdate === 'async_task_spawned')
|
|
24
|
-
return { id: update.asyncTaskId, running: true };
|
|
25
|
-
if (update.sessionUpdate === 'async_task_state_update' && !RUNNING_JOB.has(update.state ?? '')) {
|
|
26
|
-
return { id: update.asyncTaskId, running: false };
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
function airPhase({ _meta }) {
|
|
31
|
-
const phase = _meta?.jetbrains?.air?.phase;
|
|
32
|
-
if (phase === 'commentary')
|
|
33
|
-
return 'note';
|
|
34
|
-
if (phase === 'final_answer')
|
|
35
|
-
return 'answer';
|
|
36
|
-
return null;
|
|
62
|
+
function bridgeInClaudeSandbox(bridge) {
|
|
63
|
+
return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
|
|
37
64
|
}
|
|
38
65
|
export const CLIS = {
|
|
39
66
|
'codex-acp': {
|
|
40
67
|
bin: 'codex',
|
|
41
68
|
minimum: '0.159.1',
|
|
42
69
|
install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
|
|
43
|
-
adapter: {
|
|
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
|
+
},
|
|
44
76
|
args: [],
|
|
45
77
|
fullAccess: 'agent-full-access',
|
|
46
|
-
sessionMeta: {},
|
|
78
|
+
sessionMeta: () => ({}),
|
|
79
|
+
renames: true,
|
|
47
80
|
allowHouse: allowHouseInCodex,
|
|
48
81
|
signedIn: { command: ['login', 'status'] },
|
|
49
82
|
login: { args: ['login', '--device-auth'], code: 'show' },
|
|
50
|
-
|
|
51
|
-
turnStarted: (notice) => threadStatus(notice) === 'active',
|
|
52
|
-
turnEnded: (notice) => threadStatus(notice) === 'idle',
|
|
53
|
-
job: asyncTask,
|
|
54
|
-
phase: airPhase,
|
|
83
|
+
air: ['asyncTasks', 'nativeSubagentSessions'],
|
|
55
84
|
},
|
|
56
85
|
'claude-agent-acp': {
|
|
57
86
|
bin: 'claude',
|
|
@@ -65,47 +94,45 @@ export const CLIS = {
|
|
|
65
94
|
},
|
|
66
95
|
args: [],
|
|
67
96
|
fullAccess: 'bypassPermissions',
|
|
68
|
-
sessionMeta:
|
|
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,
|
|
69
109
|
allowHouse: nothing,
|
|
70
110
|
signedIn: { command: ['auth', 'status'] },
|
|
71
111
|
login: { args: ['auth', 'login', '--claudeai'], code: 'collect', rejected: 'Invalid code' },
|
|
72
|
-
|
|
73
|
-
turnStarted: () => false,
|
|
74
|
-
turnEnded: ({ method, params }) => method === 'session/update' &&
|
|
75
|
-
params?.update?.sessionUpdate === 'usage_update' &&
|
|
76
|
-
params.update._meta?.['_claude/origin'] !== undefined,
|
|
77
|
-
job: asyncTask,
|
|
78
|
-
phase: () => null,
|
|
112
|
+
air: ['asyncTasks', 'sessionFailure'],
|
|
79
113
|
},
|
|
80
114
|
'grok-build': {
|
|
81
115
|
bin: 'grok',
|
|
82
116
|
minimum: '1.0.46',
|
|
83
117
|
install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
|
|
84
118
|
adapter: null,
|
|
85
|
-
args: ['agent', '--always-approve', '--no-leader', 'stdio'],
|
|
119
|
+
args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
|
|
86
120
|
fullAccess: null,
|
|
87
|
-
sessionMeta: {},
|
|
121
|
+
sessionMeta: () => ({}),
|
|
122
|
+
renames: false,
|
|
88
123
|
allowHouse: nothing,
|
|
89
124
|
signedIn: { initializeMeta: 'defaultAuthMethodId' },
|
|
90
125
|
login: { args: ['login', '--device-auth'], code: 'show' },
|
|
91
|
-
|
|
92
|
-
turnStarted: ({ method, params }) => method === '_x.ai/queue/changed' && params?.runningPromptId !== undefined,
|
|
93
|
-
turnEnded: ({ method, params }) => method === '_x.ai/session_notification' && params?.update?.sessionUpdate === 'turn_completed',
|
|
94
|
-
job: ({ method, params }) => {
|
|
95
|
-
if (method === '_x.ai/task_backgrounded')
|
|
96
|
-
return { id: params.update.task_id, running: true };
|
|
97
|
-
if (method === '_x.ai/task_completed')
|
|
98
|
-
return { id: params.update.task_snapshot.task_id, running: false };
|
|
99
|
-
return null;
|
|
100
|
-
},
|
|
101
|
-
phase: () => null,
|
|
126
|
+
air: ['asyncTasks'],
|
|
102
127
|
},
|
|
103
128
|
};
|
|
104
129
|
const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
105
130
|
export const KIT_VERSION = KIT_PACKAGE.version;
|
|
106
131
|
const PROXY = /^https?_proxy$/i;
|
|
107
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.
|
|
108
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.
|
|
109
136
|
const READ_MS = 30_000;
|
|
110
137
|
const CAUSE_CHARACTERS = 4000;
|
|
111
138
|
function prefix(kind) {
|
|
@@ -133,8 +160,22 @@ export function run(command, args, timeoutMs, detached = false, signal) {
|
|
|
133
160
|
timeout: timeoutMs,
|
|
134
161
|
env: withoutProxy(),
|
|
135
162
|
detached,
|
|
136
|
-
signal,
|
|
163
|
+
signal: detached ? undefined : signal,
|
|
137
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
|
+
};
|
|
138
179
|
let stdout = '';
|
|
139
180
|
let output = '';
|
|
140
181
|
child.stdout.setEncoding('utf8').on('data', (chunk) => {
|
|
@@ -144,25 +185,25 @@ export function run(command, args, timeoutMs, detached = false, signal) {
|
|
|
144
185
|
child.stderr.setEncoding('utf8').on('data', (chunk) => {
|
|
145
186
|
output += chunk;
|
|
146
187
|
});
|
|
147
|
-
child.on('error', (error) =>
|
|
148
|
-
child.on('close', (status) =>
|
|
188
|
+
child.on('error', (error) => ended({ status: null, stdout, output: error.message }));
|
|
189
|
+
child.on('close', (status) => ended({ status, stdout, output }));
|
|
149
190
|
});
|
|
150
191
|
}
|
|
151
192
|
export function failureOf(ran, program) {
|
|
152
193
|
return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
|
|
153
194
|
}
|
|
154
|
-
export async function installAdapter(kind) {
|
|
195
|
+
export async function installAdapter(kind, signal) {
|
|
155
196
|
const adapter = CLIS[kind].adapter;
|
|
156
197
|
if (adapter === null)
|
|
157
198
|
return null;
|
|
158
199
|
const release = KIT_PACKAGE.peerDependencies[adapter.package];
|
|
159
200
|
if ((await installedAdapter(kind)) === release)
|
|
160
201
|
return null;
|
|
161
|
-
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);
|
|
162
203
|
return ran.status === 0 ? null : failureOf(ran, 'npm');
|
|
163
204
|
}
|
|
164
|
-
export async function installCli(kind) {
|
|
165
|
-
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);
|
|
166
207
|
return ran.status === 0 ? null : failureOf(ran, 'the install');
|
|
167
208
|
}
|
|
168
209
|
function loginShell() {
|
|
@@ -171,11 +212,11 @@ function loginShell() {
|
|
|
171
212
|
const FOUND = 'house-kit-cli ';
|
|
172
213
|
const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
|
|
173
214
|
const USER_BIN = 'case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) PATH="$PATH:$HOME/.local/bin" ;; esac; export PATH';
|
|
174
|
-
function inLoginShell(script, argument = '') {
|
|
175
|
-
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);
|
|
176
217
|
}
|
|
177
|
-
export async function locate(kind) {
|
|
178
|
-
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);
|
|
179
220
|
return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
|
|
180
221
|
}
|
|
181
222
|
export async function loginPath() {
|
|
@@ -183,8 +224,8 @@ export async function loginPath() {
|
|
|
183
224
|
const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
|
|
184
225
|
return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
|
|
185
226
|
}
|
|
186
|
-
export async function versionOf(path) {
|
|
187
|
-
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;
|
|
188
229
|
}
|
|
189
230
|
function numbers(version) {
|
|
190
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
|
+
});
|