@agentshouse/kit 0.2.1-c15804a0fa28d → 0.2.1-c1873657edab7
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 +42 -41
- package/dist/acp.js +48 -105
- package/dist/agents.js +180 -51
- package/dist/api.js +34 -2
- package/dist/app-git.js +48 -0
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +36 -57
- package/dist/calls.js +63 -0
- package/dist/chatgpt.js +32 -0
- package/dist/clis.js +138 -66
- package/dist/codex-main.js +58 -0
- package/dist/connection.js +125 -0
- package/dist/conversations.js +621 -323
- package/dist/copies.js +188 -128
- package/dist/end.js +27 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +525 -0
- package/dist/files.js +123 -17
- package/dist/git.js +11 -9
- package/dist/home.js +96 -7
- package/dist/house-cli.js +286 -0
- package/dist/house-main.js +7 -159
- package/dist/instructions.js +7 -9
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -19
- package/dist/login.js +117 -104
- package/dist/logout.js +14 -13
- package/dist/mcp.js +4 -2
- package/dist/parts.js +82 -0
- package/dist/placement.js +39 -0
- package/dist/refusals.js +12 -0
- package/dist/relay.js +12 -2
- package/dist/resident.js +106 -50
- package/dist/scope.js +206 -0
- package/dist/secret-input.js +4 -1
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +37 -30
- package/dist/skills.js +20 -32
- package/dist/transcription.js +100 -21
- package/dist/translate.js +2 -8
- package/dist/update.js +71 -50
- package/dist/wire.js +22 -0
- package/dist/work.js +346 -0
- package/package.json +6 -6
- package/dist/blocks.js +0 -51
- package/dist/stream.js +0 -47
- package/how-we-work.md +0 -23
- package/skills/ask-matt/LICENSE +0 -21
- package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
- package/skills/ask-matt/SKILL.md +0 -91
- package/skills/ask-matt/agents/openai.yaml +0 -4
- package/skills/code-review/LICENSE +0 -21
- package/skills/code-review/SKILL.md +0 -89
- package/skills/code-review/agents/openai.yaml +0 -3
- package/skills/codebase-design/DEEPENING.md +0 -37
- package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
- package/skills/codebase-design/LICENSE +0 -21
- package/skills/codebase-design/SKILL.md +0 -116
- package/skills/codebase-design/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/LICENSE +0 -21
- package/skills/diagnosing-bugs/SKILL.md +0 -140
- package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
- package/skills/domain-modeling/ADR-FORMAT.md +0 -47
- package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
- package/skills/domain-modeling/LICENSE +0 -21
- package/skills/domain-modeling/SKILL.md +0 -76
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/LICENSE +0 -21
- package/skills/grill-me/SKILL.md +0 -8
- package/skills/grill-me/agents/openai.yaml +0 -4
- package/skills/grill-with-docs/LICENSE +0 -21
- package/skills/grill-with-docs/SKILL.md +0 -8
- package/skills/grill-with-docs/agents/openai.yaml +0 -4
- package/skills/grilling/LICENSE +0 -21
- package/skills/grilling/SKILL.md +0 -30
- package/skills/grilling/agents/openai.yaml +0 -3
- package/skills/handoff/LICENSE +0 -21
- package/skills/handoff/SKILL.md +0 -17
- package/skills/handoff/agents/openai.yaml +0 -4
- package/skills/implement/LICENSE +0 -21
- package/skills/implement/SKILL.md +0 -16
- package/skills/implement/agents/openai.yaml +0 -4
- package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
- package/skills/improve-codebase-architecture/LICENSE +0 -21
- package/skills/improve-codebase-architecture/SKILL.md +0 -72
- package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
- package/skills/prototype/LICENSE +0 -21
- package/skills/prototype/LOGIC.md +0 -67
- package/skills/prototype/SKILL.md +0 -28
- package/skills/prototype/UI.md +0 -112
- package/skills/prototype/agents/openai.yaml +0 -3
- package/skills/research/LICENSE +0 -21
- package/skills/research/SKILL.md +0 -14
- package/skills/research/agents/openai.yaml +0 -3
- package/skills/resolving-merge-conflicts/LICENSE +0 -21
- package/skills/resolving-merge-conflicts/SKILL.md +0 -16
- package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
- package/skills/tdd/LICENSE +0 -21
- package/skills/tdd/SKILL.md +0 -40
- package/skills/tdd/agents/openai.yaml +0 -3
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/tests.md +0 -77
- package/skills/teach/GLOSSARY-FORMAT.md +0 -35
- package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
- package/skills/teach/LICENSE +0 -21
- package/skills/teach/MISSION-FORMAT.md +0 -31
- package/skills/teach/RESOURCES-FORMAT.md +0 -32
- package/skills/teach/SKILL.md +0 -141
- package/skills/teach/agents/openai.yaml +0 -4
- package/skills/to-questionnaire/LICENSE +0 -21
- package/skills/to-questionnaire/SKILL.md +0 -55
- package/skills/to-questionnaire/agents/openai.yaml +0 -4
- package/skills/to-spec/LICENSE +0 -21
- package/skills/to-spec/SKILL.md +0 -76
- package/skills/to-spec/agents/openai.yaml +0 -4
- package/skills/to-tickets/LICENSE +0 -21
- package/skills/to-tickets/SKILL.md +0 -106
- package/skills/to-tickets/agents/openai.yaml +0 -4
- package/skills/wait-what/LICENSE +0 -21
- package/skills/wait-what/SKILL.md +0 -8
- package/skills/wait-what/agents/openai.yaml +0 -4
- package/skills/wayfinder/LICENSE +0 -21
- package/skills/wayfinder/SKILL.md +0 -129
- package/skills/wayfinder/agents/openai.yaml +0 -4
- package/skills/writing-for-agents/LICENSE +0 -21
- package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
- package/skills/writing-for-agents/SKILL.md +0 -83
- package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/bridge.js
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { once } from 'node:events';
|
|
3
3
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
-
import { mkdir } from 'node:fs/promises';
|
|
5
4
|
import { createServer, request as httpRequest } from 'node:http';
|
|
6
5
|
import { request as httpsRequest } from 'node:https';
|
|
7
6
|
import { delimiter, dirname, join } from 'node:path';
|
|
8
7
|
import { parse } from 'yaml';
|
|
9
|
-
import { HouseRefusal, retryDelay } from './api.js';
|
|
8
|
+
import { finalRefusal, HouseRefusal, retryDelay, retrying } from './api.js';
|
|
10
9
|
import { loginPath } from './clis.js';
|
|
11
10
|
import { changedPaths, } from './copies.js';
|
|
12
|
-
import { kitHome, readEnrolment } from './home.js';
|
|
11
|
+
import { kitHome, readEnrolment, socketDirectory } from './home.js';
|
|
13
12
|
import { callHouse, mcpBody, rpc, UNREACHABLE } from './mcp.js';
|
|
14
|
-
import { operationId
|
|
15
|
-
import { refusalOf } from './refusals.js';
|
|
13
|
+
import { operationId } from './operation.js';
|
|
14
|
+
import { refusalOf, retryable } from './refusals.js';
|
|
16
15
|
import { relayed } from './relay.js';
|
|
17
16
|
const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
18
|
-
const ATTACHMENTS = '/kit/attachments/upload';
|
|
19
17
|
export async function received(request) {
|
|
20
18
|
let text = '';
|
|
21
19
|
request.setEncoding('utf8');
|
|
@@ -40,8 +38,6 @@ function refusalIn(text, status) {
|
|
|
40
38
|
return refusalOf(text) ?? { code: '', text: text === '' ? `House refused it with ${status}` : text, conflicts: [] };
|
|
41
39
|
}
|
|
42
40
|
function answered(forwarded) {
|
|
43
|
-
if (forwarded.status >= 500)
|
|
44
|
-
throw new HouseRefusal('/', forwarded.status, forwarded.text);
|
|
45
41
|
if (forwarded.status !== 200)
|
|
46
42
|
return { refused: refusalIn(forwarded.text, forwarded.status) };
|
|
47
43
|
const answer = JSON.parse(forwarded.text);
|
|
@@ -65,16 +61,11 @@ function pendingSeconds(forwarded) {
|
|
|
65
61
|
function resendAfter(forwarded, attempt) {
|
|
66
62
|
if (forwarded === null)
|
|
67
63
|
return retryDelay(attempt);
|
|
68
|
-
if (forwarded.status
|
|
69
|
-
return
|
|
64
|
+
if (forwarded.status !== 200)
|
|
65
|
+
return retryable(forwarded.text) === false ? null : retryDelay(attempt);
|
|
70
66
|
const seconds = pendingSeconds(forwarded);
|
|
71
67
|
return seconds === null ? null : seconds * 1000;
|
|
72
68
|
}
|
|
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
69
|
function editAnswer(forwarded) {
|
|
79
70
|
const answer = answered(forwarded);
|
|
80
71
|
if ('refused' in answer)
|
|
@@ -90,7 +81,7 @@ function editAnswer(forwarded) {
|
|
|
90
81
|
export async function openBridge(house, copies, conversationId, signal) {
|
|
91
82
|
const path = [join(kitHome(), 'shim'), dirname(process.execPath), await loginPath()].join(delimiter);
|
|
92
83
|
const origin = (await readEnrolment()).house;
|
|
93
|
-
const { credential } = await house.post(`/kit/conversations/${conversationId}/credential`, {}, signal);
|
|
84
|
+
const { credential } = await retrying(() => house.post(`/kit/conversations/${conversationId}/credential`, {}, signal), signal);
|
|
94
85
|
const closed = new AbortController();
|
|
95
86
|
const sent = (message, operation) => callHouse(origin, credential, message, operation, closed.signal);
|
|
96
87
|
const send = async (message, operation) => {
|
|
@@ -105,6 +96,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
105
96
|
const after = resendAfter(forwarded, attempt);
|
|
106
97
|
if (after === null)
|
|
107
98
|
return forwarded;
|
|
99
|
+
// 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
100
|
await delay(after, undefined, { signal: closed.signal });
|
|
109
101
|
}
|
|
110
102
|
};
|
|
@@ -119,46 +111,32 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
119
111
|
if ('refused' in prepared)
|
|
120
112
|
return prepared;
|
|
121
113
|
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)));
|
|
114
|
+
let text;
|
|
141
115
|
try {
|
|
142
|
-
|
|
116
|
+
text = await retrying(async () => {
|
|
117
|
+
const posted = await fetch(relayed(transfer.url), {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
120
|
+
body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
|
|
121
|
+
signal: closed.signal,
|
|
122
|
+
});
|
|
123
|
+
const answer = await posted.text();
|
|
124
|
+
if (!posted.ok)
|
|
125
|
+
throw new HouseRefusal('/', posted.status, answer);
|
|
126
|
+
return answer;
|
|
127
|
+
}, closed.signal);
|
|
143
128
|
}
|
|
144
129
|
catch (error) {
|
|
145
|
-
|
|
146
|
-
|
|
130
|
+
if (!finalRefusal(error))
|
|
131
|
+
throw error;
|
|
132
|
+
return { refused: refusalIn(error.text, error.status) };
|
|
147
133
|
}
|
|
134
|
+
const held = JSON.parse(text).transfer;
|
|
135
|
+
return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
|
|
148
136
|
};
|
|
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() };
|
|
137
|
+
const forward = (text) => {
|
|
138
|
+
const message = JSON.parse(text);
|
|
139
|
+
return send(message, message.method === 'tools/call' ? operationId() : null);
|
|
162
140
|
};
|
|
163
141
|
const caller = {
|
|
164
142
|
key: `conversation:${conversationId}`,
|
|
@@ -180,15 +158,15 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
180
158
|
const { cwd, commit } = JSON.parse(text);
|
|
181
159
|
return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
|
|
182
160
|
}
|
|
161
|
+
if (path === '/git/fetch')
|
|
162
|
+
return { status: 200, text: JSON.stringify(await copies.fetch()) };
|
|
183
163
|
if (path === '/git/observed') {
|
|
184
164
|
await copies.observed(JSON.parse(text));
|
|
185
165
|
return { status: 200, text: '{}' };
|
|
186
166
|
}
|
|
187
|
-
return path === '/'
|
|
167
|
+
return path === '/' ? forward(text) : { status: 404, text: '' };
|
|
188
168
|
};
|
|
189
|
-
const
|
|
190
|
-
await mkdir(sockets, { recursive: true, mode: 0o700 });
|
|
191
|
-
const socketPath = join(sockets, `${randomUUID()}.sock`);
|
|
169
|
+
const socketPath = join(socketDirectory(), `${randomUUID()}.sock`);
|
|
192
170
|
const bridge = createServer(async (request, response) => {
|
|
193
171
|
try {
|
|
194
172
|
const path = request.url ?? '';
|
|
@@ -204,7 +182,8 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
204
182
|
response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
|
|
205
183
|
return;
|
|
206
184
|
}
|
|
207
|
-
|
|
185
|
+
const verb = request.url === '/git/push' || request.url === '/git/fetch';
|
|
186
|
+
response.writeHead(502).end(verb && error instanceof Error ? error.message : UNREACHABLE);
|
|
208
187
|
}
|
|
209
188
|
});
|
|
210
189
|
bridge.listen(socketPath);
|
|
@@ -231,7 +210,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
231
210
|
GIT_CONFIG_VALUE_0: app,
|
|
232
211
|
},
|
|
233
212
|
tool: async (name, args) => {
|
|
234
|
-
const forwarded = await forward(
|
|
213
|
+
const forwarded = await forward(JSON.stringify(call(name, args)));
|
|
235
214
|
if (forwarded.status !== 200)
|
|
236
215
|
throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
|
|
237
216
|
const answer = JSON.parse(forwarded.text);
|
package/dist/calls.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { kitHome } from './home.js';
|
|
4
|
+
function logged(error) {
|
|
5
|
+
process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
6
|
+
}
|
|
7
|
+
export class Calls {
|
|
8
|
+
house;
|
|
9
|
+
changed;
|
|
10
|
+
carried = new Map();
|
|
11
|
+
path = join(kitHome(), 'call-results.json');
|
|
12
|
+
results;
|
|
13
|
+
returned = new Set();
|
|
14
|
+
open = 0;
|
|
15
|
+
constructor(house, changed) {
|
|
16
|
+
this.house = house;
|
|
17
|
+
this.changed = changed;
|
|
18
|
+
this.results = new Map(existsSync(this.path) ? Object.entries(JSON.parse(readFileSync(this.path, 'utf8'))) : []);
|
|
19
|
+
}
|
|
20
|
+
keepResult(callId, result) {
|
|
21
|
+
if (this.returned.has(callId))
|
|
22
|
+
return;
|
|
23
|
+
this.results.set(callId, result);
|
|
24
|
+
this.save();
|
|
25
|
+
}
|
|
26
|
+
idle() {
|
|
27
|
+
return this.open === 0;
|
|
28
|
+
}
|
|
29
|
+
carry(callId, work) {
|
|
30
|
+
const known = this.carried.get(callId);
|
|
31
|
+
const result = this.results.get(callId);
|
|
32
|
+
const carried = known ?? (result === undefined ? work() : Promise.resolve({ result }));
|
|
33
|
+
if (known === undefined) {
|
|
34
|
+
this.carried.set(callId, carried);
|
|
35
|
+
carried.catch(() => this.carried.delete(callId));
|
|
36
|
+
}
|
|
37
|
+
this.open++;
|
|
38
|
+
this.changed();
|
|
39
|
+
return carried
|
|
40
|
+
.then(async ({ result, returned }) => {
|
|
41
|
+
try {
|
|
42
|
+
await this.house.deliver(`/kit/inputs/${callId}/ack`, result);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
this.returned.add(callId);
|
|
46
|
+
if (this.results.delete(callId))
|
|
47
|
+
this.save();
|
|
48
|
+
}
|
|
49
|
+
if (known === undefined)
|
|
50
|
+
await returned?.();
|
|
51
|
+
})
|
|
52
|
+
.catch(logged)
|
|
53
|
+
.finally(() => {
|
|
54
|
+
this.open--;
|
|
55
|
+
this.changed();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
save() {
|
|
59
|
+
const temporary = `${this.path}.${process.pid}`;
|
|
60
|
+
writeFileSync(temporary, JSON.stringify(Object.fromEntries(this.results)), { mode: 0o600 });
|
|
61
|
+
renameSync(temporary, this.path);
|
|
62
|
+
}
|
|
63
|
+
}
|
package/dist/chatgpt.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { placeAppGit } from './app-git.js';
|
|
3
|
+
import { kitHome, readOwnAgent, writeOwnAgent } from './home.js';
|
|
4
|
+
import { accepted, DEFAULT_HOUSE } from './login.js';
|
|
5
|
+
const KEY_SOURCE = 'New key for a script on Connections';
|
|
6
|
+
const USAGE = `usage: kit chatgpt --key <key> [--house <origin>]
|
|
7
|
+
--key <key> a key from ${KEY_SOURCE}
|
|
8
|
+
--house <origin> the House of the key, ${DEFAULT_HOUSE} unless one is stored
|
|
9
|
+
`;
|
|
10
|
+
export async function chatgpt(argv) {
|
|
11
|
+
const { values } = parseArgs({
|
|
12
|
+
args: argv,
|
|
13
|
+
options: { key: { type: 'string' }, house: { type: 'string' }, help: { type: 'boolean', default: false } },
|
|
14
|
+
});
|
|
15
|
+
if (values.help) {
|
|
16
|
+
process.stdout.write(USAGE);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (values.key === undefined)
|
|
20
|
+
throw new Error(`pass --key with a key from ${KEY_SOURCE}`);
|
|
21
|
+
const before = await readOwnAgent();
|
|
22
|
+
const house = values.house ?? before?.house ?? DEFAULT_HOUSE;
|
|
23
|
+
if (!(await accepted(house, values.key)))
|
|
24
|
+
throw new Error(`${house} does not accept this key; pass one from ${KEY_SOURCE}`);
|
|
25
|
+
const relay = process.env.HOUSE_KIT_RELAY || undefined;
|
|
26
|
+
await writeOwnAgent({ house, ...(relay === undefined ? {} : { relay }), credential: values.key });
|
|
27
|
+
await placeAppGit(await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
|
|
28
|
+
if (before !== null && before.credential !== values.key) {
|
|
29
|
+
process.stdout.write('This replaced the key this Kit home held; give each session its own HOUSE_KIT_HOME to keep its own key.\n');
|
|
30
|
+
}
|
|
31
|
+
process.stdout.write(`house now calls ${house} as you, with the key kept in ${kitHome()}.\n`);
|
|
32
|
+
}
|
package/dist/clis.js
CHANGED
|
@@ -1,48 +1,94 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { constants, readFileSync } from 'node:fs';
|
|
4
|
+
import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { homedir, userInfo } from 'node:os';
|
|
6
|
+
import { delimiter, dirname, isAbsolute, join } from 'node:path';
|
|
6
7
|
import { kitHome } from './home.js';
|
|
7
|
-
const
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
|
|
9
|
+
function houseRule(houses) {
|
|
10
|
+
return `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`;
|
|
11
|
+
}
|
|
12
|
+
const RAW_SUBTYPES = [
|
|
13
|
+
'background_tasks_changed',
|
|
14
|
+
'task_started',
|
|
15
|
+
'task_updated',
|
|
16
|
+
'task_notification',
|
|
17
|
+
'session_state_changed',
|
|
18
|
+
'hook_started',
|
|
19
|
+
'hook_response',
|
|
20
|
+
];
|
|
21
|
+
async function executable(path) {
|
|
22
|
+
try {
|
|
23
|
+
await access(path, constants.X_OK);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function listedHouses(file) {
|
|
31
|
+
let text;
|
|
32
|
+
try {
|
|
33
|
+
text = await readFile(file, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === 'ENOENT')
|
|
37
|
+
return [];
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const listed = JSON.parse(HOUSE_EXECUTABLES.exec(text)?.[1] ?? '[]');
|
|
42
|
+
return Array.isArray(listed) ? listed.filter((house) => typeof house === 'string') : [];
|
|
21
43
|
}
|
|
22
|
-
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function allowHouseInCodex(path) {
|
|
49
|
+
const candidates = [...new Set(path.split(delimiter).filter((directory) => isAbsolute(directory)))].map((directory) => join(directory, 'house'));
|
|
50
|
+
const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
|
|
51
|
+
const file = join(rules, 'house.rules');
|
|
52
|
+
const houses = [];
|
|
53
|
+
for (const house of [...(await listedHouses(file)), ...candidates]) {
|
|
54
|
+
if (!houses.includes(house) && (await executable(house)))
|
|
55
|
+
houses.push(house);
|
|
56
|
+
}
|
|
57
|
+
await mkdir(rules, { recursive: true });
|
|
58
|
+
const temporary = `${file}.${randomUUID()}`;
|
|
59
|
+
await writeFile(temporary, houseRule(houses));
|
|
60
|
+
await rename(temporary, file);
|
|
61
|
+
}
|
|
62
|
+
async function nothing() { }
|
|
63
|
+
function answered(error) {
|
|
64
|
+
return typeof error === 'object' && error !== null ? error : {};
|
|
23
65
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
if (phase === 'commentary')
|
|
27
|
-
return 'note';
|
|
28
|
-
if (phase === 'final_answer')
|
|
29
|
-
return 'answer';
|
|
30
|
-
return null;
|
|
66
|
+
function bridgeInClaudeSandbox(bridge) {
|
|
67
|
+
return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
|
|
31
68
|
}
|
|
32
69
|
export const CLIS = {
|
|
33
70
|
'codex-acp': {
|
|
34
71
|
bin: 'codex',
|
|
35
72
|
minimum: '0.159.1',
|
|
36
73
|
install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
|
|
37
|
-
adapter: {
|
|
74
|
+
adapter: {
|
|
75
|
+
package: '@agentclientprotocol/codex-acp',
|
|
76
|
+
bin: 'codex-acp',
|
|
77
|
+
executable: 'CODEX_PATH',
|
|
78
|
+
env: { CODEX_CONFIG: JSON.stringify({ features: { shell_zsh_fork: true } }) },
|
|
79
|
+
},
|
|
38
80
|
args: [],
|
|
81
|
+
fullAccess: 'agent-full-access',
|
|
82
|
+
sessionMeta: () => ({}),
|
|
83
|
+
unknownSession: (error) => {
|
|
84
|
+
const { code, data } = answered(error);
|
|
85
|
+
return code === -32603 && typeof data?.details === 'string' && data.details.startsWith('no rollout found');
|
|
86
|
+
},
|
|
87
|
+
renames: true,
|
|
88
|
+
allowHouse: allowHouseInCodex,
|
|
39
89
|
signedIn: { command: ['login', 'status'] },
|
|
40
90
|
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,
|
|
91
|
+
air: ['asyncTasks', 'nativeSubagentSessions'],
|
|
46
92
|
},
|
|
47
93
|
'claude-agent-acp': {
|
|
48
94
|
bin: 'claude',
|
|
@@ -52,45 +98,51 @@ export const CLIS = {
|
|
|
52
98
|
package: '@agentclientprotocol/claude-agent-acp',
|
|
53
99
|
bin: 'claude-agent-acp',
|
|
54
100
|
executable: 'CLAUDE_CODE_EXECUTABLE',
|
|
55
|
-
env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp' },
|
|
101
|
+
env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp', IS_SANDBOX: '1' },
|
|
56
102
|
},
|
|
57
103
|
args: [],
|
|
104
|
+
fullAccess: 'bypassPermissions',
|
|
105
|
+
sessionMeta: (bridge, title) => ({
|
|
106
|
+
claudeCode: {
|
|
107
|
+
options: {
|
|
108
|
+
allowedTools: ['Bash(house:*)'],
|
|
109
|
+
includeHookEvents: true,
|
|
110
|
+
title,
|
|
111
|
+
settings: { sandbox: { network: bridgeInClaudeSandbox(bridge) } },
|
|
112
|
+
},
|
|
113
|
+
emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
unknownSession: (error) => answered(error).code === -32002,
|
|
117
|
+
renames: false,
|
|
118
|
+
allowHouse: nothing,
|
|
58
119
|
signedIn: { command: ['auth', 'status'] },
|
|
59
120
|
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,
|
|
121
|
+
air: ['asyncTasks', 'sessionFailure'],
|
|
67
122
|
},
|
|
68
123
|
'grok-build': {
|
|
69
124
|
bin: 'grok',
|
|
70
125
|
minimum: '1.0.46',
|
|
71
126
|
install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
|
|
72
127
|
adapter: null,
|
|
73
|
-
args: ['agent', '--no-leader', 'stdio'],
|
|
128
|
+
args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
|
|
129
|
+
fullAccess: null,
|
|
130
|
+
sessionMeta: () => ({}),
|
|
131
|
+
unknownSession: (error) => answered(error).data?.code === 'FS_NOT_FOUND',
|
|
132
|
+
renames: false,
|
|
133
|
+
allowHouse: nothing,
|
|
74
134
|
signedIn: { initializeMeta: 'defaultAuthMethodId' },
|
|
75
135
|
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,
|
|
136
|
+
air: ['asyncTasks'],
|
|
87
137
|
},
|
|
88
138
|
};
|
|
89
139
|
const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
90
|
-
export const
|
|
140
|
+
export const KIT_RELEASE = KIT_PACKAGE.version;
|
|
91
141
|
const PROXY = /^https?_proxy$/i;
|
|
92
142
|
const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
|
|
143
|
+
// An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
|
|
93
144
|
const INSTALL_MS = 15 * 60_000;
|
|
145
|
+
// 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
146
|
const READ_MS = 30_000;
|
|
95
147
|
const CAUSE_CHARACTERS = 4000;
|
|
96
148
|
function prefix(kind) {
|
|
@@ -118,8 +170,22 @@ export function run(command, args, timeoutMs, detached = false, signal) {
|
|
|
118
170
|
timeout: timeoutMs,
|
|
119
171
|
env: withoutProxy(),
|
|
120
172
|
detached,
|
|
121
|
-
signal,
|
|
173
|
+
signal: detached ? undefined : signal,
|
|
122
174
|
});
|
|
175
|
+
const stop = () => {
|
|
176
|
+
try {
|
|
177
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
178
|
+
}
|
|
179
|
+
catch { }
|
|
180
|
+
};
|
|
181
|
+
if (detached && signal?.aborted)
|
|
182
|
+
stop();
|
|
183
|
+
if (detached)
|
|
184
|
+
signal?.addEventListener('abort', stop, { once: true });
|
|
185
|
+
const ended = (ran) => {
|
|
186
|
+
signal?.removeEventListener('abort', stop);
|
|
187
|
+
resolve(ran);
|
|
188
|
+
};
|
|
123
189
|
let stdout = '';
|
|
124
190
|
let output = '';
|
|
125
191
|
child.stdout.setEncoding('utf8').on('data', (chunk) => {
|
|
@@ -129,25 +195,25 @@ export function run(command, args, timeoutMs, detached = false, signal) {
|
|
|
129
195
|
child.stderr.setEncoding('utf8').on('data', (chunk) => {
|
|
130
196
|
output += chunk;
|
|
131
197
|
});
|
|
132
|
-
child.on('error', (error) =>
|
|
133
|
-
child.on('close', (status) =>
|
|
198
|
+
child.on('error', (error) => ended({ status: null, stdout, output: error.message }));
|
|
199
|
+
child.on('close', (status) => ended({ status, stdout, output }));
|
|
134
200
|
});
|
|
135
201
|
}
|
|
136
202
|
export function failureOf(ran, program) {
|
|
137
203
|
return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
|
|
138
204
|
}
|
|
139
|
-
export async function installAdapter(kind) {
|
|
205
|
+
export async function installAdapter(kind, signal) {
|
|
140
206
|
const adapter = CLIS[kind].adapter;
|
|
141
207
|
if (adapter === null)
|
|
142
208
|
return null;
|
|
143
209
|
const release = KIT_PACKAGE.peerDependencies[adapter.package];
|
|
144
210
|
if ((await installedAdapter(kind)) === release)
|
|
145
211
|
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);
|
|
212
|
+
const ran = await run('npm', ['install', '--prefix', prefix(kind), '--cache', join(kitHome(), 'npm-cache'), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
|
|
147
213
|
return ran.status === 0 ? null : failureOf(ran, 'npm');
|
|
148
214
|
}
|
|
149
|
-
export async function installCli(kind) {
|
|
150
|
-
const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true);
|
|
215
|
+
export async function installCli(kind, signal) {
|
|
216
|
+
const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
|
|
151
217
|
return ran.status === 0 ? null : failureOf(ran, 'the install');
|
|
152
218
|
}
|
|
153
219
|
function loginShell() {
|
|
@@ -156,11 +222,11 @@ function loginShell() {
|
|
|
156
222
|
const FOUND = 'house-kit-cli ';
|
|
157
223
|
const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
|
|
158
224
|
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);
|
|
225
|
+
function inLoginShell(script, argument = '', signal) {
|
|
226
|
+
return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
|
|
161
227
|
}
|
|
162
|
-
export async function locate(kind) {
|
|
163
|
-
const ran = await inLoginShell(LOOKUP, CLIS[kind].bin);
|
|
228
|
+
export async function locate(kind, signal) {
|
|
229
|
+
const ran = await inLoginShell(LOOKUP, CLIS[kind].bin, signal);
|
|
164
230
|
return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
|
|
165
231
|
}
|
|
166
232
|
export async function loginPath() {
|
|
@@ -168,8 +234,14 @@ export async function loginPath() {
|
|
|
168
234
|
const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
|
|
169
235
|
return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
|
|
170
236
|
}
|
|
171
|
-
export async function
|
|
172
|
-
|
|
237
|
+
export async function writeCodexRule() {
|
|
238
|
+
const shell = await loginPath().catch(() => '');
|
|
239
|
+
await allowHouseInCodex([dirname(process.execPath), process.env.PATH ?? '', shell].join(delimiter)).catch((error) => {
|
|
240
|
+
throw new Error(`Codex's rule for house could not be written: ${error.message}`);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
export async function versionOf(path, signal) {
|
|
244
|
+
return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
|
|
173
245
|
}
|
|
174
246
|
function numbers(version) {
|
|
175
247
|
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
|
+
});
|