@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/README.md +14 -14
- package/dist/acp.js +34 -14
- package/dist/agents.js +50 -17
- package/dist/api.js +1 -1
- package/dist/app-git.js +10 -26
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +3 -6
- package/dist/chatgpt.js +4 -1
- package/dist/clis.js +9 -0
- package/dist/conversations.js +123 -72
- package/dist/copies.js +86 -45
- package/dist/end.js +3 -2
- package/dist/events.js +4 -0
- package/dist/files.js +1 -1
- package/dist/git.js +2 -3
- package/dist/home.js +27 -13
- package/dist/house-cli.js +291 -0
- package/dist/house-main.js +7 -289
- package/dist/inputs.js +37 -0
- package/dist/instructions.js +2 -1
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -67
- package/dist/login.js +14 -5
- package/dist/logout.js +0 -2
- package/dist/placement.js +2 -20
- package/dist/resident.js +60 -15
- package/dist/service.js +22 -0
- package/dist/sign-in.js +5 -23
- package/dist/stream.js +5 -5
- package/dist/update.js +20 -23
- package/dist/wire.js +1 -1
- package/dist/work.js +3 -5
- package/package.json +1 -1
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.
|
|
221
|
+
.filter((copy) => copy.erasing === undefined)
|
|
210
222
|
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
211
223
|
.sort();
|
|
212
224
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
|
233
|
-
|
|
253
|
+
for (const copy of held()) {
|
|
254
|
+
const returned = synced.has(copy.key);
|
|
255
|
+
if (returned && copy.erasing === undefined)
|
|
234
256
|
continue;
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
433
|
-
|
|
434
|
-
});
|
|
435
|
-
const
|
|
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 =
|
|
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
|
-
|
|
474
|
+
await this.save(await this.received(copy));
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
unchanged.push(copy);
|
|
441
478
|
}
|
|
442
479
|
}
|
|
443
|
-
const
|
|
444
|
-
|
|
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:
|
|
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
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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(
|
|
17
|
-
|
|
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 <=
|
|
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
|
|
23
|
-
const home = join(tmpdir(), `house-kit-${process.getuid()}`);
|
|
22
|
+
function privateDirectory(path) {
|
|
24
23
|
try {
|
|
25
|
-
mkdirSync(
|
|
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(
|
|
30
|
+
const found = lstatSync(path);
|
|
32
31
|
if (found.isSymbolicLink())
|
|
33
|
-
throw new Error(`${
|
|
32
|
+
throw new Error(`${path} is a symbolic link`);
|
|
34
33
|
if (!found.isDirectory())
|
|
35
|
-
throw new Error(`${
|
|
36
|
-
if (found.uid !== process.
|
|
37
|
-
throw new Error(`${
|
|
38
|
-
chmodSync(
|
|
39
|
-
return
|
|
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
|
}
|