@agentshouse/kit 0.2.1-cb345f4157685 → 0.2.1-cb66cb2fd0696

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 (140) hide show
  1. package/README.md +28 -28
  2. package/dist/acp.js +58 -7
  3. package/dist/agents.js +85 -40
  4. package/dist/api.js +5 -5
  5. package/dist/authority-main.js +2 -2
  6. package/dist/bridge.js +1 -1
  7. package/dist/building.js +50 -0
  8. package/dist/calls.js +71 -0
  9. package/dist/clis.js +34 -40
  10. package/dist/{stream.js → connection.js} +31 -49
  11. package/dist/conversations.js +423 -139
  12. package/dist/copies.js +36 -43
  13. package/dist/end.js +14 -4
  14. package/dist/events.js +29 -8
  15. package/dist/files.js +139 -37
  16. package/dist/git.js +3 -3
  17. package/dist/home.js +0 -7
  18. package/dist/house-cli.js +95 -87
  19. package/dist/house-main.js +3 -3
  20. package/dist/instructions.js +29 -9
  21. package/dist/kit-cli.js +4 -4
  22. package/dist/kit-main.js +3 -3
  23. package/dist/login.js +4 -4
  24. package/dist/mcp.js +6 -2
  25. package/dist/placement.js +21 -20
  26. package/dist/relay.js +21 -0
  27. package/dist/resident.js +45 -38
  28. package/dist/scope.js +15 -4
  29. package/dist/scripts.js +255 -0
  30. package/dist/service.js +1 -1
  31. package/dist/sign-in.js +22 -21
  32. package/dist/silence.js +28 -0
  33. package/dist/skills.js +42 -97
  34. package/dist/transcription.js +83 -43
  35. package/dist/translate.js +2 -8
  36. package/dist/update.js +91 -125
  37. package/dist/wire.js +19 -3
  38. package/package.json +20 -21
  39. package/dist/bootstrap.json +0 -24
  40. package/dist/clis-main.js +0 -23
  41. package/dist/inputs.js +0 -37
  42. package/how-we-work.md +0 -33
  43. package/skills/ask-matt/LICENSE +0 -21
  44. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  45. package/skills/ask-matt/SKILL.md +0 -97
  46. package/skills/ask-matt/agents/openai.yaml +0 -5
  47. package/skills/code-review/LICENSE +0 -21
  48. package/skills/code-review/SKILL.md +0 -89
  49. package/skills/code-review/agents/openai.yaml +0 -3
  50. package/skills/codebase-design/DEEPENING.md +0 -37
  51. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  52. package/skills/codebase-design/LICENSE +0 -21
  53. package/skills/codebase-design/SKILL.md +0 -114
  54. package/skills/codebase-design/agents/openai.yaml +0 -3
  55. package/skills/diagnosing-bugs/LICENSE +0 -21
  56. package/skills/diagnosing-bugs/SKILL.md +0 -138
  57. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  58. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  59. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  60. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  61. package/skills/domain-modeling/LICENSE +0 -21
  62. package/skills/domain-modeling/SKILL.md +0 -74
  63. package/skills/domain-modeling/agents/openai.yaml +0 -3
  64. package/skills/grill-me/LICENSE +0 -21
  65. package/skills/grill-me/SKILL.md +0 -7
  66. package/skills/grill-me/agents/openai.yaml +0 -5
  67. package/skills/grill-with-docs/LICENSE +0 -21
  68. package/skills/grill-with-docs/SKILL.md +0 -7
  69. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  70. package/skills/grilling/LICENSE +0 -21
  71. package/skills/grilling/SKILL.md +0 -30
  72. package/skills/grilling/agents/openai.yaml +0 -3
  73. package/skills/handoff/LICENSE +0 -21
  74. package/skills/handoff/SKILL.md +0 -16
  75. package/skills/handoff/agents/openai.yaml +0 -5
  76. package/skills/implement/LICENSE +0 -21
  77. package/skills/implement/SKILL.md +0 -17
  78. package/skills/implement/agents/openai.yaml +0 -5
  79. package/skills/implement-spec/LICENSE +0 -21
  80. package/skills/implement-spec/SKILL.md +0 -40
  81. package/skills/implement-spec/agents/openai.yaml +0 -5
  82. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  83. package/skills/improve-codebase-architecture/LICENSE +0 -21
  84. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  85. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  86. package/skills/pr/CREDITS.md +0 -3
  87. package/skills/pr/LICENSE +0 -21
  88. package/skills/pr/SKILL.md +0 -170
  89. package/skills/pr/agents/openai.yaml +0 -3
  90. package/skills/prototype/LICENSE +0 -21
  91. package/skills/prototype/LOGIC.md +0 -67
  92. package/skills/prototype/SKILL.md +0 -26
  93. package/skills/prototype/UI.md +0 -112
  94. package/skills/prototype/agents/openai.yaml +0 -3
  95. package/skills/research/LICENSE +0 -21
  96. package/skills/research/SKILL.md +0 -12
  97. package/skills/research/agents/openai.yaml +0 -3
  98. package/skills/retro/LICENSE +0 -21
  99. package/skills/retro/SKILL.md +0 -44
  100. package/skills/retro/agents/openai.yaml +0 -5
  101. package/skills/tdd/LICENSE +0 -21
  102. package/skills/tdd/SKILL.md +0 -38
  103. package/skills/tdd/agents/openai.yaml +0 -3
  104. package/skills/tdd/mocking.md +0 -59
  105. package/skills/tdd/tests.md +0 -77
  106. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  107. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  108. package/skills/teach/LICENSE +0 -21
  109. package/skills/teach/MISSION-FORMAT.md +0 -31
  110. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  111. package/skills/teach/SKILL.md +0 -140
  112. package/skills/teach/agents/openai.yaml +0 -5
  113. package/skills/to-questionnaire/LICENSE +0 -21
  114. package/skills/to-questionnaire/SKILL.md +0 -54
  115. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  116. package/skills/to-spec/LICENSE +0 -21
  117. package/skills/to-spec/SKILL.md +0 -75
  118. package/skills/to-spec/agents/openai.yaml +0 -5
  119. package/skills/to-tickets/LICENSE +0 -21
  120. package/skills/to-tickets/SKILL.md +0 -105
  121. package/skills/to-tickets/agents/openai.yaml +0 -5
  122. package/skills/triage/AGENT-BRIEF.md +0 -207
  123. package/skills/triage/LICENSE +0 -21
  124. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  125. package/skills/triage/SKILL.md +0 -112
  126. package/skills/triage/agents/openai.yaml +0 -5
  127. package/skills/wait-what/LICENSE +0 -21
  128. package/skills/wait-what/SKILL.md +0 -7
  129. package/skills/wait-what/agents/openai.yaml +0 -5
  130. package/skills/wayfinder/LICENSE +0 -21
  131. package/skills/wayfinder/SKILL.md +0 -128
  132. package/skills/wayfinder/agents/openai.yaml +0 -5
  133. package/skills/wizard/LICENSE +0 -21
  134. package/skills/wizard/SKILL.md +0 -44
  135. package/skills/wizard/agents/openai.yaml +0 -3
  136. package/skills/wizard/template.sh +0 -223
  137. package/skills/writing-for-agents/LICENSE +0 -21
  138. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  139. package/skills/writing-for-agents/SKILL.md +0 -81
  140. package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/copies.js CHANGED
@@ -11,7 +11,7 @@ import { changed } from './translate.js';
11
11
  import { relayed } from './relay.js';
12
12
  const BRANCH = 'main';
13
13
  const PROVENANCE = '_provenance';
14
- const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
14
+ const IDENTITY = { GIT_COMMITTER_NAME: 'Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
15
15
  function causeOf(error) {
16
16
  return error instanceof Error ? error.message : String(error);
17
17
  }
@@ -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
  }
@@ -130,7 +133,7 @@ async function ignoreRules(repository, writable, copy) {
130
133
  ].map(pattern);
131
134
  const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
132
135
  await ruled(join(repository, '.gitignore'), [
133
- '# House Kit writes this file and .ignore. The paths below are read-only House',
136
+ '# Kit writes this file and .ignore. The paths below are read-only House',
134
137
  '# content: Git does not track them, and House refuses a change to them even',
135
138
  '# when this file is edited or a path is force-added.',
136
139
  '/.gitignore',
@@ -145,7 +148,7 @@ async function ignoreRules(repository, writable, copy) {
145
148
  '',
146
149
  ].join('\n'));
147
150
  await ruled(join(repository, '.ignore'), [
148
- '# House Kit writes this file so native search still reads the read-only House',
151
+ '# Kit writes this file so native search still reads the read-only House',
149
152
  '# content that .gitignore keeps out of Git.',
150
153
  `!${PROVENANCE}`,
151
154
  ...[...house, ...granted].map((rule) => `!${rule}`),
@@ -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 });
@@ -400,7 +388,7 @@ export class LocalCopies {
400
388
  throw new Error('House serves nothing of it to copy');
401
389
  const repository = join(localCopies(), 'rooms', read.anchor);
402
390
  if (await exists(repository))
403
- throw new Error(`${repository} already exists and House Kit leaves it as it is`);
391
+ throw new Error(`${repository} already exists and Kit leaves it as it is`);
404
392
  const staging = join(dirname(repository), `.${randomUUID()}`);
405
393
  await mkdir(staging, { recursive: true });
406
394
  let house;
@@ -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,21 +1,31 @@
1
+ import { KIT_RELEASE } from './clis.js';
1
2
  import { forgetRejected } from './home.js';
2
3
  import { installCommand } from './placement.js';
3
4
  import { killDescendants } from './scope.js';
4
- export const REPLACED = 'another Kit started for this Environment';
5
+ export const REPLACED = 'another Kit process started for this Environment';
5
6
  function exit(reason, status) {
6
7
  process.stderr.write(`kit: ${reason}\n`);
7
8
  killDescendants();
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);
15
16
  }
16
- export function updateRequired() {
17
+ function updateLine(lead) {
17
18
  const command = installCommand();
18
- end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
19
+ return command === null ? `${lead}; install it again.` : `${lead}. Update it: ${command}`;
20
+ }
21
+ export function updateRequired() {
22
+ end(updateLine('This Kit is too old for House'));
23
+ }
24
+ export function unknownMethod(kind) {
25
+ return updateLine(`This Kit does not know ${kind}`);
26
+ }
27
+ export function newerThanHouse(release, command) {
28
+ end(`Kit ${KIT_RELEASE} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
19
29
  }
20
30
  export async function signInAgain(rejected) {
21
31
  await forgetRejected(rejected);
package/dist/events.js CHANGED
@@ -1,8 +1,13 @@
1
+ const AUTONOMOUS_ORIGINS = new Set(['task-notification', 'peer', 'coordinator', 'observer', 'observer-activity']);
1
2
  const COMMAND = { shown: 'command' };
2
3
  const HIDDEN = { shown: 'hidden' };
3
4
  const FINISHED = new Set(['completed', 'failed']);
4
5
  export const RUNNING_JOB = new Set(['running', 'paused']);
5
- const ROUTINE_TARGETS = { send_routine_request: 'access', cancel_routine_request: 'request' };
6
+ const MESSAGE_TARGETS = new Map([
7
+ ['send_routine_request', ['routine', 'agent']],
8
+ ['cancel_routine_request', ['request']],
9
+ ['send_mail', ['conversation', 'request']],
10
+ ]);
6
11
  function words(line) {
7
12
  const found = [];
8
13
  let word = null;
@@ -48,11 +53,13 @@ export function houseMessage(line) {
48
53
  catch {
49
54
  return null;
50
55
  }
51
- const argument = ROUTINE_TARGETS[call.command];
52
- if (argument === undefined)
56
+ if (call === null)
53
57
  return null;
54
- const target = call.arguments?.[argument];
55
- return { shown: 'message', command: call.command, target: typeof target === 'string' ? target : null };
58
+ const targets = MESSAGE_TARGETS.get(call.command);
59
+ if (targets === undefined)
60
+ return null;
61
+ const target = targets.map((argument) => call.arguments?.[argument]).find((value) => typeof value === 'string');
62
+ return { shown: 'message', command: call.command, target: target ?? null };
56
63
  }
57
64
  function decidedByLine(command, final) {
58
65
  const line = typeof command === 'string' ? command : Array.isArray(command) ? command.at(-1) : undefined;
@@ -269,6 +276,7 @@ class Reader {
269
276
  class ClaudeReader extends Reader {
270
277
  watchedOptions = { model: 'model' };
271
278
  tasks = new Map();
279
+ stop = null;
272
280
  own(message) {
273
281
  if (message.params?.update?._meta?.claudeCode?.parentToolUseId !== undefined)
274
282
  return [];
@@ -276,7 +284,9 @@ class ClaudeReader extends Reader {
276
284
  }
277
285
  update(update) {
278
286
  if (update.sessionUpdate === 'usage_update' && update._meta?.['_claude/origin'] !== undefined) {
279
- return [...super.update(update), { kind: 'ended' }];
287
+ const stop = this.stop;
288
+ this.stop = null;
289
+ return [...super.update(update), { kind: 'ended', stop }];
280
290
  }
281
291
  if (update.sessionUpdate === 'session_info_update' && update._meta?.jetbrains?.air?.sessionFailure !== undefined) {
282
292
  return [{ kind: 'marker', marker: 'retry', text: update._meta.jetbrains.air.sessionFailure.title }];
@@ -287,6 +297,13 @@ class ClaudeReader extends Reader {
287
297
  if (message.method !== '_claude/sdkMessage')
288
298
  return [];
289
299
  const sdk = message.params.message;
300
+ if (sdk.type === 'result') {
301
+ if (AUTONOMOUS_ORIGINS.has(sdk.origin?.kind))
302
+ this.stop = null;
303
+ else
304
+ this.stop = sdk.stop_reason === 'refusal' ? 'refusal' : sdk.is_error === true ? null : 'end_turn';
305
+ return [];
306
+ }
290
307
  switch (sdk.subtype) {
291
308
  case 'task_started':
292
309
  if (sdk.tool_use_id !== undefined)
@@ -294,6 +311,10 @@ class ClaudeReader extends Reader {
294
311
  return [];
295
312
  case 'task_notification':
296
313
  return sdk.tool_use_id === undefined ? [] : this.finish(sdk.tool_use_id, sdk.summary ?? null);
314
+ case 'task_progress':
315
+ if (!this.subagents.has(sdk.tool_use_id) || this.done.has(sdk.tool_use_id) || typeof sdk.description !== 'string')
316
+ return [];
317
+ return [{ kind: 'subagent-activity', id: sdk.tool_use_id, activity: sdk.description }];
297
318
  default:
298
319
  return [];
299
320
  }
@@ -356,7 +377,7 @@ class CodexReader extends Reader {
356
377
  if (codex?.threadStatus?.type === 'active')
357
378
  return [{ kind: 'started' }];
358
379
  if (codex?.threadStatus?.type === 'idle')
359
- return [{ kind: 'ended' }];
380
+ return [{ kind: 'ended', stop: null }];
360
381
  if (codex?.error !== undefined)
361
382
  return [{ kind: 'marker', marker: 'retry', text: codex.error.message ?? '' }];
362
383
  return [];
@@ -429,7 +450,7 @@ class GrokReader extends Reader {
429
450
  return (message.params.runningPromptId ?? null) !== null && session === this.session ? [{ kind: 'started' }] : [];
430
451
  }
431
452
  if (message.params?.update?.sessionUpdate === 'turn_completed') {
432
- return session === this.session ? [{ kind: 'ended' }] : [];
453
+ return session === this.session ? [{ kind: 'ended', stop: null }] : [];
433
454
  }
434
455
  if (message.method === '_x.ai/models/update') {
435
456
  this.offered(message.params);
package/dist/files.js CHANGED
@@ -1,42 +1,144 @@
1
- import { createHash } from 'node:crypto';
2
- import { mkdir, writeFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import { HouseRefusal, retrying } from './api.js';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { createWriteStream } from 'node:fs';
3
+ import { mkdir, rename, rm } from 'node:fs/promises';
4
+ import { basename, dirname, join } from 'node:path';
5
+ import { Readable, Transform } from 'node:stream';
6
+ import { pipeline } from 'node:stream/promises';
7
+ import { parseDocument } from 'yaml';
8
+ import { retryDelay } from './api.js';
9
+ import { Unsettled } from './mcp.js';
5
10
  import { relayed } from './relay.js';
6
- export function verified(bytes, sha256, name) {
7
- if (createHash('sha256').update(bytes).digest('hex') !== sha256)
8
- throw new Error(`${name} did not arrive whole; download it again`);
9
- return bytes;
10
- }
11
- export async function placeFiles(house, directory, files, signal) {
12
- const paths = [];
13
- for (const file of files) {
14
- const original = await retrying(() => house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal), signal);
15
- const answer = await fetch(relayed(original.download.url), { signal });
16
- if (!answer.ok)
17
- throw new Error(`${file.name} did not download: ${answer.status}`);
18
- const folder = join(directory, '.house', 'files', original.message, file.version);
19
- await mkdir(join(directory, '.house')).catch((error) => {
20
- if (error.code !== 'EEXIST')
11
+ const STALLED_ROUNDS = 8;
12
+ const PART_CHECKSUM = 'x-amz-checksum-sha256';
13
+ const LINK = /\]\((\/rooms\/[^/)\s]+\/[^)\s]+)\)/gu;
14
+ export function linkedPaths(text) {
15
+ return [...new Set(Array.from(text.matchAll(LINK), (link) => link[1]))];
16
+ }
17
+ export function inspectedFile(answer) {
18
+ const document = parseDocument(answer);
19
+ if (document.errors.length > 0)
20
+ return null;
21
+ const inspected = document.toJS();
22
+ if (typeof inspected?.download !== 'string')
23
+ return null;
24
+ return {
25
+ download: inspected.download,
26
+ sha256: String(inspected.sha256),
27
+ media_type: typeof inspected.media_type === 'string' ? inspected.media_type : null,
28
+ };
29
+ }
30
+ function paused(milliseconds) {
31
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
32
+ }
33
+ export function storeRefusal(act, status) {
34
+ return new Error(`the ${act} answered ${status}; ${act} the file again`);
35
+ }
36
+ async function partDigests(uploading, partBytes) {
37
+ const whole = createHash('sha256');
38
+ const digests = [];
39
+ for (let start = 0; digests.length === 0 || start < uploading.bytes; start += partBytes) {
40
+ const end = Math.min(start + partBytes, uploading.bytes);
41
+ const digest = createHash('sha256');
42
+ for await (const chunk of uploading.read(start, end)) {
43
+ digest.update(chunk);
44
+ whole.update(chunk);
45
+ }
46
+ digests.push(digest.digest());
47
+ }
48
+ if (whole.digest('hex') !== uploading.sha256)
49
+ throw new Error(`${uploading.name} changed while it was uploaded; upload it again`);
50
+ return digests;
51
+ }
52
+ export async function uploaded(declared, uploading) {
53
+ const partBytes = declared.part_bytes;
54
+ const digests = await partDigests(uploading, partBytes);
55
+ const partSha256 = digests.map((digest) => digest.toString('hex'));
56
+ let answer = await finishing(uploading, declared.id, partSha256);
57
+ let idle = 0;
58
+ let stalled = 0;
59
+ while ('upload' in answer) {
60
+ const { id, parts } = answer.upload;
61
+ let sent = 0;
62
+ let failure = null;
63
+ for (const { part, url } of parts ?? []) {
64
+ const start = (part - 1) * partBytes;
65
+ try {
66
+ await uploading.send(url, start, Math.min(start + partBytes, uploading.bytes), {
67
+ [PART_CHECKSUM]: digests[part - 1].toString('base64'),
68
+ });
69
+ sent++;
70
+ }
71
+ catch (error) {
72
+ failure = error;
73
+ break;
74
+ }
75
+ }
76
+ if (sent > 0) {
77
+ idle = 0;
78
+ stalled = 0;
79
+ }
80
+ else {
81
+ if (failure !== null && ++stalled >= STALLED_ROUNDS)
82
+ throw failure;
83
+ // A round that sent no part, its store unreachable, is followed by the next finishing call on the jittered curve.
84
+ await paused(retryDelay(idle++));
85
+ }
86
+ answer = await finishing(uploading, id, partSha256);
87
+ }
88
+ return answer;
89
+ }
90
+ async function finishing(uploading, upload, partSha256) {
91
+ for (let unsettled = 1;; unsettled++) {
92
+ try {
93
+ return await uploading.finish(upload, partSha256);
94
+ }
95
+ catch (error) {
96
+ if (!(error instanceof Unsettled) || unsettled === STALLED_ROUNDS)
21
97
  throw error;
22
- });
23
- await mkdir(folder, { recursive: true });
24
- const path = join(folder, file.name);
25
- await writeFile(path, verified(Buffer.from(await answer.arrayBuffer()), file.sha256, file.name));
26
- paths.push(path);
98
+ }
99
+ // A finishing call House did not settle, unreached or refused short of final, is sent again on the jittered curve as a conversation resends it, so a dropped connection or a House restart resumes the upload instead of ending it.
100
+ await paused(retryDelay(unsettled - 1));
27
101
  }
28
- return paths;
29
- }
30
- export function uploadBytes(upload, bytes) {
31
- return retrying(async () => {
32
- const answer = await fetch(relayed(upload.url), {
33
- method: 'POST',
34
- headers: { 'content-type': 'application/octet-stream', 'x-house-byte-operation': upload.operation },
35
- body: bytes,
102
+ }
103
+ async function drained(body) {
104
+ for await (const _chunk of body)
105
+ ;
106
+ }
107
+ export async function downloaded(address, fetching, destination, name) {
108
+ for (let asked = 1;; asked++) {
109
+ const { download, sha256 } = await address();
110
+ const answer = await fetching(download);
111
+ if (answer.status === 403 && asked === 1) {
112
+ await drained(answer.body);
113
+ continue;
114
+ }
115
+ if (answer.status !== 200) {
116
+ await drained(answer.body);
117
+ throw storeRefusal('download', answer.status);
118
+ }
119
+ await mkdir(dirname(destination), { recursive: true });
120
+ const partial = join(dirname(destination), `.${basename(destination)}.${randomUUID()}`);
121
+ const digest = createHash('sha256');
122
+ const hashed = new Transform({
123
+ transform(chunk, _encoding, done) {
124
+ digest.update(chunk);
125
+ done(null, chunk);
126
+ },
36
127
  });
37
- const text = await answer.text();
38
- if (!answer.ok)
39
- throw new HouseRefusal(upload.url, answer.status, text);
40
- return JSON.parse(text);
41
- });
128
+ try {
129
+ await pipeline(answer.body, hashed, createWriteStream(partial, { flags: 'wx' }));
130
+ if (digest.digest('hex') !== sha256)
131
+ throw new Error(`${name} did not arrive whole; download it again`);
132
+ await rename(partial, destination);
133
+ }
134
+ catch (error) {
135
+ await rm(partial, { force: true });
136
+ throw error;
137
+ }
138
+ return;
139
+ }
140
+ }
141
+ export async function fetched(url, signal) {
142
+ const answer = await fetch(relayed(url), { signal });
143
+ return { status: answer.status, body: answer.body ?? Readable.from([]) };
42
144
  }
package/dist/git.js CHANGED
@@ -2,16 +2,16 @@ import { spawn } from 'node:child_process';
2
2
  import { isUtf8 } from 'node:buffer';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { join } from 'node:path';
5
- import { withoutProxy } from './clis.js';
6
5
  import { workspaceRoot } from './home.js';
6
+ import { childEnvironment } from './relay.js';
7
7
  export function localCopies() {
8
8
  return join(workspaceRoot(), 'local-copies');
9
9
  }
10
10
  export const HOUSE_REF = 'refs/house/received';
11
- const COMMITTER = 'House Kit <house-kit@localhost>';
11
+ const COMMITTER = 'Kit <house-kit@localhost>';
12
12
  export function run(cwd, args, input, env = {}) {
13
13
  return new Promise((resolve, reject) => {
14
- const child = spawn('git', args, { cwd, env: { ...withoutProxy(), ...env } });
14
+ const child = spawn('git', args, { cwd, env: { ...childEnvironment(), ...env } });
15
15
  const stdout = [];
16
16
  let stderr = '';
17
17
  child.stdout.on('data', (chunk) => stdout.push(chunk));
package/dist/home.js CHANGED
@@ -8,7 +8,6 @@ const ENROLMENT = 'credential.json';
8
8
  const DISCONNECTED = 'disconnected.json';
9
9
  const CONFIGURATION = 'kit.json';
10
10
  const OWN_AGENT = 'own-agent.json';
11
- const RESTART = 'restart';
12
11
  let defaulted;
13
12
  function writable(directory) {
14
13
  try {
@@ -132,12 +131,6 @@ export async function forgetRejected(rejected) {
132
131
  export function enrolled() {
133
132
  return existsSync(join(kitHome(), ENROLMENT));
134
133
  }
135
- export function restartAsked() {
136
- return existsSync(join(kitHome(), RESTART));
137
- }
138
- export function forgetRestart() {
139
- return rm(join(kitHome(), RESTART), { force: true });
140
- }
141
134
  export function readOwnAgent() {
142
135
  return readHome(OWN_AGENT);
143
136
  }