@agentshouse/kit 0.2.1-c0b0ffa6c025b → 0.2.1-c0eea939ba749

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/dist/copies.js CHANGED
@@ -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
  }
@@ -33,6 +36,9 @@ function textOnly(paths) {
33
36
  const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
34
37
  return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
35
38
  }
39
+ function outside(path) {
40
+ return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
41
+ }
36
42
  function short(commit) {
37
43
  return commit.slice(0, 12);
38
44
  }
@@ -52,6 +58,9 @@ function expiredAuthority(error) {
52
58
  return undefined;
53
59
  }
54
60
  }
61
+ function grouped(items, size) {
62
+ return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
63
+ }
55
64
  function covers(path, prefix) {
56
65
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
57
66
  }
@@ -60,6 +69,9 @@ function sidecar(path) {
60
69
  segments.splice(-1, 0, PROVENANCE);
61
70
  return segments.join('/');
62
71
  }
72
+ function recordOf(key) {
73
+ return join(kitHome(), 'local-copies', `${key}.json`);
74
+ }
63
75
  function rootOf(handle) {
64
76
  return `/rooms/${handle}`;
65
77
  }
@@ -206,48 +218,62 @@ export class LocalCopies {
206
218
  async mapping() {
207
219
  await this.answering;
208
220
  return [...this.copies.values()]
209
- .filter((copy) => copy.active)
221
+ .filter((copy) => copy.erasing === undefined)
210
222
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
211
223
  .sort();
212
224
  }
213
- async connected() {
214
- const copied = await this.answered(() => this.fetched(false));
215
- if (copied.refused)
216
- logged(copied.text);
225
+ connected() {
226
+ return this.selected();
227
+ }
228
+ async changed(rooms) {
229
+ if (rooms.length > 0)
230
+ await this.selected(rooms);
217
231
  }
218
232
  fetch() {
219
233
  return this.answered(() => this.fetched(true));
220
234
  }
235
+ async selected(rooms) {
236
+ const copied = await this.answered(() => this.fetched(false, rooms));
237
+ if (copied.refused)
238
+ logged(copied.text);
239
+ }
221
240
  answered(work) {
222
241
  const answered = this.answering.then(work);
223
242
  this.answering = answered.catch(() => undefined);
224
243
  return answered;
225
244
  }
226
- async fetched(refreshing) {
227
- const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
245
+ async fetched(refreshing, rooms) {
246
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
228
247
  const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
229
- return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
248
+ const named = rooms === undefined ? null : new Set(rooms.map((room) => `rooms/${room}`));
249
+ const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
250
+ return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
230
251
  const said = [];
231
252
  let refused = false;
232
- for (const copy of this.copies.values()) {
233
- if (synced.has(copy.key) || !copy.active)
253
+ for (const copy of held()) {
254
+ const returned = synced.has(copy.key);
255
+ if (returned && copy.erasing === undefined)
234
256
  continue;
235
- await this.save({ ...copy, active: false });
236
- said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
257
+ try {
258
+ await this.erase(copy);
259
+ said.push(`${rootOf(copy.room_handle)}: ${returned ? '' : 'no longer synced to this Environment; '}erased from the Local copy`);
260
+ }
261
+ catch (error) {
262
+ refused = true;
263
+ said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
264
+ }
237
265
  }
238
266
  const current = [];
239
267
  for (const [key, handle] of synced) {
240
268
  try {
241
269
  const held = this.copies.get(key);
270
+ if (held?.erasing)
271
+ continue;
242
272
  if (held === undefined || !(await exists(held.repository))) {
243
273
  said.push(await this.materialize(key, handle));
244
274
  continue;
245
275
  }
246
- const copy = held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle });
247
- if (copy.active)
248
- current.push(copy);
249
- else
250
- said.push(...moved(copy, await this.save(await this.received({ ...copy, active: true }))));
276
+ current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
251
277
  }
252
278
  catch (error) {
253
279
  refused = true;
@@ -255,13 +281,12 @@ export class LocalCopies {
255
281
  }
256
282
  }
257
283
  if (refreshing && current.length > 0) {
258
- try {
259
- const refreshed = await this.refreshed(current);
260
- current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
261
- }
262
- catch (error) {
284
+ const failed = await this.refreshed(current).then(() => null, houseCause);
285
+ for (const copy of current)
286
+ said.push(...moved(copy, this.copies.get(copy.key)));
287
+ if (failed !== null) {
263
288
  refused = true;
264
- said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
289
+ said.push(failed);
265
290
  }
266
291
  }
267
292
  return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
@@ -273,6 +298,8 @@ export class LocalCopies {
273
298
  return;
274
299
  await this.exclusive(key, async () => {
275
300
  const copy = this.copies.get(key);
301
+ if (copy === undefined || copy.erasing)
302
+ return;
276
303
  let moves = copy.moves;
277
304
  for (const { from, to } of report.moves) {
278
305
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -295,11 +322,11 @@ export class LocalCopies {
295
322
  async push(cwd, selector, caller) {
296
323
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
297
324
  if (located.status !== 0)
298
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
325
+ return outside(cwd);
299
326
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
300
327
  const key = await this.keyAt(dirname(common));
301
328
  if (key === undefined)
302
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
329
+ return outside(worktree);
303
330
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
304
331
  }
305
332
  async keyAt(repository) {
@@ -320,7 +347,7 @@ export class LocalCopies {
320
347
  return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
321
348
  }
322
349
  async save(copy) {
323
- const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
350
+ const path = recordOf(copy.key);
324
351
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
325
352
  const temporary = `${path}.${process.pid}`;
326
353
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -328,6 +355,12 @@ export class LocalCopies {
328
355
  this.copies.set(copy.key, copy);
329
356
  return copy;
330
357
  }
358
+ async erase(copy) {
359
+ await this.save({ ...copy, erasing: true });
360
+ await rm(copy.repository, { recursive: true, force: true });
361
+ await rm(recordOf(copy.key), { force: true });
362
+ this.copies.delete(copy.key);
363
+ }
331
364
  async read(handle) {
332
365
  const prefixes = [rootOf(handle)];
333
366
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
@@ -394,7 +427,6 @@ export class LocalCopies {
394
427
  room_ref: read.anchor,
395
428
  room_handle: handle,
396
429
  repository,
397
- active: true,
398
430
  house,
399
431
  position: read.position,
400
432
  log_epoch: read.log_epoch,
@@ -429,19 +461,24 @@ export class LocalCopies {
429
461
  return next;
430
462
  }
431
463
  async refreshed(copies) {
432
- const discovery = await this.house.post('/kit/door/discover', {
433
- prefixes: copies.map((copy) => rootOf(copy.room_handle)),
434
- });
435
- const regranted = new Map();
464
+ const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map((copy) => rootOf(copy.room_handle)) });
465
+ const [first, ...rest] = copies;
466
+ const { bounds, authorities } = await discover([first]);
467
+ for (const group of grouped(rest, bounds.max_prefixes))
468
+ authorities.push(...(await discover(group)).authorities);
469
+ const unchanged = [];
436
470
  for (const copy of copies) {
437
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
471
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
438
472
  if (authority !== undefined &&
439
473
  JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
440
- regranted.set(copy.key, await this.save(await this.received(copy)));
474
+ await this.save(await this.received(copy));
475
+ }
476
+ else {
477
+ unchanged.push(copy);
441
478
  }
442
479
  }
443
- const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
444
- return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
480
+ for (const group of grouped(unchanged, bounds.max_prefixes))
481
+ await this.caughtUp(group, false);
445
482
  }
446
483
  async caughtUp(copies, whole) {
447
484
  const caught = new Map(copies.map((copy) => [copy.key, copy]));
@@ -523,6 +560,8 @@ export class LocalCopies {
523
560
  }
524
561
  async pushed(key, worktree, selector, caller) {
525
562
  let copy = this.copies.get(key);
563
+ if (copy === undefined || copy.erasing)
564
+ return outside(worktree);
526
565
  if (copy.push?.state === 'submitted')
527
566
  return this.settle(copy, caller);
528
567
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -536,9 +575,6 @@ export class LocalCopies {
536
575
  }
537
576
  copy = await this.save(without(copy));
538
577
  }
539
- if (!copy.active) {
540
- return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
541
- }
542
578
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
543
579
  const base = listed.find((ancestor) => ancestor in copy.manifests);
544
580
  if (base === undefined) {
@@ -549,7 +585,7 @@ export class LocalCopies {
549
585
  changes = await this.derived(copy, base, commit);
550
586
  }
551
587
  catch (error) {
552
- return { refused: true, text: causeOf(error) };
588
+ return { refused: true, text: houseCause(error) };
553
589
  }
554
590
  if (changes.length === 0)
555
591
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -576,12 +612,17 @@ export class LocalCopies {
576
612
  const after = await tree(copy.repository, commit);
577
613
  const moved = movedPaths(copy.moves, before, after);
578
614
  const arrived = new Set(moved.values());
579
- const binary = [];
580
- for (const [path, entry] of after)
581
- if (before.get(path) !== entry && !(await isText(copy.repository, entry)))
582
- binary.push(path);
583
- if (binary.length > 0)
584
- throw new Error(textOnly(binary));
615
+ const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
616
+ if (arriving.length > 0) {
617
+ const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
618
+ const binary = [];
619
+ for (const [path, entry] of arriving) {
620
+ if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
621
+ binary.push(path);
622
+ }
623
+ if (binary.length > 0)
624
+ throw new Error(textOnly(binary));
625
+ }
585
626
  const text = (entry) => blobText(copy.repository, entry);
586
627
  const changes = [];
587
628
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
package/dist/end.js CHANGED
@@ -13,8 +13,9 @@ export function end(reason) {
13
13
  export function restart(reason) {
14
14
  exit(reason, 1);
15
15
  }
16
- export function updateRequired(house) {
17
- end(`This Kit is too old for House. Update it: ${installCommand(house)}`);
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}`);
18
19
  }
19
20
  export async function signInAgain(rejected) {
20
21
  await forgetRejected(rejected);
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
@@ -11,7 +11,7 @@ export function verified(bytes, sha256, name) {
11
11
  export async function placeFiles(house, directory, files, signal) {
12
12
  const paths = [];
13
13
  for (const file of files) {
14
- 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);
15
15
  const answer = await fetch(relayed(original.download.url), { signal });
16
16
  if (!answer.ok)
17
17
  throw new Error(`${file.name} did not download: ${answer.status}`);
package/dist/git.js CHANGED
@@ -49,16 +49,15 @@ export async function tree(repository, commit) {
49
49
  }
50
50
  return entries;
51
51
  }
52
- const TEXT_MAX_BYTES = 16 * 1024 * 1024;
53
52
  async function blob(repository, object) {
54
53
  return (await run(repository, ['cat-file', 'blob', object])).stdout;
55
54
  }
56
55
  export async function blobText(repository, object) {
57
56
  return (await blob(repository, object)).toString('utf8');
58
57
  }
59
- export async function isText(repository, object) {
58
+ export async function isText(repository, object, maxBytes) {
60
59
  const bytes = await blob(repository, object);
61
- return bytes.length <= TEXT_MAX_BYTES && isUtf8(bytes) && !bytes.includes(0);
60
+ return bytes.length <= maxBytes && isUtf8(bytes) && !bytes.includes(0);
62
61
  }
63
62
  export function blobId(content) {
64
63
  const bytes = Buffer.from(content, 'utf8');
package/dist/home.js CHANGED
@@ -1,8 +1,8 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { accessSync, chmodSync, constants, existsSync, linkSync, lstatSync, mkdirSync, 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';
3
3
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
4
  import { homedir, tmpdir } from 'node:os';
5
- import { join } from 'node:path';
5
+ import { join, resolve } from 'node:path';
6
6
  import { selectRelay } from './relay.js';
7
7
  const ENROLMENT = 'credential.json';
8
8
  const DISCONNECTED = 'disconnected.json';
@@ -19,24 +19,26 @@ function writable(directory) {
19
19
  return false;
20
20
  }
21
21
  }
22
- function ownTemporaryHome() {
23
- const home = join(tmpdir(), `house-kit-${process.getuid()}`);
22
+ function privateDirectory(path) {
24
23
  try {
25
- mkdirSync(home, { mode: 0o700 });
24
+ mkdirSync(path, { mode: 0o700 });
26
25
  }
27
26
  catch (error) {
28
27
  if (error.code !== 'EEXIST')
29
28
  throw error;
30
29
  }
31
- const found = lstatSync(home);
30
+ const found = lstatSync(path);
32
31
  if (found.isSymbolicLink())
33
- throw new Error(`${home} is a symbolic link`);
32
+ throw new Error(`${path} is a symbolic link`);
34
33
  if (!found.isDirectory())
35
- throw new Error(`${home} is not a directory`);
36
- if (found.uid !== process.getuid())
37
- throw new Error(`${home} is not owned by this User`);
38
- chmodSync(home, 0o700);
39
- return home;
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()}`));
40
42
  }
41
43
  function defaultHome() {
42
44
  const home = join(homedir(), '.house-kit');
@@ -45,6 +47,18 @@ function defaultHome() {
45
47
  export function kitHome() {
46
48
  return process.env.HOUSE_KIT_HOME || (defaulted ??= defaultHome());
47
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');
61
+ }
48
62
  export function workspaceRoot() {
49
63
  return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
50
64
  }