@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.
Files changed (133) hide show
  1. package/README.md +44 -42
  2. package/dist/acp.js +102 -111
  3. package/dist/agents.js +156 -40
  4. package/dist/api.js +10 -6
  5. package/dist/app-git.js +10 -26
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +15 -44
  8. package/dist/building.js +46 -0
  9. package/dist/calls.js +71 -0
  10. package/dist/chatgpt.js +32 -0
  11. package/dist/clis.js +153 -76
  12. package/dist/codex-main.js +58 -0
  13. package/dist/connection.js +116 -0
  14. package/dist/conversations.js +730 -332
  15. package/dist/copies.js +185 -123
  16. package/dist/end.js +30 -5
  17. package/dist/enrol-main.js +7 -1
  18. package/dist/events.js +546 -0
  19. package/dist/files.js +139 -17
  20. package/dist/git.js +13 -11
  21. package/dist/home.js +89 -7
  22. package/dist/house-cli.js +299 -0
  23. package/dist/house-main.js +7 -159
  24. package/dist/instructions.js +8 -10
  25. package/dist/kit-cli.js +68 -0
  26. package/dist/kit-main.js +8 -22
  27. package/dist/login.js +118 -107
  28. package/dist/logout.js +14 -16
  29. package/dist/mcp.js +6 -2
  30. package/dist/parts.js +82 -0
  31. package/dist/placement.js +39 -0
  32. package/dist/relay.js +33 -2
  33. package/dist/resident.js +115 -49
  34. package/dist/scope.js +214 -0
  35. package/dist/scripts.js +255 -0
  36. package/dist/service-main.js +9 -0
  37. package/dist/service.js +55 -0
  38. package/dist/sign-in.js +23 -40
  39. package/dist/silence.js +28 -0
  40. package/dist/skills.js +49 -35
  41. package/dist/transcription.js +85 -45
  42. package/dist/translate.js +2 -8
  43. package/dist/update.js +115 -54
  44. package/dist/wire.js +22 -0
  45. package/dist/work.js +346 -0
  46. package/package.json +20 -20
  47. package/dist/blocks.js +0 -51
  48. package/dist/bootstrap.json +0 -24
  49. package/dist/clis-main.js +0 -23
  50. package/dist/stream.js +0 -72
  51. package/how-we-work.md +0 -23
  52. package/skills/ask-matt/LICENSE +0 -21
  53. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  54. package/skills/ask-matt/SKILL.md +0 -91
  55. package/skills/ask-matt/agents/openai.yaml +0 -4
  56. package/skills/code-review/LICENSE +0 -21
  57. package/skills/code-review/SKILL.md +0 -89
  58. package/skills/code-review/agents/openai.yaml +0 -3
  59. package/skills/codebase-design/DEEPENING.md +0 -37
  60. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  61. package/skills/codebase-design/LICENSE +0 -21
  62. package/skills/codebase-design/SKILL.md +0 -116
  63. package/skills/codebase-design/agents/openai.yaml +0 -3
  64. package/skills/diagnosing-bugs/LICENSE +0 -21
  65. package/skills/diagnosing-bugs/SKILL.md +0 -140
  66. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  67. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  68. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  69. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  70. package/skills/domain-modeling/LICENSE +0 -21
  71. package/skills/domain-modeling/SKILL.md +0 -76
  72. package/skills/domain-modeling/agents/openai.yaml +0 -3
  73. package/skills/grill-me/LICENSE +0 -21
  74. package/skills/grill-me/SKILL.md +0 -8
  75. package/skills/grill-me/agents/openai.yaml +0 -4
  76. package/skills/grill-with-docs/LICENSE +0 -21
  77. package/skills/grill-with-docs/SKILL.md +0 -8
  78. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  79. package/skills/grilling/LICENSE +0 -21
  80. package/skills/grilling/SKILL.md +0 -30
  81. package/skills/grilling/agents/openai.yaml +0 -3
  82. package/skills/handoff/LICENSE +0 -21
  83. package/skills/handoff/SKILL.md +0 -17
  84. package/skills/handoff/agents/openai.yaml +0 -4
  85. package/skills/implement/LICENSE +0 -21
  86. package/skills/implement/SKILL.md +0 -16
  87. package/skills/implement/agents/openai.yaml +0 -4
  88. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  89. package/skills/improve-codebase-architecture/LICENSE +0 -21
  90. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  91. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  92. package/skills/prototype/LICENSE +0 -21
  93. package/skills/prototype/LOGIC.md +0 -67
  94. package/skills/prototype/SKILL.md +0 -28
  95. package/skills/prototype/UI.md +0 -112
  96. package/skills/prototype/agents/openai.yaml +0 -3
  97. package/skills/research/LICENSE +0 -21
  98. package/skills/research/SKILL.md +0 -14
  99. package/skills/research/agents/openai.yaml +0 -3
  100. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  101. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  102. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  103. package/skills/tdd/LICENSE +0 -21
  104. package/skills/tdd/SKILL.md +0 -40
  105. package/skills/tdd/agents/openai.yaml +0 -3
  106. package/skills/tdd/mocking.md +0 -59
  107. package/skills/tdd/tests.md +0 -77
  108. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  109. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  110. package/skills/teach/LICENSE +0 -21
  111. package/skills/teach/MISSION-FORMAT.md +0 -31
  112. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  113. package/skills/teach/SKILL.md +0 -141
  114. package/skills/teach/agents/openai.yaml +0 -4
  115. package/skills/to-questionnaire/LICENSE +0 -21
  116. package/skills/to-questionnaire/SKILL.md +0 -55
  117. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  118. package/skills/to-spec/LICENSE +0 -21
  119. package/skills/to-spec/SKILL.md +0 -76
  120. package/skills/to-spec/agents/openai.yaml +0 -4
  121. package/skills/to-tickets/LICENSE +0 -21
  122. package/skills/to-tickets/SKILL.md +0 -106
  123. package/skills/to-tickets/agents/openai.yaml +0 -4
  124. package/skills/wait-what/LICENSE +0 -21
  125. package/skills/wait-what/SKILL.md +0 -8
  126. package/skills/wait-what/agents/openai.yaml +0 -4
  127. package/skills/wayfinder/LICENSE +0 -21
  128. package/skills/wayfinder/SKILL.md +0 -129
  129. package/skills/wayfinder/agents/openai.yaml +0 -4
  130. package/skills/writing-for-agents/LICENSE +0 -21
  131. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  132. package/skills/writing-for-agents/SKILL.md +0 -83
  133. 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, realpath } from 'node:fs/promises';
3
- import { killTree, startAdapter } from './acp.js';
4
- import { CLIS, KIT_VERSION, below, installAdapter, installCli, locate, run, versionOf } from './clis.js';
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
- if (adapter !== undefined)
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.minimum === undefined ? {} : { minimum: reading.minimum }),
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
- function tooOld(kind, release) {
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
- constructor(house) {
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
- return reading.then(({ agents, transcribe }) => {
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(resolved.failure ?? tooOld(kind, resolved.release));
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 || known?.minimum !== undefined ? SIGNED_OUT : (known ?? SIGNED_OUT);
166
- if (known?.release !== release || known.minimum !== undefined) {
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
- offer(kind, cli, kept)
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
- return this.queue(kind, () => this.ensure(kind, signal), signal);
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
- await this.examine(kind, known, async (kind, cli) => ((await signedIn(kind, cli, known, signal)) ? { ...known, signed_in: true } : SIGNED_OUT), signal);
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
- update(kind, reading) {
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 realpath(await this.ensure(kind, signal));
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.readings.set(kind, reading);
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, offer);
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
- kit_version: KIT_VERSION,
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 { end, SIGN_IN_AGAIN } from './end.js';
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 Kits one outage dropped and retries within half a minute of House's return.
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 on the jittered curve.
32
- await delay(retryDelay(attempt), undefined, { signal });
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
- end(SIGN_IN_AGAIN);
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
- import { quoted } from './shell.js';
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 forget(house) {
24
- await configured(['--remove-section', section(house)]);
25
- }
26
- async function register(house) {
27
- await forget(house);
28
- for (const value of ['', helper()]) {
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;
@@ -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
- setGlobalProxyFromEnv();
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, uuidOf } from './operation.js';
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 = 4 * 1024 * 1024;
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 write House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
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
- let mutations = null;
145
- const writes = async (tool) => {
146
- mutations ??= house
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 === '/' || path === ATTACHMENTS ? forward(path, text) : { status: 404, text: '' };
167
+ return path === '/' ? forward(text) : { status: 404, text: '' };
196
168
  };
197
- const sockets = join(kitHome(), 'bridges');
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
- response.writeHead(502).end(request.url === '/git/push' && error instanceof Error ? error.message : UNREACHABLE);
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('/', JSON.stringify(call(name, args)));
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);
@@ -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
+ }