@agentshouse/kit 0.2.1-cca496635583e → 0.2.1-ccf188e02985d

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 (94) hide show
  1. package/README.md +23 -22
  2. package/dist/acp.js +34 -14
  3. package/dist/agents.js +108 -21
  4. package/dist/api.js +5 -1
  5. package/dist/app-git.js +10 -26
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +3 -6
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +51 -9
  10. package/dist/conversations.js +137 -70
  11. package/dist/copies.js +78 -34
  12. package/dist/end.js +5 -0
  13. package/dist/events.js +4 -0
  14. package/dist/files.js +8 -2
  15. package/dist/git.js +10 -8
  16. package/dist/home.js +56 -5
  17. package/dist/house-cli.js +291 -0
  18. package/dist/house-main.js +7 -189
  19. package/dist/inputs.js +37 -0
  20. package/dist/instructions.js +4 -28
  21. package/dist/kit-cli.js +68 -0
  22. package/dist/kit-main.js +8 -22
  23. package/dist/login.js +47 -13
  24. package/dist/logout.js +0 -2
  25. package/dist/parts.js +34 -16
  26. package/dist/placement.js +30 -0
  27. package/dist/relay.js +1 -1
  28. package/dist/resident.js +65 -16
  29. package/dist/service.js +22 -0
  30. package/dist/sign-in.js +5 -23
  31. package/dist/skills.js +81 -12
  32. package/dist/stream.js +66 -17
  33. package/dist/transcription.js +3 -3
  34. package/dist/update.js +48 -29
  35. package/dist/wire.js +6 -0
  36. package/dist/work.js +3 -5
  37. package/how-we-work.md +13 -3
  38. package/package.json +2 -1
  39. package/skills/ask-matt/SKILL.md +17 -11
  40. package/skills/ask-matt/agents/openai.yaml +1 -0
  41. package/skills/code-review/SKILL.md +5 -5
  42. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  43. package/skills/codebase-design/SKILL.md +0 -2
  44. package/skills/diagnosing-bugs/SKILL.md +2 -4
  45. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  46. package/skills/domain-modeling/SKILL.md +12 -14
  47. package/skills/grill-me/SKILL.md +1 -2
  48. package/skills/grill-me/agents/openai.yaml +1 -0
  49. package/skills/grill-with-docs/SKILL.md +1 -2
  50. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  51. package/skills/grilling/SKILL.md +2 -2
  52. package/skills/handoff/SKILL.md +2 -3
  53. package/skills/handoff/agents/openai.yaml +1 -0
  54. package/skills/implement/SKILL.md +5 -4
  55. package/skills/implement/agents/openai.yaml +1 -0
  56. package/skills/implement-spec/SKILL.md +40 -0
  57. package/skills/implement-spec/agents/openai.yaml +5 -0
  58. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  59. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  60. package/skills/pr/CREDITS.md +3 -0
  61. package/skills/pr/LICENSE +21 -0
  62. package/skills/pr/SKILL.md +170 -0
  63. package/skills/pr/agents/openai.yaml +3 -0
  64. package/skills/prototype/SKILL.md +0 -2
  65. package/skills/research/SKILL.md +0 -2
  66. package/skills/retro/LICENSE +21 -0
  67. package/skills/retro/SKILL.md +44 -0
  68. package/skills/retro/agents/openai.yaml +5 -0
  69. package/skills/tdd/SKILL.md +2 -4
  70. package/skills/teach/SKILL.md +3 -4
  71. package/skills/teach/agents/openai.yaml +1 -0
  72. package/skills/to-questionnaire/SKILL.md +1 -2
  73. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  74. package/skills/to-spec/SKILL.md +1 -2
  75. package/skills/to-spec/agents/openai.yaml +1 -0
  76. package/skills/to-tickets/SKILL.md +3 -4
  77. package/skills/to-tickets/agents/openai.yaml +1 -0
  78. package/skills/triage/AGENT-BRIEF.md +207 -0
  79. package/skills/triage/LICENSE +21 -0
  80. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  81. package/skills/triage/SKILL.md +112 -0
  82. package/skills/triage/agents/openai.yaml +5 -0
  83. package/skills/wait-what/SKILL.md +2 -3
  84. package/skills/wait-what/agents/openai.yaml +1 -0
  85. package/skills/wayfinder/SKILL.md +5 -6
  86. package/skills/wayfinder/agents/openai.yaml +1 -0
  87. package/skills/wizard/LICENSE +21 -0
  88. package/skills/wizard/SKILL.md +44 -0
  89. package/skills/wizard/agents/openai.yaml +3 -0
  90. package/skills/wizard/template.sh +223 -0
  91. package/skills/writing-for-agents/SKILL.md +0 -2
  92. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  93. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  94. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/copies.js CHANGED
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import { mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { finalRefusal, HouseRefusal, retrying } from './api.js';
6
- import { blobId, blobText, git, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
6
+ import { blobId, blobText, git, isText, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
7
7
  import { kitHome } from './home.js';
8
8
  import { operationId } from './operation.js';
9
9
  import { refusalOf } from './refusals.js';
@@ -15,6 +15,9 @@ const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-
15
15
  function causeOf(error) {
16
16
  return error instanceof Error ? error.message : String(error);
17
17
  }
18
+ function houseCause(error) {
19
+ return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
20
+ }
18
21
  function logged(error) {
19
22
  process.stderr.write(`kit: ${causeOf(error)}\n`);
20
23
  }
@@ -29,6 +32,13 @@ const REBASED = new Set([
29
32
  function rebaseFixes(refusal) {
30
33
  return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
31
34
  }
35
+ function textOnly(paths) {
36
+ const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
37
+ return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
38
+ }
39
+ function outside(path) {
40
+ return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
41
+ }
32
42
  function short(commit) {
33
43
  return commit.slice(0, 12);
34
44
  }
@@ -56,6 +66,9 @@ function sidecar(path) {
56
66
  segments.splice(-1, 0, PROVENANCE);
57
67
  return segments.join('/');
58
68
  }
69
+ function recordOf(key) {
70
+ return join(kitHome(), 'local-copies', `${key}.json`);
71
+ }
59
72
  function rootOf(handle) {
60
73
  return `/rooms/${handle}`;
61
74
  }
@@ -202,48 +215,62 @@ export class LocalCopies {
202
215
  async mapping() {
203
216
  await this.answering;
204
217
  return [...this.copies.values()]
205
- .filter((copy) => copy.active)
218
+ .filter((copy) => copy.erasing === undefined)
206
219
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
207
220
  .sort();
208
221
  }
209
- async connected() {
210
- const copied = await this.answered(() => this.fetched(false));
211
- if (copied.refused)
212
- logged(copied.text);
222
+ connected() {
223
+ return this.selected();
224
+ }
225
+ async changed(rooms) {
226
+ if (rooms.length > 0)
227
+ await this.selected(rooms);
213
228
  }
214
229
  fetch() {
215
230
  return this.answered(() => this.fetched(true));
216
231
  }
232
+ async selected(rooms) {
233
+ const copied = await this.answered(() => this.fetched(false, rooms));
234
+ if (copied.refused)
235
+ logged(copied.text);
236
+ }
217
237
  answered(work) {
218
238
  const answered = this.answering.then(work);
219
239
  this.answering = answered.catch(() => undefined);
220
240
  return answered;
221
241
  }
222
- async fetched(refreshing) {
223
- const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
242
+ async fetched(refreshing, rooms) {
243
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
224
244
  const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
225
- return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
245
+ const named = rooms === undefined ? null : new Set(rooms.map((room) => `rooms/${room}`));
246
+ const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
247
+ return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
226
248
  const said = [];
227
249
  let refused = false;
228
- for (const copy of this.copies.values()) {
229
- if (synced.has(copy.key) || !copy.active)
250
+ for (const copy of held()) {
251
+ const returned = synced.has(copy.key);
252
+ if (returned && copy.erasing === undefined)
230
253
  continue;
231
- await this.save({ ...copy, active: false });
232
- said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
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
+ }
233
262
  }
234
263
  const current = [];
235
264
  for (const [key, handle] of synced) {
236
265
  try {
237
266
  const held = this.copies.get(key);
267
+ if (held?.erasing)
268
+ continue;
238
269
  if (held === undefined || !(await exists(held.repository))) {
239
270
  said.push(await this.materialize(key, handle));
240
271
  continue;
241
272
  }
242
- const copy = held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle });
243
- if (copy.active)
244
- current.push(copy);
245
- else
246
- said.push(...moved(copy, await this.save(await this.received({ ...copy, active: true }))));
273
+ current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
247
274
  }
248
275
  catch (error) {
249
276
  refused = true;
@@ -257,7 +284,7 @@ export class LocalCopies {
257
284
  }
258
285
  catch (error) {
259
286
  refused = true;
260
- said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
287
+ said.push(houseCause(error));
261
288
  }
262
289
  }
263
290
  return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
@@ -269,6 +296,8 @@ export class LocalCopies {
269
296
  return;
270
297
  await this.exclusive(key, async () => {
271
298
  const copy = this.copies.get(key);
299
+ if (copy === undefined || copy.erasing)
300
+ return;
272
301
  let moves = copy.moves;
273
302
  for (const { from, to } of report.moves) {
274
303
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -291,11 +320,11 @@ export class LocalCopies {
291
320
  async push(cwd, selector, caller) {
292
321
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
293
322
  if (located.status !== 0)
294
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
323
+ return outside(cwd);
295
324
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
296
325
  const key = await this.keyAt(dirname(common));
297
326
  if (key === undefined)
298
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
327
+ return outside(worktree);
299
328
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
300
329
  }
301
330
  async keyAt(repository) {
@@ -316,7 +345,7 @@ export class LocalCopies {
316
345
  return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
317
346
  }
318
347
  async save(copy) {
319
- const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
348
+ const path = recordOf(copy.key);
320
349
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
321
350
  const temporary = `${path}.${process.pid}`;
322
351
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -324,6 +353,12 @@ export class LocalCopies {
324
353
  this.copies.set(copy.key, copy);
325
354
  return copy;
326
355
  }
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
+ }
327
362
  async read(handle) {
328
363
  const prefixes = [rootOf(handle)];
329
364
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
@@ -390,7 +425,6 @@ export class LocalCopies {
390
425
  room_ref: read.anchor,
391
426
  room_handle: handle,
392
427
  repository,
393
- active: true,
394
428
  house,
395
429
  position: read.position,
396
430
  log_epoch: read.log_epoch,
@@ -519,6 +553,8 @@ export class LocalCopies {
519
553
  }
520
554
  async pushed(key, worktree, selector, caller) {
521
555
  let copy = this.copies.get(key);
556
+ if (copy === undefined || copy.erasing)
557
+ return outside(worktree);
522
558
  if (copy.push?.state === 'submitted')
523
559
  return this.settle(copy, caller);
524
560
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -532,9 +568,6 @@ export class LocalCopies {
532
568
  }
533
569
  copy = await this.save(without(copy));
534
570
  }
535
- if (!copy.active) {
536
- return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
537
- }
538
571
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
539
572
  const base = listed.find((ancestor) => ancestor in copy.manifests);
540
573
  if (base === undefined) {
@@ -545,7 +578,7 @@ export class LocalCopies {
545
578
  changes = await this.derived(copy, base, commit);
546
579
  }
547
580
  catch (error) {
548
- return { refused: true, text: causeOf(error) };
581
+ return { refused: true, text: houseCause(error) };
549
582
  }
550
583
  if (changes.length === 0)
551
584
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -572,7 +605,18 @@ export class LocalCopies {
572
605
  const after = await tree(copy.repository, commit);
573
606
  const moved = movedPaths(copy.moves, before, after);
574
607
  const arrived = new Set(moved.values());
575
- const text = (entry, path) => blobText(copy.repository, entry, path);
608
+ const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
609
+ if (arriving.length > 0) {
610
+ const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
611
+ const binary = [];
612
+ for (const [path, entry] of arriving) {
613
+ if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
614
+ binary.push(path);
615
+ }
616
+ if (binary.length > 0)
617
+ throw new Error(textOnly(binary));
618
+ }
619
+ const text = (entry) => blobText(copy.repository, entry);
576
620
  const changes = [];
577
621
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
578
622
  if (arrived.has(path))
@@ -585,7 +629,7 @@ export class LocalCopies {
585
629
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
586
630
  const moving = after.get(to);
587
631
  if (moving !== prior) {
588
- changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving, to) });
632
+ changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
589
633
  }
590
634
  continue;
591
635
  }
@@ -594,9 +638,9 @@ export class LocalCopies {
594
638
  if (next === undefined)
595
639
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
596
640
  else if (prior === undefined)
597
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
641
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
598
642
  else
599
- changes.push(...changed(`${root}/${path}`, path, known, await text(prior, path), await text(next, path)));
643
+ changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
600
644
  }
601
645
  return changes;
602
646
  }
@@ -663,7 +707,7 @@ export class LocalCopies {
663
707
  written.push({
664
708
  path: `${root}/${path}`,
665
709
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
666
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
710
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
667
711
  });
668
712
  }
669
713
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -707,7 +751,7 @@ export class LocalCopies {
707
751
  {
708
752
  path: `${rootOf(copy.room_handle)}/${path}`,
709
753
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
710
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
754
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
711
755
  },
712
756
  ];
713
757
  }
@@ -752,7 +796,7 @@ export class LocalCopies {
752
796
  async landed(repository, changes, house, selected) {
753
797
  const text = async (path) => {
754
798
  const entry = house.get(relative(path));
755
- return entry === undefined ? undefined : blobText(repository, entry, path);
799
+ return entry === undefined ? undefined : blobText(repository, entry);
756
800
  };
757
801
  for (const change of changes) {
758
802
  const current = await text(change.path);
package/dist/end.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { forgetRejected } from './home.js';
2
+ import { installCommand } from './placement.js';
2
3
  import { killDescendants } from './scope.js';
3
4
  export const REPLACED = 'another Kit started for this Environment';
4
5
  function exit(reason, status) {
@@ -12,6 +13,10 @@ export function end(reason) {
12
13
  export function restart(reason) {
13
14
  exit(reason, 1);
14
15
  }
16
+ export function updateRequired() {
17
+ 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
+ }
15
20
  export async function signInAgain(rejected) {
16
21
  await forgetRejected(rejected);
17
22
  restart('sign in again');
package/dist/events.js CHANGED
@@ -512,6 +512,10 @@ class GrokReader extends Reader {
512
512
  return [];
513
513
  }
514
514
  }
515
+ export function answeredFailure(answer) {
516
+ const failure = answer._meta?.jetbrains?.air?.sessionFailure;
517
+ return typeof failure?.title === 'string' ? { title: failure.title, limit: failure.category === 'limit' } : null;
518
+ }
515
519
  export function readerFor(kind) {
516
520
  if (kind === 'claude-agent-acp')
517
521
  return new ClaudeReader();
package/dist/files.js CHANGED
@@ -1,11 +1,17 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { mkdir, writeFile } from 'node:fs/promises';
2
3
  import { join } from 'node:path';
3
4
  import { HouseRefusal, retrying } from './api.js';
4
5
  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
+ }
5
11
  export async function placeFiles(house, directory, files, signal) {
6
12
  const paths = [];
7
13
  for (const file of files) {
8
- const original = await house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal);
14
+ const original = await retrying(() => house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal), signal);
9
15
  const answer = await fetch(relayed(original.download.url), { signal });
10
16
  if (!answer.ok)
11
17
  throw new Error(`${file.name} did not download: ${answer.status}`);
@@ -16,7 +22,7 @@ export async function placeFiles(house, directory, files, signal) {
16
22
  });
17
23
  await mkdir(folder, { recursive: true });
18
24
  const path = join(folder, file.name);
19
- await writeFile(path, Buffer.from(await answer.arrayBuffer()));
25
+ await writeFile(path, verified(Buffer.from(await answer.arrayBuffer()), file.sha256, file.name));
20
26
  paths.push(path);
21
27
  }
22
28
  return paths;
package/dist/git.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { isUtf8 } from 'node:buffer';
2
3
  import { createHash } from 'node:crypto';
3
4
  import { join } from 'node:path';
4
5
  import { withoutProxy } from './clis.js';
@@ -48,14 +49,15 @@ export async function tree(repository, commit) {
48
49
  }
49
50
  return entries;
50
51
  }
51
- export async function blobText(repository, object, path) {
52
- const bytes = (await run(repository, ['cat-file', 'blob', object])).stdout;
53
- try {
54
- return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
55
- }
56
- catch {
57
- throw new Error(`${path} is binary and House stores only text; remove it from the commit`);
58
- }
52
+ async function blob(repository, object) {
53
+ return (await run(repository, ['cat-file', 'blob', object])).stdout;
54
+ }
55
+ export async function blobText(repository, object) {
56
+ return (await blob(repository, object)).toString('utf8');
57
+ }
58
+ export async function isText(repository, object, maxBytes) {
59
+ const bytes = await blob(repository, object);
60
+ return bytes.length <= maxBytes && isUtf8(bytes) && !bytes.includes(0);
59
61
  }
60
62
  export function blobId(content) {
61
63
  const bytes = Buffer.from(content, 'utf8');
package/dist/home.js CHANGED
@@ -1,15 +1,63 @@
1
- import { existsSync, linkSync, readFileSync, renameSync, rmSync } from 'node:fs';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { accessSync, chmodSync, constants, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync } from 'node:fs';
2
3
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
- import { homedir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { homedir, tmpdir } from 'node:os';
5
+ import { join, resolve } from 'node:path';
5
6
  import { selectRelay } from './relay.js';
6
7
  const ENROLMENT = 'credential.json';
7
8
  const DISCONNECTED = 'disconnected.json';
8
9
  const CONFIGURATION = 'kit.json';
9
10
  const OWN_AGENT = 'own-agent.json';
10
11
  const RESTART = 'restart';
12
+ let defaulted;
13
+ function writable(directory) {
14
+ try {
15
+ accessSync(directory, constants.W_OK | constants.X_OK);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ function privateDirectory(path) {
23
+ try {
24
+ mkdirSync(path, { mode: 0o700 });
25
+ }
26
+ catch (error) {
27
+ if (error.code !== 'EEXIST')
28
+ throw error;
29
+ }
30
+ const found = lstatSync(path);
31
+ if (found.isSymbolicLink())
32
+ throw new Error(`${path} is a symbolic link`);
33
+ if (!found.isDirectory())
34
+ throw new Error(`${path} is not a directory`);
35
+ if (found.uid !== process.geteuid())
36
+ throw new Error(`${path} is not owned by this User`);
37
+ chmodSync(path, 0o700);
38
+ return path;
39
+ }
40
+ function ownTemporaryHome() {
41
+ return privateDirectory(join(tmpdir(), `house-kit-${process.getuid()}`));
42
+ }
43
+ function defaultHome() {
44
+ const home = join(homedir(), '.house-kit');
45
+ return existsSync(home) || writable(homedir()) ? home : ownTemporaryHome();
46
+ }
11
47
  export function kitHome() {
12
- return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
48
+ return process.env.HOUSE_KIT_HOME || (defaulted ??= defaultHome());
49
+ }
50
+ export function socketDirectory() {
51
+ const sockets = privateDirectory(join(realpathSync('/tmp'), `hk-${process.geteuid()}`));
52
+ const home = kitHome();
53
+ const physical = existsSync(home) ? realpathSync(home) : resolve(home);
54
+ return privateDirectory(join(sockets, createHash('sha256').update(physical).digest('hex').slice(0, 12)));
55
+ }
56
+ export function ownerSocket() {
57
+ return join(socketDirectory(), 'owner.sock');
58
+ }
59
+ export function ownerLock() {
60
+ return join(socketDirectory(), 'owner.lock');
13
61
  }
14
62
  export function workspaceRoot() {
15
63
  return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
@@ -30,7 +78,7 @@ export async function readHome(name) {
30
78
  export async function writeHome(name, value) {
31
79
  const home = kitHome();
32
80
  await mkdir(home, { recursive: true, mode: 0o700 });
33
- const temporary = join(home, `${name}.${process.pid}`);
81
+ const temporary = join(home, `${name}.${randomUUID()}`);
34
82
  await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
35
83
  await rename(temporary, join(home, name));
36
84
  }
@@ -47,6 +95,9 @@ export async function writeEnrolment(enrolment) {
47
95
  export function readDisconnected() {
48
96
  return readHome(DISCONNECTED);
49
97
  }
98
+ export function forgetDisconnected() {
99
+ return rm(join(kitHome(), DISCONNECTED), { force: true });
100
+ }
50
101
  export async function forgetEnrolment({ house, environment }) {
51
102
  await writeHome(DISCONNECTED, { house, environment });
52
103
  await rm(join(kitHome(), ENROLMENT), { force: true });