@agentshouse/kit 0.2.1-c4ea88ba9424e → 0.2.1-c534eec03292e
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 +44 -42
- package/dist/acp.js +102 -111
- package/dist/agents.js +156 -40
- package/dist/api.js +10 -6
- package/dist/app-git.js +10 -26
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +15 -44
- package/dist/building.js +46 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +32 -0
- package/dist/clis.js +153 -76
- package/dist/codex-main.js +58 -0
- package/dist/connection.js +116 -0
- package/dist/conversations.js +730 -332
- package/dist/copies.js +185 -123
- package/dist/end.js +30 -5
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +546 -0
- package/dist/files.js +139 -17
- package/dist/git.js +13 -11
- package/dist/home.js +89 -7
- package/dist/house-cli.js +299 -0
- package/dist/house-main.js +7 -159
- package/dist/instructions.js +8 -10
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -22
- package/dist/login.js +118 -107
- package/dist/logout.js +14 -16
- package/dist/mcp.js +6 -2
- package/dist/parts.js +82 -0
- package/dist/placement.js +39 -0
- package/dist/relay.js +33 -2
- package/dist/resident.js +115 -49
- package/dist/scope.js +214 -0
- package/dist/scripts.js +255 -0
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/sign-in.js +23 -40
- package/dist/silence.js +28 -0
- package/dist/skills.js +49 -35
- package/dist/transcription.js +85 -45
- package/dist/translate.js +2 -8
- package/dist/update.js +115 -54
- package/dist/wire.js +22 -0
- package/dist/work.js +346 -0
- package/package.json +20 -20
- package/dist/blocks.js +0 -51
- package/dist/bootstrap.json +0 -24
- package/dist/clis-main.js +0 -23
- package/dist/stream.js +0 -72
- 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/agents.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { client } from '@agentclientprotocol/sdk';
|
|
2
|
-
import { readFile
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { signedOut, startAdapter } from './acp.js';
|
|
5
|
+
import { CLIS, KIT_RELEASE, below, installAdapter, installCli, locate, run, updateCli, versionOf } from './clis.js';
|
|
5
6
|
import { agentBase, kitHome } from './home.js';
|
|
7
|
+
import { updatesItself } from './placement.js';
|
|
6
8
|
import { holdTranscription, transcriptionReport } from './transcription.js';
|
|
9
|
+
import { KIT_WIRE } from './wire.js';
|
|
10
|
+
// Two quiet seconds let a fresh conversation start before background model discovery competes for its CPU.
|
|
11
|
+
const MODEL_PROBE_QUIET_MS = 2_000;
|
|
12
|
+
// A CLI's maker can publish several releases a day, so running each chosen CLI's update command hourly brings a new one to the next conversation within the hour for one small check.
|
|
13
|
+
export const CLI_UPDATE_MS = 60 * 60_000;
|
|
14
|
+
// A sign-in's model read that has not answered in ten seconds leaves its sign-in to the status check, so a hung CLI never holds the sign-in's result.
|
|
15
|
+
const SIGNED_IN_READ_MS = 10_000;
|
|
7
16
|
const SIGNED_OUT = { signed_in: false, models: [], modes: [] };
|
|
17
|
+
const INSTALLING = { release: null, failure: null, installing: true, ...SIGNED_OUT };
|
|
8
18
|
function select(options, category) {
|
|
9
19
|
const option = options.find((candidate) => candidate.category === category);
|
|
10
20
|
return option?.type === 'select' ? option : undefined;
|
|
@@ -21,6 +31,22 @@ function causeOf(error) {
|
|
|
21
31
|
function logged(error) {
|
|
22
32
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
23
33
|
}
|
|
34
|
+
const LAPTOP_CHASSIS = new Set(['8', '9', '10', '14', '30', '31', '32']);
|
|
35
|
+
const DESKTOP_CHASSIS = new Set(['3', '4', '5', '6', '7', '15', '16']);
|
|
36
|
+
const SERVER_CHASSIS = new Set(['17', '18', '19', '20', '21', '22', '23', '24']);
|
|
37
|
+
export function chassisForm(value) {
|
|
38
|
+
const chassis = value.trim();
|
|
39
|
+
if (LAPTOP_CHASSIS.has(chassis))
|
|
40
|
+
return 'laptop';
|
|
41
|
+
if (DESKTOP_CHASSIS.has(chassis))
|
|
42
|
+
return 'desktop';
|
|
43
|
+
if (SERVER_CHASSIS.has(chassis))
|
|
44
|
+
return 'server';
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
export function hostVm(version) {
|
|
48
|
+
return /docker desktop|microsoft|wsl|linuxkit/i.test(version);
|
|
49
|
+
}
|
|
24
50
|
async function models(kind, adapter, known) {
|
|
25
51
|
const agent = adapter.connection.agent;
|
|
26
52
|
const opened = await agent.request('session/new', { cwd: kitHome(), mcpServers: [] });
|
|
@@ -47,13 +73,6 @@ async function models(kind, adapter, known) {
|
|
|
47
73
|
}
|
|
48
74
|
return { models: offered, modes };
|
|
49
75
|
}
|
|
50
|
-
async function signedIn(kind, cli, known, signal) {
|
|
51
|
-
const probe = CLIS[kind].signedIn;
|
|
52
|
-
// A sign-in check that has not answered in half a minute counts as signed out rather than stalling the report.
|
|
53
|
-
if ('command' in probe)
|
|
54
|
-
return (await run(cli, probe.command, 30_000, false, signal)).status === 0;
|
|
55
|
-
return (await offer(kind, cli, known, signal)).signed_in;
|
|
56
|
-
}
|
|
57
76
|
function ended(signal) {
|
|
58
77
|
return new Promise((_, reject) => {
|
|
59
78
|
if (signal.aborted)
|
|
@@ -80,6 +99,8 @@ async function offer(kind, cli, known, signal) {
|
|
|
80
99
|
catch (error) {
|
|
81
100
|
if (!signal?.aborted)
|
|
82
101
|
process.stderr.write(`kit: ${kind} did not offer its models: ${causeOf(error)}\n`);
|
|
102
|
+
if (signedOut(error))
|
|
103
|
+
return SIGNED_OUT;
|
|
83
104
|
return {
|
|
84
105
|
signed_in: adapter === undefined && 'initializeMeta' in probe ? known.signed_in : true,
|
|
85
106
|
models: known.models,
|
|
@@ -87,26 +108,24 @@ async function offer(kind, cli, known, signal) {
|
|
|
87
108
|
};
|
|
88
109
|
}
|
|
89
110
|
finally {
|
|
90
|
-
|
|
91
|
-
killTree(adapter.child);
|
|
111
|
+
adapter?.retire();
|
|
92
112
|
}
|
|
93
113
|
}
|
|
94
114
|
function reported(kind, reading) {
|
|
115
|
+
const { minimum, update } = CLIS[kind];
|
|
95
116
|
return {
|
|
96
117
|
kind,
|
|
97
118
|
release: reading.release,
|
|
98
|
-
...(reading.
|
|
119
|
+
...(reading.release !== null && below(reading.release, minimum) ? { minimum, update } : {}),
|
|
99
120
|
failure: reading.failure,
|
|
121
|
+
...(reading.installing === undefined ? {} : { installing: reading.installing }),
|
|
100
122
|
signed_in: reading.signed_in,
|
|
101
123
|
models: reading.models,
|
|
102
124
|
modes: reading.modes,
|
|
103
125
|
full_access: CLIS[kind].fullAccess,
|
|
104
126
|
};
|
|
105
127
|
}
|
|
106
|
-
|
|
107
|
-
const cli = CLIS[kind];
|
|
108
|
-
return `${cli.bin} ${release} is older than ${cli.minimum}, the oldest this Kit runs`;
|
|
109
|
-
}
|
|
128
|
+
const NOT_READY = "the Agent's CLI is not ready on this computer";
|
|
110
129
|
async function sshHostKey() {
|
|
111
130
|
// Listing a local host key takes a moment, so ten seconds only bounds a stuck ssh-keygen.
|
|
112
131
|
const listed = await run('ssh-keygen', ['-l', '-E', 'sha256', '-f', '/etc/ssh/ssh_host_ed25519_key.pub'], 10_000);
|
|
@@ -119,18 +138,44 @@ async function operatingSystem() {
|
|
|
119
138
|
const release = await readFile('/etc/os-release', 'utf8');
|
|
120
139
|
return /^PRETTY_NAME="?([^"\n]*)"?$/m.exec(release)?.[1] ?? release;
|
|
121
140
|
}
|
|
141
|
+
async function hostForm() {
|
|
142
|
+
if (process.platform === 'darwin') {
|
|
143
|
+
const battery = await run('pmset', ['-g', 'batt'], 10_000);
|
|
144
|
+
return battery.status === 0 && battery.output.includes('InternalBattery') ? 'laptop' : 'desktop';
|
|
145
|
+
}
|
|
146
|
+
if (process.platform !== 'linux')
|
|
147
|
+
return undefined;
|
|
148
|
+
const [container, version, chassis] = await Promise.all([
|
|
149
|
+
readFile('/.dockerenv', 'utf8').then(() => true).catch(() => false),
|
|
150
|
+
readFile('/proc/version', 'utf8').catch(() => ''),
|
|
151
|
+
readFile('/sys/class/dmi/id/chassis_type', 'utf8').catch(() => ''),
|
|
152
|
+
]);
|
|
153
|
+
if (container && hostVm(version))
|
|
154
|
+
return undefined;
|
|
155
|
+
return chassisForm(chassis);
|
|
156
|
+
}
|
|
122
157
|
export class Agents {
|
|
123
158
|
desired = { agents: [], routes: [], transcribe: false };
|
|
124
159
|
house;
|
|
125
160
|
reading = Promise.resolve();
|
|
126
161
|
queues = new Map();
|
|
127
162
|
installing = Promise.resolve();
|
|
163
|
+
installs = new Map();
|
|
128
164
|
readings = new Map();
|
|
129
165
|
reads = new Map();
|
|
166
|
+
starts = 0;
|
|
130
167
|
sending = Promise.resolve();
|
|
131
168
|
sent = null;
|
|
132
|
-
|
|
169
|
+
refreshing = 0;
|
|
170
|
+
loaded = Promise.resolve();
|
|
171
|
+
updating = null;
|
|
172
|
+
changed;
|
|
173
|
+
constructor(house, changed = () => undefined) {
|
|
133
174
|
this.house = house;
|
|
175
|
+
this.changed = changed;
|
|
176
|
+
}
|
|
177
|
+
idle() {
|
|
178
|
+
return this.refreshing === 0 && this.updating === null;
|
|
134
179
|
}
|
|
135
180
|
read() {
|
|
136
181
|
return this.reading.then(() => undefined, () => undefined);
|
|
@@ -139,35 +184,49 @@ export class Agents {
|
|
|
139
184
|
return this.desired.routes.find((route) => route.agent_id === agentId);
|
|
140
185
|
}
|
|
141
186
|
refresh() {
|
|
187
|
+
this.refreshing++;
|
|
188
|
+
this.changed();
|
|
142
189
|
const reading = this.read().then(async () => {
|
|
143
190
|
this.desired = await this.house.deliver('/kit/agents/desired', {});
|
|
144
191
|
return this.desired;
|
|
145
192
|
});
|
|
146
193
|
this.reading = reading;
|
|
147
|
-
|
|
194
|
+
this.loaded = reading
|
|
195
|
+
.then(({ agents, transcribe }) => Promise.all([
|
|
148
196
|
holdTranscription(transcribe)
|
|
149
197
|
.then((changed) => (changed ? this.report() : undefined))
|
|
150
|
-
.catch(logged)
|
|
198
|
+
.catch(logged),
|
|
151
199
|
Promise.all(agents.map((kind) => this.load(kind)))
|
|
152
200
|
.then(() => this.report())
|
|
153
|
-
.catch(logged)
|
|
201
|
+
.catch(logged),
|
|
202
|
+
]))
|
|
203
|
+
.catch(() => undefined)
|
|
204
|
+
.finally(() => {
|
|
205
|
+
this.refreshing--;
|
|
206
|
+
this.changed();
|
|
154
207
|
});
|
|
208
|
+
return reading.then(() => undefined);
|
|
209
|
+
}
|
|
210
|
+
keepCurrent() {
|
|
211
|
+
this.updateAll(this.loaded);
|
|
212
|
+
setInterval(() => this.updateAll(Promise.resolve()), CLI_UPDATE_MS);
|
|
155
213
|
}
|
|
156
214
|
cli(kind) {
|
|
215
|
+
this.starts += 1;
|
|
157
216
|
return this.queue(kind, async () => {
|
|
158
217
|
const resolved = await this.resolve(kind);
|
|
159
218
|
if (!('cli' in resolved)) {
|
|
160
219
|
this.update(kind, resolved);
|
|
161
|
-
throw new Error(
|
|
220
|
+
throw new Error(NOT_READY);
|
|
162
221
|
}
|
|
163
222
|
const { cli, release } = resolved;
|
|
164
223
|
const known = this.readings.get(kind);
|
|
165
|
-
const kept = known?.release === null
|
|
166
|
-
if (known?.release !== release ||
|
|
224
|
+
const kept = known?.release === null ? SIGNED_OUT : (known ?? SIGNED_OUT);
|
|
225
|
+
if (known?.release !== release || this.installs.has(kind)) {
|
|
167
226
|
this.update(kind, { release, failure: null, signed_in: kept.signed_in, models: kept.models, modes: kept.modes });
|
|
168
227
|
}
|
|
169
228
|
const read = this.nextRead(kind);
|
|
170
|
-
|
|
229
|
+
this.quietOffer(kind, cli, kept)
|
|
171
230
|
.then((offered) => {
|
|
172
231
|
const current = this.readings.get(kind);
|
|
173
232
|
if (this.reads.get(kind) === read && current?.release === release)
|
|
@@ -178,7 +237,8 @@ export class Agents {
|
|
|
178
237
|
});
|
|
179
238
|
}
|
|
180
239
|
located(kind, signal) {
|
|
181
|
-
|
|
240
|
+
const owner = {};
|
|
241
|
+
return this.queue(kind, () => this.ensure(kind, signal, owner).finally(() => this.settle(kind, owner)), signal);
|
|
182
242
|
}
|
|
183
243
|
async reread(kind) {
|
|
184
244
|
await this.examine(kind, this.readings.get(kind) ?? SIGNED_OUT, offer);
|
|
@@ -186,7 +246,13 @@ export class Agents {
|
|
|
186
246
|
}
|
|
187
247
|
async recheck(kind, signal) {
|
|
188
248
|
const known = this.readings.get(kind) ?? SIGNED_OUT;
|
|
189
|
-
|
|
249
|
+
const owner = {};
|
|
250
|
+
try {
|
|
251
|
+
await this.examine(kind, known, (kind, cli) => offer(kind, cli, known, AbortSignal.any([signal, AbortSignal.timeout(SIGNED_IN_READ_MS)])), signal, owner);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
this.settle(kind, owner);
|
|
255
|
+
}
|
|
190
256
|
if (!signal.aborted)
|
|
191
257
|
await this.report();
|
|
192
258
|
}
|
|
@@ -200,8 +266,18 @@ export class Agents {
|
|
|
200
266
|
this.reads.set(kind, read);
|
|
201
267
|
return read;
|
|
202
268
|
}
|
|
203
|
-
|
|
269
|
+
hold(kind, reading) {
|
|
204
270
|
this.readings.set(kind, reading);
|
|
271
|
+
this.installs.delete(kind);
|
|
272
|
+
}
|
|
273
|
+
update(kind, reading) {
|
|
274
|
+
this.hold(kind, reading);
|
|
275
|
+
this.report().catch(logged);
|
|
276
|
+
}
|
|
277
|
+
settle(kind, owner) {
|
|
278
|
+
if (this.installs.get(kind) !== owner)
|
|
279
|
+
return;
|
|
280
|
+
this.installs.delete(kind);
|
|
205
281
|
this.report().catch(logged);
|
|
206
282
|
}
|
|
207
283
|
queue(kind, work, signal) {
|
|
@@ -212,13 +288,15 @@ export class Agents {
|
|
|
212
288
|
this.queues.set(kind, queued.catch(() => undefined));
|
|
213
289
|
return signal === undefined ? queued : Promise.race([queued, ended(signal)]);
|
|
214
290
|
}
|
|
215
|
-
async ensure(kind, signal) {
|
|
291
|
+
async ensure(kind, signal, owner = {}) {
|
|
216
292
|
const failed = await installAdapter(kind, signal);
|
|
217
293
|
if (failed !== null)
|
|
218
294
|
throw new Error(failed);
|
|
219
295
|
const found = await locate(kind, signal);
|
|
220
296
|
if (found !== null)
|
|
221
297
|
return found;
|
|
298
|
+
this.installs.set(kind, owner);
|
|
299
|
+
this.report().catch(logged);
|
|
222
300
|
const installing = this.installing.catch(() => undefined).then(() => installCli(kind, signal));
|
|
223
301
|
this.installing = installing;
|
|
224
302
|
const failure = await installing;
|
|
@@ -229,10 +307,10 @@ export class Agents {
|
|
|
229
307
|
throw new Error(`the login shell does not find ${CLIS[kind].bin} after its install`);
|
|
230
308
|
return installed;
|
|
231
309
|
}
|
|
232
|
-
async resolve(kind, signal) {
|
|
310
|
+
async resolve(kind, signal, owner) {
|
|
233
311
|
let cli;
|
|
234
312
|
try {
|
|
235
|
-
cli = await
|
|
313
|
+
cli = await this.ensure(kind, signal, owner);
|
|
236
314
|
}
|
|
237
315
|
catch (error) {
|
|
238
316
|
const failure = causeOf(error);
|
|
@@ -242,33 +320,71 @@ export class Agents {
|
|
|
242
320
|
const release = await versionOf(cli, signal);
|
|
243
321
|
if (release === null)
|
|
244
322
|
return { release: null, failure: `${cli} --version names no version`, ...SIGNED_OUT };
|
|
245
|
-
const minimum = CLIS[kind].minimum;
|
|
246
|
-
if (below(release, minimum))
|
|
247
|
-
return { release, minimum, failure: null, ...SIGNED_OUT };
|
|
248
323
|
return { cli, release };
|
|
249
324
|
}
|
|
250
|
-
async examine(kind, known, read, signal) {
|
|
325
|
+
async examine(kind, known, read, signal, owner) {
|
|
251
326
|
const current = this.nextRead(kind);
|
|
252
|
-
const resolved = await this.queue(kind, () => this.resolve(kind, signal), signal);
|
|
327
|
+
const resolved = await this.queue(kind, () => this.resolve(kind, signal, owner), signal);
|
|
253
328
|
const reading = 'cli' in resolved ? { release: resolved.release, failure: null, ...(await read(kind, resolved.cli, known)) } : resolved;
|
|
254
329
|
if (this.reads.get(kind) === current && !signal?.aborted)
|
|
255
|
-
this.
|
|
330
|
+
this.hold(kind, reading);
|
|
331
|
+
}
|
|
332
|
+
updateAll(after) {
|
|
333
|
+
if (this.updating !== null)
|
|
334
|
+
return;
|
|
335
|
+
this.updating = after
|
|
336
|
+
.then(async () => {
|
|
337
|
+
for (const kind of this.desired.agents) {
|
|
338
|
+
if (this.installs.has(kind) || !this.readings.get(kind)?.release)
|
|
339
|
+
continue;
|
|
340
|
+
const failure = await updateCli(kind);
|
|
341
|
+
if (failure !== null)
|
|
342
|
+
process.stderr.write(`kit: ${kind} did not update: ${failure}\n`);
|
|
343
|
+
await this.readVersion(kind);
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
.catch(logged)
|
|
347
|
+
.finally(() => {
|
|
348
|
+
this.updating = null;
|
|
349
|
+
this.changed();
|
|
350
|
+
});
|
|
351
|
+
this.changed();
|
|
352
|
+
}
|
|
353
|
+
async readVersion(kind) {
|
|
354
|
+
const cli = await locate(kind);
|
|
355
|
+
const release = cli === null ? null : await versionOf(cli);
|
|
356
|
+
const current = this.readings.get(kind);
|
|
357
|
+
if (release === null || !current?.release || current.release === release)
|
|
358
|
+
return;
|
|
359
|
+
this.update(kind, { ...current, release });
|
|
360
|
+
}
|
|
361
|
+
async quietOffer(kind, cli, known) {
|
|
362
|
+
for (let seen = -1; seen !== this.starts;) {
|
|
363
|
+
seen = this.starts;
|
|
364
|
+
await delay(MODEL_PROBE_QUIET_MS);
|
|
365
|
+
}
|
|
366
|
+
return offer(kind, cli, known);
|
|
256
367
|
}
|
|
257
368
|
async load(kind) {
|
|
258
369
|
const known = this.readings.get(kind);
|
|
259
370
|
if (known !== undefined && known.failure === null)
|
|
260
371
|
return;
|
|
261
|
-
await this.examine(kind, SIGNED_OUT,
|
|
372
|
+
await this.examine(kind, SIGNED_OUT, (kind, cli, known) => this.quietOffer(kind, cli, known));
|
|
373
|
+
await this.report();
|
|
262
374
|
}
|
|
263
375
|
async send() {
|
|
264
376
|
const agents = this.desired.agents.flatMap((kind) => {
|
|
265
|
-
const reading = this.readings.get(kind);
|
|
377
|
+
const reading = this.installs.has(kind) ? INSTALLING : this.readings.get(kind);
|
|
266
378
|
return reading === undefined ? [] : [reported(kind, reading)];
|
|
267
379
|
});
|
|
268
380
|
const hostKey = await sshHostKey();
|
|
381
|
+
const form = await hostForm();
|
|
269
382
|
const body = {
|
|
270
383
|
os: await operatingSystem(),
|
|
271
|
-
|
|
384
|
+
...(form === undefined ? {} : { form }),
|
|
385
|
+
kit_release: KIT_RELEASE,
|
|
386
|
+
wire: KIT_WIRE,
|
|
387
|
+
updates_itself: await updatesItself(),
|
|
272
388
|
agent_base: agentBase(),
|
|
273
389
|
agents,
|
|
274
390
|
transcription: transcriptionReport(),
|
package/dist/api.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
-
import {
|
|
2
|
+
import { signInAgain } from './end.js';
|
|
3
3
|
import { readEnrolment } from './home.js';
|
|
4
4
|
import { retryable } from './refusals.js';
|
|
5
5
|
import { relayed } from './relay.js';
|
|
6
|
+
import { endUnserved, KIT_WIRE, KIT_WIRE_HEADER } from './wire.js';
|
|
6
7
|
export class HouseRefusal extends Error {
|
|
7
8
|
status;
|
|
8
9
|
text;
|
|
@@ -16,10 +17,10 @@ export function finalRefusal(error) {
|
|
|
16
17
|
return error instanceof HouseRefusal && retryable(error.text) === false;
|
|
17
18
|
}
|
|
18
19
|
export function retryDelay(attempt) {
|
|
19
|
-
// Full jitter, doubling from one second to a 30-second cap, spreads the
|
|
20
|
+
// Full jitter, doubling from one second to a 30-second cap, spreads the Kit processes one outage dropped and retries within half a minute of House's return.
|
|
20
21
|
return Math.random() * Math.min(30_000, 1000 * 2 ** attempt);
|
|
21
22
|
}
|
|
22
|
-
export async function retrying(sent, signal) {
|
|
23
|
+
export async function retrying(sent, signal, pause = retryDelay) {
|
|
23
24
|
for (let attempt = 0;; attempt++) {
|
|
24
25
|
try {
|
|
25
26
|
return await sent();
|
|
@@ -28,8 +29,8 @@ export async function retrying(sent, signal) {
|
|
|
28
29
|
if (!(error instanceof HouseRefusal) || retryable(error.text) !== true)
|
|
29
30
|
throw error;
|
|
30
31
|
}
|
|
31
|
-
// A request House refused as retryable is sent again
|
|
32
|
-
await delay(
|
|
32
|
+
// A request House refused as retryable is sent again after the caller's pause, the jittered curve unless it names its own.
|
|
33
|
+
await delay(pause(attempt), undefined, { signal });
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
export async function post(house, path, body, credential, signal) {
|
|
@@ -37,12 +38,15 @@ export async function post(house, path, body, credential, signal) {
|
|
|
37
38
|
method: 'POST',
|
|
38
39
|
headers: {
|
|
39
40
|
'content-type': 'application/json',
|
|
41
|
+
[KIT_WIRE_HEADER]: String(KIT_WIRE),
|
|
40
42
|
...(credential === undefined ? {} : { authorization: `Bearer ${credential}` }),
|
|
41
43
|
},
|
|
42
44
|
body: JSON.stringify(body),
|
|
43
45
|
signal,
|
|
44
46
|
});
|
|
45
47
|
const text = await answer.text();
|
|
48
|
+
if (!answer.ok)
|
|
49
|
+
endUnserved(text);
|
|
46
50
|
if (!answer.ok)
|
|
47
51
|
throw new HouseRefusal(path, answer.status, text);
|
|
48
52
|
return JSON.parse(text);
|
|
@@ -54,7 +58,7 @@ async function enrolledPost(path, body, signal) {
|
|
|
54
58
|
}
|
|
55
59
|
catch (error) {
|
|
56
60
|
if (error instanceof HouseRefusal && error.status === 401)
|
|
57
|
-
|
|
61
|
+
await signInAgain(credential);
|
|
58
62
|
throw error;
|
|
59
63
|
}
|
|
60
64
|
}
|
package/dist/app-git.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import { extname, resolve } from 'node:path';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
1
|
import { run } from './git.js';
|
|
4
2
|
import { kitHome, readOwnAgent } from './home.js';
|
|
5
|
-
|
|
3
|
+
const HELPER = '!kit git-credential';
|
|
6
4
|
function section(house) {
|
|
7
5
|
return `credential.${new URL('/app/', house).href}`;
|
|
8
6
|
}
|
|
9
|
-
function helper() {
|
|
10
|
-
const main = fileURLToPath(new URL(`./kit-main${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
|
|
11
|
-
return `!HOUSE_KIT_HOME=${quoted(resolve(kitHome()))} ${quoted(process.execPath)} ${quoted(main)} git-credential`;
|
|
12
|
-
}
|
|
13
7
|
async function configured(args) {
|
|
14
8
|
try {
|
|
15
9
|
return await run(kitHome(), ['config', '--global', ...args]);
|
|
@@ -20,30 +14,18 @@ async function configured(args) {
|
|
|
20
14
|
throw error;
|
|
21
15
|
}
|
|
22
16
|
}
|
|
23
|
-
async function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const ran = await configured(['--add', `${section(house)}.helper`, value]);
|
|
17
|
+
export async function placeAppGit(ownAgent) {
|
|
18
|
+
if (ownAgent === null)
|
|
19
|
+
return;
|
|
20
|
+
await configured(['--remove-section', section(ownAgent.house)]);
|
|
21
|
+
for (const value of ['', HELPER]) {
|
|
22
|
+
const ran = await configured(['--add', `${section(ownAgent.house)}.helper`, value]);
|
|
30
23
|
if (ran === null)
|
|
31
24
|
return;
|
|
32
25
|
if (ran.status !== 0)
|
|
33
26
|
throw new Error(`git config failed: ${ran.stderr.trim()}`);
|
|
34
27
|
}
|
|
35
28
|
}
|
|
36
|
-
export async function placeAppGit(before, after) {
|
|
37
|
-
if (before !== null && before.house !== after?.house)
|
|
38
|
-
await forget(before.house);
|
|
39
|
-
if (after !== null)
|
|
40
|
-
await register(after.house);
|
|
41
|
-
}
|
|
42
|
-
export async function forgetAppGit() {
|
|
43
|
-
const ownAgent = await readOwnAgent();
|
|
44
|
-
if (ownAgent !== null)
|
|
45
|
-
await forget(ownAgent.house);
|
|
46
|
-
}
|
|
47
29
|
export async function answerGitCredential(operation) {
|
|
48
30
|
let asked = '';
|
|
49
31
|
for await (const chunk of process.stdin)
|
|
@@ -55,8 +37,10 @@ export async function answerGitCredential(operation) {
|
|
|
55
37
|
.filter((line) => line.includes('='))
|
|
56
38
|
.map((line) => [line.slice(0, line.indexOf('=')), line.slice(line.indexOf('=') + 1)]));
|
|
57
39
|
const ownAgent = await readOwnAgent();
|
|
58
|
-
if (ownAgent === null)
|
|
40
|
+
if (ownAgent === null) {
|
|
41
|
+
process.stderr.write(`kit: Kit home ${kitHome()} is not signed in; run kit login\n`);
|
|
59
42
|
return;
|
|
43
|
+
}
|
|
60
44
|
const house = new URL(ownAgent.house);
|
|
61
45
|
if (attributes.get('protocol') !== house.protocol.slice(0, -1) || attributes.get('host') !== house.host)
|
|
62
46
|
return;
|
package/dist/authority-main.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { setGlobalProxyFromEnv } from 'node:http';
|
|
2
1
|
import { HouseRefusal, post } from './api.js';
|
|
3
2
|
import { readEnrolment } from './home.js';
|
|
3
|
+
import { proxyUnlessRelayed } from './relay.js';
|
|
4
4
|
function unanswered(error) {
|
|
5
5
|
return error instanceof HouseRefusal ? error.status >= 500 : error instanceof TypeError && error.message === 'fetch failed';
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
proxyUnlessRelayed();
|
|
8
8
|
try {
|
|
9
9
|
const { house, credential } = (await readEnrolment());
|
|
10
10
|
try {
|
package/dist/bridge.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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';
|
|
@@ -9,13 +8,12 @@ import { parse } from 'yaml';
|
|
|
9
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
|
|
13
|
+
import { operationId } from './operation.js';
|
|
15
14
|
import { refusalOf, retryable } from './refusals.js';
|
|
16
15
|
import { relayed } from './relay.js';
|
|
17
|
-
const MCP_REQUEST_BYTES =
|
|
18
|
-
const ATTACHMENTS = '/kit/attachments/upload';
|
|
16
|
+
const MCP_REQUEST_BYTES = 4_000_000;
|
|
19
17
|
export async function received(request) {
|
|
20
18
|
let text = '';
|
|
21
19
|
request.setEncoding('utf8');
|
|
@@ -68,11 +66,6 @@ function resendAfter(forwarded, attempt) {
|
|
|
68
66
|
const seconds = pendingSeconds(forwarded);
|
|
69
67
|
return seconds === null ? null : seconds * 1000;
|
|
70
68
|
}
|
|
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
69
|
function editAnswer(forwarded) {
|
|
77
70
|
const answer = answered(forwarded);
|
|
78
71
|
if ('refused' in answer)
|
|
@@ -88,7 +81,7 @@ function editAnswer(forwarded) {
|
|
|
88
81
|
export async function openBridge(house, copies, conversationId, signal) {
|
|
89
82
|
const path = [join(kitHome(), 'shim'), dirname(process.execPath), await loginPath()].join(delimiter);
|
|
90
83
|
const origin = (await readEnrolment()).house;
|
|
91
|
-
const { credential } = await house.post(`/kit/conversations/${conversationId}/credential`, {}, signal);
|
|
84
|
+
const { credential } = await retrying(() => house.post(`/kit/conversations/${conversationId}/credential`, {}, signal), signal);
|
|
92
85
|
const closed = new AbortController();
|
|
93
86
|
const sent = (message, operation) => callHouse(origin, credential, message, operation, closed.signal);
|
|
94
87
|
const send = async (message, operation) => {
|
|
@@ -103,7 +96,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
103
96
|
const after = resendAfter(forwarded, attempt);
|
|
104
97
|
if (after === null)
|
|
105
98
|
return forwarded;
|
|
106
|
-
// A
|
|
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.
|
|
107
100
|
await delay(after, undefined, { signal: closed.signal });
|
|
108
101
|
}
|
|
109
102
|
};
|
|
@@ -141,32 +134,9 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
141
134
|
const held = JSON.parse(text).transfer;
|
|
142
135
|
return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
|
|
143
136
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
.post('/kit/tools/mutations', {}, closed.signal)
|
|
148
|
-
.then(({ tools }) => new Set(tools.filter((listed) => listed.writes).map((listed) => listed.name)));
|
|
149
|
-
try {
|
|
150
|
-
return (await mutations).has(tool);
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
mutations = null;
|
|
154
|
-
throw error;
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
const forward = async (path, text) => {
|
|
158
|
-
if (path === '/') {
|
|
159
|
-
const message = JSON.parse(text);
|
|
160
|
-
const tool = message.method === 'tools/call' ? String(message.params.name) : null;
|
|
161
|
-
return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
|
|
162
|
-
}
|
|
163
|
-
const answer = await fetch(relayed(new URL(path, origin)), {
|
|
164
|
-
method: 'POST',
|
|
165
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
|
|
166
|
-
body: versioned(conversationId, text),
|
|
167
|
-
signal: closed.signal,
|
|
168
|
-
});
|
|
169
|
-
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);
|
|
170
140
|
};
|
|
171
141
|
const caller = {
|
|
172
142
|
key: `conversation:${conversationId}`,
|
|
@@ -188,15 +158,15 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
188
158
|
const { cwd, commit } = JSON.parse(text);
|
|
189
159
|
return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
|
|
190
160
|
}
|
|
161
|
+
if (path === '/git/fetch')
|
|
162
|
+
return { status: 200, text: JSON.stringify(await copies.fetch()) };
|
|
191
163
|
if (path === '/git/observed') {
|
|
192
164
|
await copies.observed(JSON.parse(text));
|
|
193
165
|
return { status: 200, text: '{}' };
|
|
194
166
|
}
|
|
195
|
-
return path === '/'
|
|
167
|
+
return path === '/' ? forward(text) : { status: 404, text: '' };
|
|
196
168
|
};
|
|
197
|
-
const
|
|
198
|
-
await mkdir(sockets, { recursive: true, mode: 0o700 });
|
|
199
|
-
const socketPath = join(sockets, `${randomUUID()}.sock`);
|
|
169
|
+
const socketPath = join(socketDirectory(), `${randomUUID()}.sock`);
|
|
200
170
|
const bridge = createServer(async (request, response) => {
|
|
201
171
|
try {
|
|
202
172
|
const path = request.url ?? '';
|
|
@@ -212,7 +182,8 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
212
182
|
response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
|
|
213
183
|
return;
|
|
214
184
|
}
|
|
215
|
-
|
|
185
|
+
const verb = request.url === '/git/push' || request.url === '/git/fetch';
|
|
186
|
+
response.writeHead(502).end(verb && error instanceof Error ? error.message : UNREACHABLE);
|
|
216
187
|
}
|
|
217
188
|
});
|
|
218
189
|
bridge.listen(socketPath);
|
|
@@ -239,7 +210,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
239
210
|
GIT_CONFIG_VALUE_0: app,
|
|
240
211
|
},
|
|
241
212
|
tool: async (name, args) => {
|
|
242
|
-
const forwarded = await forward(
|
|
213
|
+
const forwarded = await forward(JSON.stringify(call(name, args)));
|
|
243
214
|
if (forwarded.status !== 200)
|
|
244
215
|
throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
|
|
245
216
|
const answer = JSON.parse(forwarded.text);
|
package/dist/building.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { rm } from 'node:fs/promises';
|
|
4
|
+
import { Transform } from 'node:stream';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
import { readEnrolment } from './home.js';
|
|
7
|
+
import { relayed } from './relay.js';
|
|
8
|
+
const MANIFEST = 'bootstrap.json';
|
|
9
|
+
async function served(name, signal) {
|
|
10
|
+
const enrolment = await readEnrolment();
|
|
11
|
+
if (enrolment === null)
|
|
12
|
+
throw new Error('this Kit is connected to no House');
|
|
13
|
+
const answer = await fetch(relayed(new URL(`/kit-installer/${encodeURIComponent(name)}`, enrolment.house)), { signal });
|
|
14
|
+
if (answer.status === 200 && answer.body !== null)
|
|
15
|
+
return answer;
|
|
16
|
+
await answer.body?.cancel();
|
|
17
|
+
throw new Error(`House answered ${answer.status} for ${name}`);
|
|
18
|
+
}
|
|
19
|
+
export async function servedDigest(name, signal) {
|
|
20
|
+
const manifest = (await (await served(MANIFEST, signal)).json());
|
|
21
|
+
const sha256 = manifest.files?.[name]?.sha256;
|
|
22
|
+
return typeof sha256 === 'string' ? sha256 : null;
|
|
23
|
+
}
|
|
24
|
+
export async function downloadServed(name, sha256, destination, signal, most = Infinity) {
|
|
25
|
+
const answer = await served(name, signal);
|
|
26
|
+
const digest = createHash('sha256');
|
|
27
|
+
let bytes = 0;
|
|
28
|
+
const hashed = new Transform({
|
|
29
|
+
transform(chunk, _encoding, done) {
|
|
30
|
+
bytes += chunk.length;
|
|
31
|
+
if (bytes > most)
|
|
32
|
+
return done(new Error(`${name} is larger than its pinned ${most} bytes`));
|
|
33
|
+
digest.update(chunk);
|
|
34
|
+
done(null, chunk);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
await pipeline(answer.body, hashed, createWriteStream(destination), { signal });
|
|
39
|
+
if (digest.digest('hex') !== sha256)
|
|
40
|
+
throw new Error(`${name} does not match its sha256`);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
await rm(destination, { force: true });
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|