@agentshouse/kit 0.2.1-cb345f4157685 → 0.2.1-cb8dc89b78e07

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 CHANGED
@@ -46,8 +46,8 @@ this computer as a new one when House no longer holds it.
46
46
  In a container you do not control, such as ChatGPT's, `npm i -g @agentshouse/kit` and `kit chatgpt --key <key>` with a key made on Connections let `house <tool>` act as you, starting and configuring nothing else.
47
47
 
48
48
  `kit resident` keeps the Environment connected and, as it connects, copies the text of each Room you chose for it as a
49
- Git repository in the workspace's `local-copies`, and erases a Room's copy, unpushed commits included, once the Room
50
- may no longer be copied here. `house fetch` does the same and records House's changes on each copy's
49
+ Git repository in the workspace's `local-copies`, and keeps a Room's copy as it is, no longer refreshing it, once the
50
+ Room may no longer be copied here. `house fetch` does the same and records House's changes on each copy's
51
51
  `refs/house/received`; rebase onto it, commit and run `house push --owner` in the copy, through the install command in
52
52
  the container. An Agent runs both, without `--owner`. `house upload` and `house download` move a Room's other files.
53
53
 
package/dist/agents.js CHANGED
@@ -10,6 +10,7 @@ import { KIT_WIRE } from './wire.js';
10
10
  // Two quiet seconds let a fresh conversation start before background model discovery competes for its CPU.
11
11
  const MODEL_PROBE_QUIET_MS = 2_000;
12
12
  const SIGNED_OUT = { signed_in: false, models: [], modes: [] };
13
+ const INSTALLING = { release: null, failure: null, installing: true, ...SIGNED_OUT };
13
14
  function select(options, category) {
14
15
  const option = options.find((candidate) => candidate.category === category);
15
16
  return option?.type === 'select' ? option : undefined;
@@ -117,6 +118,7 @@ function reported(kind, reading) {
117
118
  release: reading.release,
118
119
  ...(reading.minimum === undefined ? {} : { minimum: reading.minimum }),
119
120
  failure: reading.failure,
121
+ ...(reading.installing === undefined ? {} : { installing: reading.installing }),
120
122
  signed_in: reading.signed_in,
121
123
  models: reading.models,
122
124
  modes: reading.modes,
@@ -162,6 +164,7 @@ export class Agents {
162
164
  reading = Promise.resolve();
163
165
  queues = new Map();
164
166
  installing = Promise.resolve();
167
+ installs = new Map();
165
168
  readings = new Map();
166
169
  reads = new Map();
167
170
  starts = 0;
@@ -217,7 +220,7 @@ export class Agents {
217
220
  const { cli, release } = resolved;
218
221
  const known = this.readings.get(kind);
219
222
  const kept = known?.release === null || known?.minimum !== undefined ? SIGNED_OUT : (known ?? SIGNED_OUT);
220
- if (known?.release !== release || known.minimum !== undefined) {
223
+ if (known?.release !== release || known.minimum !== undefined || this.installs.has(kind)) {
221
224
  this.update(kind, { release, failure: null, signed_in: kept.signed_in, models: kept.models, modes: kept.modes });
222
225
  }
223
226
  const read = this.nextRead(kind);
@@ -232,7 +235,8 @@ export class Agents {
232
235
  });
233
236
  }
234
237
  located(kind, signal) {
235
- return this.queue(kind, () => this.ensure(kind, signal), signal);
238
+ const owner = {};
239
+ return this.queue(kind, () => this.ensure(kind, signal, owner).finally(() => this.settle(kind, owner)), signal);
236
240
  }
237
241
  async reread(kind) {
238
242
  await this.examine(kind, this.readings.get(kind) ?? SIGNED_OUT, offer);
@@ -240,7 +244,13 @@ export class Agents {
240
244
  }
241
245
  async recheck(kind, signal) {
242
246
  const known = this.readings.get(kind) ?? SIGNED_OUT;
243
- await this.examine(kind, known, async (kind, cli) => ((await signedIn(kind, cli, known, signal)) ? { ...known, signed_in: true } : SIGNED_OUT), signal);
247
+ const owner = {};
248
+ try {
249
+ await this.examine(kind, known, async (kind, cli) => ((await signedIn(kind, cli, known, signal)) ? { ...known, signed_in: true } : SIGNED_OUT), signal, owner);
250
+ }
251
+ finally {
252
+ this.settle(kind, owner);
253
+ }
244
254
  if (!signal.aborted)
245
255
  await this.report();
246
256
  }
@@ -254,8 +264,18 @@ export class Agents {
254
264
  this.reads.set(kind, read);
255
265
  return read;
256
266
  }
257
- update(kind, reading) {
267
+ hold(kind, reading) {
258
268
  this.readings.set(kind, reading);
269
+ this.installs.delete(kind);
270
+ }
271
+ update(kind, reading) {
272
+ this.hold(kind, reading);
273
+ this.report().catch(logged);
274
+ }
275
+ settle(kind, owner) {
276
+ if (this.installs.get(kind) !== owner)
277
+ return;
278
+ this.installs.delete(kind);
259
279
  this.report().catch(logged);
260
280
  }
261
281
  queue(kind, work, signal) {
@@ -266,13 +286,15 @@ export class Agents {
266
286
  this.queues.set(kind, queued.catch(() => undefined));
267
287
  return signal === undefined ? queued : Promise.race([queued, ended(signal)]);
268
288
  }
269
- async ensure(kind, signal) {
289
+ async ensure(kind, signal, owner = {}) {
270
290
  const failed = await installAdapter(kind, signal);
271
291
  if (failed !== null)
272
292
  throw new Error(failed);
273
293
  const found = await locate(kind, signal);
274
294
  if (found !== null)
275
295
  return found;
296
+ this.installs.set(kind, owner);
297
+ this.report().catch(logged);
276
298
  const installing = this.installing.catch(() => undefined).then(() => installCli(kind, signal));
277
299
  this.installing = installing;
278
300
  const failure = await installing;
@@ -283,10 +305,10 @@ export class Agents {
283
305
  throw new Error(`the login shell does not find ${CLIS[kind].bin} after its install`);
284
306
  return installed;
285
307
  }
286
- async resolve(kind, signal) {
308
+ async resolve(kind, signal, owner) {
287
309
  let cli;
288
310
  try {
289
- cli = await realpath(await this.ensure(kind, signal));
311
+ cli = await realpath(await this.ensure(kind, signal, owner));
290
312
  }
291
313
  catch (error) {
292
314
  const failure = causeOf(error);
@@ -301,12 +323,12 @@ export class Agents {
301
323
  return { release, minimum, failure: null, ...SIGNED_OUT };
302
324
  return { cli, release };
303
325
  }
304
- async examine(kind, known, read, signal) {
326
+ async examine(kind, known, read, signal, owner) {
305
327
  const current = this.nextRead(kind);
306
- const resolved = await this.queue(kind, () => this.resolve(kind, signal), signal);
328
+ const resolved = await this.queue(kind, () => this.resolve(kind, signal, owner), signal);
307
329
  const reading = 'cli' in resolved ? { release: resolved.release, failure: null, ...(await read(kind, resolved.cli, known)) } : resolved;
308
330
  if (this.reads.get(kind) === current && !signal?.aborted)
309
- this.readings.set(kind, reading);
331
+ this.hold(kind, reading);
310
332
  }
311
333
  async quietOffer(kind, cli, known) {
312
334
  for (let seen = -1; seen !== this.starts;) {
@@ -324,7 +346,7 @@ export class Agents {
324
346
  }
325
347
  async send() {
326
348
  const agents = this.desired.agents.flatMap((kind) => {
327
- const reading = this.readings.get(kind);
349
+ const reading = this.installs.has(kind) ? INSTALLING : this.readings.get(kind);
328
350
  return reading === undefined ? [] : [reported(kind, reading)];
329
351
  });
330
352
  const hostKey = await sshHostKey();
package/dist/api.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { setTimeout as delay } from 'node:timers/promises';
2
- import { signInAgain, updateRequired } 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 { KIT_WIRE, KIT_WIRE_HEADER, tooOld } from './wire.js';
6
+ import { endUnserved, KIT_WIRE, KIT_WIRE_HEADER } from './wire.js';
7
7
  export class HouseRefusal extends Error {
8
8
  status;
9
9
  text;
@@ -45,8 +45,8 @@ export async function post(house, path, body, credential, signal) {
45
45
  signal,
46
46
  });
47
47
  const text = await answer.text();
48
- if (!answer.ok && tooOld(text))
49
- updateRequired();
48
+ if (!answer.ok)
49
+ endUnserved(text);
50
50
  if (!answer.ok)
51
51
  throw new HouseRefusal(path, answer.status, text);
52
52
  return JSON.parse(text);
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "version": "0.2.1-cb345f4157685",
3
+ "version": "0.2.1-cb8dc89b78e07",
4
4
  "hosts": {
5
5
  "macos": {
6
6
  "asset": {
7
7
  "name": "connect-linux.sh",
8
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cb345f4157685/connect-linux.sh"
8
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cb8dc89b78e07/connect-linux.sh"
9
9
  }
10
10
  },
11
11
  "linux": {
12
12
  "asset": {
13
13
  "name": "connect-linux.sh",
14
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cb345f4157685/connect-linux.sh"
14
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cb8dc89b78e07/connect-linux.sh"
15
15
  }
16
16
  },
17
17
  "windows": {
18
18
  "asset": {
19
19
  "name": "connect-windows.ps1",
20
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cb345f4157685/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cb8dc89b78e07/connect-windows.ps1"
21
21
  }
22
22
  }
23
23
  }
@@ -31,6 +31,7 @@ const ENDED = {
31
31
  const NO_START = "the Agent's CLI did not start";
32
32
  const NO_SESSION = "the Agent's CLI did not open a session";
33
33
  const NO_RESUME = "the Agent's CLI could not resume this conversation";
34
+ const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
34
35
  const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
35
36
  class NotStarted extends Error {
36
37
  }
@@ -42,6 +43,14 @@ async function step(reason, work) {
42
43
  throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
43
44
  }
44
45
  }
46
+ function signedOut(error) {
47
+ return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
48
+ }
49
+ function unopened(reason) {
50
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
51
+ }
52
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
53
+ const STARTING_SHOWN_AFTER_MS = 1_000;
45
54
  function unbridged(error) {
46
55
  return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
47
56
  }
@@ -58,8 +67,7 @@ function causeOf(error) {
58
67
  return error instanceof Error ? error.message : String(error);
59
68
  }
60
69
  function answeredWith(error) {
61
- const signedOut = typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
62
- return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
70
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
63
71
  }
64
72
  function exitedWith(running, cause) {
65
73
  return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
@@ -83,10 +91,10 @@ function marker(bridge) {
83
91
  class Turn {
84
92
  id = randomUUID();
85
93
  parts;
86
- admitted = false;
87
94
  questions = 0;
88
95
  secrets = 0;
89
96
  prompted = false;
97
+ taken = null;
90
98
  ended = false;
91
99
  images = 0;
92
100
  uploads = [];
@@ -100,6 +108,8 @@ class Conversation {
100
108
  session = null;
101
109
  turn = null;
102
110
  commands = null;
111
+ options = null;
112
+ exitUntold = false;
103
113
  context = null;
104
114
  leftovers = new Set();
105
115
  waiting = [];
@@ -107,6 +117,7 @@ class Conversation {
107
117
  subagents = new Map();
108
118
  held = [];
109
119
  opening = null;
120
+ launch = null;
110
121
  prompted = null;
111
122
  kills = 0;
112
123
  killed = new AbortController();
@@ -115,6 +126,11 @@ class Conversation {
115
126
  open = 0;
116
127
  queue = Promise.resolve();
117
128
  reports = Promise.resolve();
129
+ starting = null;
130
+ preparing = null;
131
+ told = null;
132
+ untold = false;
133
+ shown = null;
118
134
  constructor(id) {
119
135
  this.id = id;
120
136
  }
@@ -187,6 +203,14 @@ export class Conversations {
187
203
  }
188
204
  opened() {
189
205
  this.watched = false;
206
+ for (const conversation of this.conversations.values()) {
207
+ if (conversation.running !== null)
208
+ this.facts(conversation);
209
+ else if (conversation.exitUntold)
210
+ this.process(conversation, false);
211
+ if (conversation.untold || conversation.told !== null)
212
+ this.retell(conversation);
213
+ }
190
214
  }
191
215
  watching(watched) {
192
216
  const resend = watched && !this.watched;
@@ -208,9 +232,10 @@ export class Conversations {
208
232
  }
209
233
  async carry(conversation, input) {
210
234
  if (input.kind === 'open')
211
- return { ack: Promise.resolve(await this.open(conversation, input)) };
235
+ return { ack: this.open(conversation, input) };
212
236
  if (input.kind === 'message')
213
237
  return this.message(conversation, input);
238
+ await this.launched(conversation);
214
239
  if (input.kind === 'interrupt') {
215
240
  if (--conversation.interrupts === 0)
216
241
  conversation.interrupted = new AbortController();
@@ -234,22 +259,58 @@ export class Conversations {
234
259
  }
235
260
  return { ack: Promise.resolve({}) };
236
261
  }
237
- async open(conversation, input) {
262
+ open(conversation, input) {
238
263
  if (conversation.running !== null)
239
- return {};
240
- try {
241
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
242
- }
243
- catch (error) {
244
- return { refused: refusal(conversation, error) };
245
- }
264
+ return Promise.resolve({});
265
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, input.agent_id), null, true);
266
+ return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
267
+ }
268
+ launch(conversation, route, sessionId, empty) {
269
+ let opened;
270
+ let failed;
271
+ const bridge = new Promise((resolve, reject) => {
272
+ opened = resolve;
273
+ failed = reject;
274
+ });
275
+ bridge.catch(() => undefined);
276
+ const session = this.start(conversation, route, sessionId, empty, opened);
277
+ session.catch(failed);
278
+ const launch = { bridge, session };
279
+ conversation.launch = launch;
280
+ void session
281
+ .catch(() => undefined)
282
+ .then(() => {
283
+ if (conversation.launch !== launch)
284
+ return;
285
+ conversation.launch = null;
286
+ this.next(conversation);
287
+ });
288
+ this.next(conversation);
289
+ return launch;
290
+ }
291
+ async launched(conversation) {
292
+ while (conversation.launch !== null)
293
+ await conversation.launch.session.catch(() => undefined);
294
+ }
295
+ instructed(conversation, route) {
296
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
297
+ if (bridge === undefined)
298
+ return null;
299
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
300
+ read.catch(() => undefined);
301
+ return read;
246
302
  }
247
303
  async message(conversation, input) {
248
304
  const content = [{ type: 'text', text: String(input.text) }];
249
305
  let route;
306
+ let instructing = null;
250
307
  try {
251
308
  route = await this.route(conversation, input.agent_id);
309
+ if (input.first === true)
310
+ instructing = this.instructed(conversation, route);
252
311
  const files = input.files;
312
+ if (files.length > 0)
313
+ this.prepares(conversation, 'files');
253
314
  const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
254
315
  const lines = await this.transcribed(conversation, files, paths);
255
316
  if (lines.length > 0)
@@ -258,11 +319,13 @@ export class Conversations {
258
319
  throw new Error(KILLED);
259
320
  }
260
321
  catch (error) {
322
+ this.prepares(conversation, null);
261
323
  return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
262
324
  }
325
+ conversation.preparing = null;
263
326
  return {
264
327
  ack: new Promise((settle) => {
265
- conversation.waiting.push({ input, route, content, settle });
328
+ conversation.waiting.push({ input, route, content, instructing, settle });
266
329
  this.next(conversation);
267
330
  }),
268
331
  };
@@ -271,8 +334,12 @@ export class Conversations {
271
334
  if (conversation.sending !== null || conversation.open > 0)
272
335
  return;
273
336
  const waiting = conversation.waiting.shift();
274
- if (waiting === undefined)
337
+ if (waiting === undefined) {
338
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
339
+ if (conversation.starting !== step)
340
+ this.starts(conversation, step);
275
341
  return;
342
+ }
276
343
  conversation.sending = this.send(conversation, waiting).finally(() => {
277
344
  conversation.sending = null;
278
345
  this.next(conversation);
@@ -282,27 +349,33 @@ export class Conversations {
282
349
  const { input } = waiting;
283
350
  let ack = {};
284
351
  let started = false;
352
+ let instructing = waiting.instructing;
285
353
  const prompt = [];
286
354
  try {
355
+ if (conversation.running === null) {
356
+ this.starts(conversation, 'session');
357
+ await this.launched(conversation);
358
+ }
287
359
  if (conversation.running === null) {
288
360
  const stored = conversation.session ?? input.provider_session_id;
289
361
  const empty = input.first === true && conversation.prompted !== stored;
290
- started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
362
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
363
+ if (input.first === true)
364
+ instructing = this.instructed(conversation, waiting.route);
365
+ started = (await launch.session) !== stored;
291
366
  }
292
367
  const session = conversation.running.sessionId;
293
368
  if (started || (input.first === true && session !== input.provider_session_id)) {
294
369
  ack = { provider_session_id: session };
295
370
  }
296
371
  if (input.first === true) {
297
- prompt.push({
298
- type: 'text',
299
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
300
- });
372
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
301
373
  }
302
374
  if (conversation.kills > 0)
303
375
  throw new Error(KILLED);
304
376
  }
305
377
  catch (error) {
378
+ this.starts(conversation, null);
306
379
  if (started) {
307
380
  conversation.session = null;
308
381
  await this.kill(conversation);
@@ -310,15 +383,45 @@ export class Conversations {
310
383
  waiting.settle({ refused: refusal(conversation, error) });
311
384
  return;
312
385
  }
313
- this.prompt(conversation, [...prompt, ...waiting.content]);
386
+ this.starts(conversation, null);
387
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.inputs.hands(String(input.input_id), ack));
314
388
  waiting.settle(ack);
315
389
  }
390
+ starts(conversation, step) {
391
+ conversation.starting = step;
392
+ if (conversation.shown !== null)
393
+ clearTimeout(conversation.shown);
394
+ conversation.shown = null;
395
+ if (step === null || conversation.told !== null) {
396
+ this.tell(conversation, step);
397
+ return;
398
+ }
399
+ conversation.shown = setTimeout(() => {
400
+ conversation.shown = null;
401
+ if (conversation.starting === step)
402
+ this.tell(conversation, step);
403
+ }, STARTING_SHOWN_AFTER_MS);
404
+ }
405
+ prepares(conversation, step) {
406
+ conversation.preparing = step;
407
+ this.next(conversation);
408
+ }
409
+ tell(conversation, step) {
410
+ if (conversation.told === step && !conversation.untold)
411
+ return;
412
+ conversation.told = step;
413
+ this.retell(conversation);
414
+ }
415
+ retell(conversation) {
416
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
417
+ }
316
418
  async transcribed(conversation, files, paths) {
317
419
  const lines = [];
318
420
  for (const [index, path] of paths.entries()) {
319
421
  lines.push(path);
320
422
  if (files[index].media_type?.startsWith('audio/') !== true)
321
423
  continue;
424
+ this.prepares(conversation, 'transcription');
322
425
  try {
323
426
  const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
324
427
  if (text !== null && text !== '')
@@ -385,7 +488,8 @@ export class Conversations {
385
488
  }
386
489
  return route;
387
490
  }
388
- async start(conversation, route, sessionId, empty) {
491
+ async start(conversation, routed, sessionId, empty, bridged) {
492
+ const route = await routed;
389
493
  const reader = readerFor(route.kind);
390
494
  reader.session = sessionId;
391
495
  const app = client({ name: '@agentshouse/kit' })
@@ -404,6 +508,7 @@ export class Conversations {
404
508
  let adapter = null;
405
509
  try {
406
510
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
511
+ bridged(bridge);
407
512
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
408
513
  const streamed = await step(NO_CONNECTION, work.open(cli));
409
514
  adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
@@ -415,21 +520,19 @@ export class Conversations {
415
520
  const cwd = route.working_directory;
416
521
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
417
522
  const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
418
- const opened = resumed ?? (await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta })));
523
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
419
524
  reader.session = opened.sessionId;
420
525
  if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
421
526
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
422
- await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
527
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
423
528
  }
424
529
  const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
425
530
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
426
531
  void work.settled().then(() => scope.record());
427
532
  conversation.running = running;
428
533
  conversation.session = running.sessionId;
429
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
430
- if (conversation.commands !== null)
431
- this.commands(conversation, conversation.commands);
432
- this.options(conversation, options);
534
+ conversation.options = options;
535
+ this.facts(conversation);
433
536
  void adapter.lost.then((cause) => this.exited(conversation, running, cause));
434
537
  return running.sessionId;
435
538
  }
@@ -450,7 +553,7 @@ export class Conversations {
450
553
  }
451
554
  catch (error) {
452
555
  if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
453
- throw new NotStarted(NO_RESUME, { cause: error });
556
+ throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
454
557
  }
455
558
  logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
456
559
  return null;
@@ -490,11 +593,22 @@ export class Conversations {
490
593
  conversation.leftovers.add(running.scope);
491
594
  conversation.running = null;
492
595
  conversation.commands = null;
596
+ conversation.options = null;
493
597
  for (const [id, question] of this.questions) {
494
598
  if (question.conversation === conversation)
495
599
  this.questions.delete(id);
496
600
  }
497
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
601
+ this.process(conversation, false);
602
+ }
603
+ process(conversation, running) {
604
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
605
+ }
606
+ facts(conversation) {
607
+ this.process(conversation, true);
608
+ if (conversation.commands !== null)
609
+ this.commands(conversation, conversation.commands);
610
+ if (conversation.options !== null)
611
+ this.options(conversation, conversation.options);
498
612
  }
499
613
  commands(conversation, commands) {
500
614
  conversation.commands = commands;
@@ -505,6 +619,7 @@ export class Conversations {
505
619
  options(conversation, options) {
506
620
  if (conversation.running === null)
507
621
  return;
622
+ conversation.options = options;
508
623
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
509
624
  }
510
625
  deliver(conversation, turn, event, body) {
@@ -519,7 +634,7 @@ export class Conversations {
519
634
  return delivered;
520
635
  }
521
636
  write(conversation, turn, operation) {
522
- if (!turn.admitted || turn.ended || !this.watched)
637
+ if (turn.ended || !this.watched)
523
638
  return;
524
639
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
525
640
  }
@@ -530,12 +645,7 @@ export class Conversations {
530
645
  begin(conversation) {
531
646
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
532
647
  conversation.open++;
533
- void this.deliver(conversation, turn, 'started', {})
534
- .then(() => {
535
- turn.admitted = true;
536
- this.replay(conversation, turn);
537
- })
538
- .catch(logged);
648
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
539
649
  conversation.turn = turn;
540
650
  if (conversation.context !== null)
541
651
  turn.parts.measure(conversation.context);
@@ -568,12 +678,14 @@ export class Conversations {
568
678
  this.next(conversation);
569
679
  this.kit.changed();
570
680
  }
571
- prompt(conversation, prompt) {
681
+ prompt(conversation, prompt, taken) {
572
682
  const running = conversation.running;
573
683
  const turn = this.begin(conversation);
574
684
  turn.prompted = true;
685
+ turn.taken = taken;
575
686
  conversation.prompted = running.sessionId;
576
687
  running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
688
+ this.took(turn);
577
689
  const notice = answeredFailure(answer);
578
690
  if (notice !== null) {
579
691
  this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
@@ -586,6 +698,7 @@ export class Conversations {
586
698
  this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
587
699
  }
588
700
  else {
701
+ this.took(turn);
589
702
  if (conversation.open === 1 && conversation.running === running) {
590
703
  running.adapter.retire();
591
704
  this.detach(conversation, running);
@@ -595,7 +708,9 @@ export class Conversations {
595
708
  });
596
709
  }
597
710
  started(conversation) {
598
- return conversation.turn ?? this.begin(conversation);
711
+ const turn = conversation.turn ?? this.begin(conversation);
712
+ this.took(turn);
713
+ return turn;
599
714
  }
600
715
  ended(conversation) {
601
716
  const turn = conversation.turn;
@@ -690,6 +805,11 @@ export class Conversations {
690
805
  }
691
806
  this.apply(conversation, this.started(conversation), event);
692
807
  }
808
+ took(turn) {
809
+ const taken = turn.taken;
810
+ turn.taken = null;
811
+ taken?.();
812
+ }
693
813
  apply(conversation, turn, event) {
694
814
  switch (event.kind) {
695
815
  case 'text':
package/dist/copies.js CHANGED
@@ -58,6 +58,9 @@ function expiredAuthority(error) {
58
58
  return undefined;
59
59
  }
60
60
  }
61
+ function grouped(items, size) {
62
+ return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
63
+ }
61
64
  function covers(path, prefix) {
62
65
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
63
66
  }
@@ -215,7 +218,7 @@ export class LocalCopies {
215
218
  async mapping() {
216
219
  await this.answering;
217
220
  return [...this.copies.values()]
218
- .filter((copy) => copy.erasing === undefined)
221
+ .filter((copy) => copy.unfollowed === undefined)
219
222
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
220
223
  .sort();
221
224
  }
@@ -247,30 +250,24 @@ export class LocalCopies {
247
250
  return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
248
251
  const said = [];
249
252
  let refused = false;
253
+ const mark = refreshing ? 'said' : 'unsaid';
250
254
  for (const copy of held()) {
251
- const returned = synced.has(copy.key);
252
- if (returned && copy.erasing === undefined)
255
+ if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
253
256
  continue;
254
- try {
255
- await this.erase(copy);
256
- said.push(`${rootOf(copy.room_handle)}: ${returned ? '' : 'no longer synced to this Environment; '}erased from the Local copy`);
257
- }
258
- catch (error) {
259
- refused = true;
260
- said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
261
- }
257
+ await this.save({ ...copy, unfollowed: mark });
258
+ if (refreshing)
259
+ said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
262
260
  }
263
261
  const current = [];
264
262
  for (const [key, handle] of synced) {
265
263
  try {
266
264
  const held = this.copies.get(key);
267
- if (held?.erasing)
268
- continue;
269
265
  if (held === undefined || !(await exists(held.repository))) {
270
266
  said.push(await this.materialize(key, handle));
271
267
  continue;
272
268
  }
273
- current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
269
+ const { unfollowed, ...followed } = held;
270
+ current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
274
271
  }
275
272
  catch (error) {
276
273
  refused = true;
@@ -278,13 +275,12 @@ export class LocalCopies {
278
275
  }
279
276
  }
280
277
  if (refreshing && current.length > 0) {
281
- try {
282
- const refreshed = await this.refreshed(current);
283
- current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
284
- }
285
- catch (error) {
278
+ const failed = await this.refreshed(current).then(() => null, houseCause);
279
+ for (const copy of current)
280
+ said.push(...moved(copy, this.copies.get(copy.key)));
281
+ if (failed !== null) {
286
282
  refused = true;
287
- said.push(houseCause(error));
283
+ said.push(failed);
288
284
  }
289
285
  }
290
286
  return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
@@ -296,8 +292,6 @@ export class LocalCopies {
296
292
  return;
297
293
  await this.exclusive(key, async () => {
298
294
  const copy = this.copies.get(key);
299
- if (copy === undefined || copy.erasing)
300
- return;
301
295
  let moves = copy.moves;
302
296
  for (const { from, to } of report.moves) {
303
297
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -353,12 +347,6 @@ export class LocalCopies {
353
347
  this.copies.set(copy.key, copy);
354
348
  return copy;
355
349
  }
356
- async erase(copy) {
357
- await this.save({ ...copy, erasing: true });
358
- await rm(copy.repository, { recursive: true, force: true });
359
- await rm(recordOf(copy.key), { force: true });
360
- this.copies.delete(copy.key);
361
- }
362
350
  async read(handle) {
363
351
  const prefixes = [rootOf(handle)];
364
352
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
@@ -459,19 +447,24 @@ export class LocalCopies {
459
447
  return next;
460
448
  }
461
449
  async refreshed(copies) {
462
- const discovery = await this.house.post('/kit/door/discover', {
463
- prefixes: copies.map((copy) => rootOf(copy.room_handle)),
464
- });
465
- const regranted = new Map();
450
+ const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map((copy) => rootOf(copy.room_handle)) });
451
+ const [first, ...rest] = copies;
452
+ const { bounds, authorities } = await discover([first]);
453
+ for (const group of grouped(rest, bounds.max_prefixes))
454
+ authorities.push(...(await discover(group)).authorities);
455
+ const unchanged = [];
466
456
  for (const copy of copies) {
467
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
457
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
468
458
  if (authority !== undefined &&
469
459
  JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
470
- regranted.set(copy.key, await this.save(await this.received(copy)));
460
+ await this.save(await this.received(copy));
461
+ }
462
+ else {
463
+ unchanged.push(copy);
471
464
  }
472
465
  }
473
- const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
474
- return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
466
+ for (const group of grouped(unchanged, bounds.max_prefixes))
467
+ await this.caughtUp(group, false);
475
468
  }
476
469
  async caughtUp(copies, whole) {
477
470
  const caught = new Map(copies.map((copy) => [copy.key, copy]));
@@ -553,8 +546,6 @@ export class LocalCopies {
553
546
  }
554
547
  async pushed(key, worktree, selector, caller) {
555
548
  let copy = this.copies.get(key);
556
- if (copy === undefined || copy.erasing)
557
- return outside(worktree);
558
549
  if (copy.push?.state === 'submitted')
559
550
  return this.settle(copy, caller);
560
551
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -669,8 +660,10 @@ export class LocalCopies {
669
660
  }
670
661
  if (outcome.refused.code === 'expired')
671
662
  return this.recovered(copy);
672
- const rebased = rebaseFixes(outcome.refused);
673
663
  let settled = await this.save(without(copy));
664
+ if (settled.unfollowed !== undefined)
665
+ return { refused: true, text: outcome.refused.text };
666
+ const rebased = rebaseFixes(outcome.refused);
674
667
  try {
675
668
  settled = (await this.caughtUp([settled], true))[0];
676
669
  }
package/dist/end.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { KIT_VERSION } from './clis.js';
1
2
  import { forgetRejected } from './home.js';
2
3
  import { installCommand } from './placement.js';
3
4
  import { killDescendants } from './scope.js';
@@ -8,7 +9,7 @@ function exit(reason, status) {
8
9
  process.exit(status);
9
10
  }
10
11
  export function end(reason) {
11
- exit(reason, 0);
12
+ exit(reason, process.env.HOUSE_KIT_RESIDENT === String(process.pid) ? 0 : 1);
12
13
  }
13
14
  export function restart(reason) {
14
15
  exit(reason, 1);
@@ -17,6 +18,9 @@ export function updateRequired() {
17
18
  const command = installCommand();
18
19
  end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
19
20
  }
21
+ export function newerThanHouse(release, command) {
22
+ end(`Kit ${KIT_VERSION} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
23
+ }
20
24
  export async function signInAgain(rejected) {
21
25
  await forgetRejected(rejected);
22
26
  restart('sign in again');
package/dist/events.js CHANGED
@@ -2,7 +2,7 @@ const COMMAND = { shown: 'command' };
2
2
  const HIDDEN = { shown: 'hidden' };
3
3
  const FINISHED = new Set(['completed', 'failed']);
4
4
  export const RUNNING_JOB = new Set(['running', 'paused']);
5
- const ROUTINE_TARGETS = { send_routine_request: 'access', cancel_routine_request: 'request' };
5
+ const MESSAGE_TARGETS = { send_routine_request: 'access', cancel_routine_request: 'request', send_mail: 'request' };
6
6
  function words(line) {
7
7
  const found = [];
8
8
  let word = null;
@@ -48,7 +48,7 @@ export function houseMessage(line) {
48
48
  catch {
49
49
  return null;
50
50
  }
51
- const argument = ROUTINE_TARGETS[call.command];
51
+ const argument = MESSAGE_TARGETS[call.command];
52
52
  if (argument === undefined)
53
53
  return null;
54
54
  const target = call.arguments?.[argument];
package/dist/inputs.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { kitHome } from './home.js';
1
4
  function logged(error) {
2
5
  process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
3
6
  }
@@ -5,17 +8,28 @@ export class Inputs {
5
8
  house;
6
9
  changed;
7
10
  carried = new Map();
11
+ path = join(kitHome(), 'handed-inputs.json');
12
+ handed;
13
+ answered = new Set();
8
14
  open = 0;
9
15
  constructor(house, changed) {
10
16
  this.house = house;
11
17
  this.changed = changed;
18
+ this.handed = new Map(existsSync(this.path) ? Object.entries(JSON.parse(readFileSync(this.path, 'utf8'))) : []);
19
+ }
20
+ hands(inputId, ack) {
21
+ if (this.answered.has(inputId))
22
+ return;
23
+ this.handed.set(inputId, ack);
24
+ this.save();
12
25
  }
13
26
  idle() {
14
27
  return this.open === 0;
15
28
  }
16
29
  carry(inputId, work) {
17
30
  const known = this.carried.get(inputId);
18
- const carried = known ?? work();
31
+ const handed = this.handed.get(inputId);
32
+ const carried = known ?? (handed === undefined ? work() : Promise.resolve({ ack: handed }));
19
33
  if (known === undefined) {
20
34
  this.carried.set(inputId, carried);
21
35
  carried.catch(() => this.carried.delete(inputId));
@@ -24,7 +38,14 @@ export class Inputs {
24
38
  this.changed();
25
39
  return carried
26
40
  .then(async ({ ack, acknowledged }) => {
27
- await this.house.deliver(`/kit/inputs/${inputId}/ack`, ack);
41
+ try {
42
+ await this.house.deliver(`/kit/inputs/${inputId}/ack`, ack);
43
+ }
44
+ finally {
45
+ this.answered.add(inputId);
46
+ if (this.handed.delete(inputId))
47
+ this.save();
48
+ }
28
49
  if (known === undefined)
29
50
  await acknowledged?.();
30
51
  })
@@ -34,4 +55,9 @@ export class Inputs {
34
55
  this.changed();
35
56
  });
36
57
  }
58
+ save() {
59
+ const temporary = `${this.path}.${process.pid}`;
60
+ writeFileSync(temporary, JSON.stringify(Object.fromEntries(this.handed)), { mode: 0o600 });
61
+ renameSync(temporary, this.path);
62
+ }
37
63
  }
@@ -4,9 +4,9 @@ const SOURCE_LINE = "From House:";
4
4
  const HOUSE_LINE = "Work with House through the `house` CLI: run `house --help`, and `house find_command` for House's guides and formats.";
5
5
  const FILE_LINE = 'To give the User a file, upload it with `house upload` and link it; for an App, a Site or a website, follow `house find_command \'{"query":"app"}\'`. Never start a server.';
6
6
  const READ_LINE = "Optional per-Room instructions: `/rooms/private/rooms/<handle>/AGENTS.md`.";
7
- const WAIT_LINE = 'To get what another Agent will write into a Room, `create_trigger` on where it will land with a `conversation` action, then end your turn: this conversation continues when it lands.';
7
+ const REQUEST_LINE = "For another Agent's help, `send_routine_request`; its mail comes back into this conversation. If an Agent of your User has no Routine to call, write one in the Private Room and `create_routine_access` with your User as its only caller.";
8
8
  const COPIES_LINE = `House Local copies; before work run \`house fetch\` and rebase onto \`${HOUSE_REF}\`; after it commit, then \`house push\`:`;
9
9
  export async function instructions(bridge, base, copies) {
10
- const lines = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE, WAIT_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
10
+ const lines = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE, REQUEST_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
11
11
  return [lines.join('\n'), base, (await readHowWeWork(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
12
12
  }
package/dist/placement.js CHANGED
@@ -18,9 +18,9 @@ export async function updatesItself() {
18
18
  // sudo answers at once, so ten seconds only bounds a stuck sudo.
19
19
  return (await run('sudo', ['-n', 'true'], 10_000)).status === 0;
20
20
  }
21
- export function installCommand() {
21
+ function recorded(name) {
22
22
  try {
23
- return readFileSync(join(kitHome(), 'install-command'), 'utf8').trim();
23
+ return readFileSync(join(kitHome(), name), 'utf8').trim();
24
24
  }
25
25
  catch (error) {
26
26
  if (error.code === 'ENOENT')
@@ -28,3 +28,12 @@ export function installCommand() {
28
28
  throw error;
29
29
  }
30
30
  }
31
+ export function installCommand() {
32
+ return recorded('install-command');
33
+ }
34
+ export function machine() {
35
+ const placed = recorded('placement');
36
+ if (placed === 'container' || placed === 'server' || placed === 'windows')
37
+ return placed;
38
+ return process.platform === 'darwin' ? 'macos' : 'linux';
39
+ }
package/dist/scope.js CHANGED
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  const LISTING_BYTES = 1 << 28;
5
5
  const CGROUPS = '/sys/fs/cgroup';
6
6
  const GONE = new Set(['ENOENT', 'ESRCH']);
7
+ const EXITED = new Set(['Z', 'X']);
7
8
  function failure(error) {
8
9
  return error.code ?? '';
9
10
  }
@@ -24,6 +25,8 @@ function read(pid) {
24
25
  try {
25
26
  const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
26
27
  const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
28
+ if (EXITED.has(fields[0]))
29
+ return null;
27
30
  const command = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replaceAll('\0', ' ');
28
31
  return { parent: Number(fields[1]), session: fields[3], command };
29
32
  }
package/dist/stream.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { subscribe } from 'node:diagnostics_channel';
2
2
  import { HouseRefusal, retryDelay } from './api.js';
3
- import { end, REPLACED, signInAgain, updateRequired } from './end.js';
3
+ import { end, REPLACED, signInAgain } from './end.js';
4
4
  import { readEnrolment } from './home.js';
5
5
  import { relayed } from './relay.js';
6
- import { KIT_WIRE, tooOld } from './wire.js';
6
+ import { endUnserved, KIT_WIRE } from './wire.js';
7
7
  const PATH = '/kit/stream';
8
8
  const SUBPROTOCOL = `house.kit.stream.${KIT_WIRE}`;
9
9
  const REFUSAL_HEADER = 'x-house-refusal';
@@ -80,8 +80,8 @@ export function holdStream(handlers) {
80
80
  void signInAgain(credential);
81
81
  return;
82
82
  }
83
- if (refused !== null && tooOld(refused.text))
84
- updateRequired();
83
+ if (refused !== null)
84
+ endUnserved(refused.text);
85
85
  if (!opened && refused !== null)
86
86
  process.stderr.write(`kit: ${refused.message}\n`);
87
87
  // A closed socket reopens in one to three seconds, spread so Kits one restart closed return apart; an opening House refused or left unanswered backs off on the jittered curve, retryable or not, since a resident outlives a House deploy.
package/dist/translate.js CHANGED
@@ -3,14 +3,8 @@ function same(left, right) {
3
3
  return JSON.stringify(left) === JSON.stringify(right);
4
4
  }
5
5
  function applied(document, changes) {
6
- let current = document;
7
- for (const change of changes) {
8
- const result = apply(current, change);
9
- if (result.outcome !== 'applied')
10
- return null;
11
- current = result.document;
12
- }
13
- return serialize(current);
6
+ const result = apply(document, changes);
7
+ return result.outcome === 'applied' ? serialize(result.document) : null;
14
8
  }
15
9
  function components(path, before, after, text) {
16
10
  const edits = [];
package/dist/wire.js CHANGED
@@ -1,6 +1,22 @@
1
+ import { newerThanHouse, updateRequired } from './end.js';
2
+ import { machine } from './placement.js';
1
3
  import { refusalOf } from './refusals.js';
2
4
  export const KIT_WIRE = 2;
3
5
  export const KIT_WIRE_HEADER = 'x-house-kit-wire';
4
- export function tooOld(text) {
5
- return refusalOf(text)?.code === 'kit_update_required';
6
+ function newer(text) {
7
+ let body;
8
+ try {
9
+ body = JSON.parse(text);
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ return body?.error?.code === 'kit_newer_than_house' ? body.error : null;
15
+ }
16
+ export function endUnserved(text) {
17
+ if (refusalOf(text)?.code === 'kit_update_required')
18
+ updateRequired();
19
+ const refused = newer(text);
20
+ if (refused !== null)
21
+ newerThanHouse(refused.release, refused.commands[machine()]);
6
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.2.1-cb345f4157685",
3
+ "version": "0.2.1-cb8dc89b78e07",
4
4
  "description": "House Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@agentclientprotocol/sdk": "1.7.0",
31
- "@agentshouse/mdmodel": "0.1.0-c68e2039f1ca8",
31
+ "@agentshouse/mdmodel": "0.1.0-cabd61587647f",
32
32
  "yaml": "2.9.1"
33
33
  },
34
34
  "peerDependencies": {