@agentshouse/kit 0.2.1-cde47fd72fe59 → 0.2.1-ce3d2bb67177c
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 +18 -18
- package/dist/acp.js +2 -2
- package/dist/agents.js +2 -2
- package/dist/api.js +5 -5
- package/dist/bootstrap.json +4 -4
- package/dist/calls.js +63 -0
- package/dist/clis.js +1 -1
- package/dist/{stream.js → connection.js} +29 -27
- package/dist/conversations.js +289 -101
- package/dist/copies.js +15 -29
- package/dist/end.js +6 -2
- package/dist/events.js +2 -2
- package/dist/files.js +123 -37
- package/dist/git.js +1 -1
- package/dist/house-cli.js +95 -59
- package/dist/instructions.js +2 -2
- package/dist/kit-cli.js +2 -2
- package/dist/login.js +1 -3
- package/dist/mcp.js +4 -2
- package/dist/placement.js +11 -2
- package/dist/resident.js +32 -32
- package/dist/scope.js +3 -0
- package/dist/service.js +1 -1
- package/dist/sign-in.js +19 -19
- package/dist/skills.js +3 -3
- package/dist/transcription.js +51 -20
- package/dist/update.js +32 -32
- package/dist/wire.js +19 -3
- package/package.json +2 -2
- package/dist/inputs.js +0 -37
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: '
|
|
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
|
}
|
|
@@ -133,7 +133,7 @@ async function ignoreRules(repository, writable, copy) {
|
|
|
133
133
|
].map(pattern);
|
|
134
134
|
const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
|
|
135
135
|
await ruled(join(repository, '.gitignore'), [
|
|
136
|
-
'#
|
|
136
|
+
'# Kit writes this file and .ignore. The paths below are read-only House',
|
|
137
137
|
'# content: Git does not track them, and House refuses a change to them even',
|
|
138
138
|
'# when this file is edited or a path is force-added.',
|
|
139
139
|
'/.gitignore',
|
|
@@ -148,7 +148,7 @@ async function ignoreRules(repository, writable, copy) {
|
|
|
148
148
|
'',
|
|
149
149
|
].join('\n'));
|
|
150
150
|
await ruled(join(repository, '.ignore'), [
|
|
151
|
-
'#
|
|
151
|
+
'# Kit writes this file so native search still reads the read-only House',
|
|
152
152
|
'# content that .gitignore keeps out of Git.',
|
|
153
153
|
`!${PROVENANCE}`,
|
|
154
154
|
...[...house, ...granted].map((rule) => `!${rule}`),
|
|
@@ -218,7 +218,7 @@ export class LocalCopies {
|
|
|
218
218
|
async mapping() {
|
|
219
219
|
await this.answering;
|
|
220
220
|
return [...this.copies.values()]
|
|
221
|
-
.filter((copy) => copy.
|
|
221
|
+
.filter((copy) => copy.unfollowed === undefined)
|
|
222
222
|
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
223
223
|
.sort();
|
|
224
224
|
}
|
|
@@ -250,30 +250,24 @@ export class LocalCopies {
|
|
|
250
250
|
return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
|
|
251
251
|
const said = [];
|
|
252
252
|
let refused = false;
|
|
253
|
+
const mark = refreshing ? 'said' : 'unsaid';
|
|
253
254
|
for (const copy of held()) {
|
|
254
|
-
|
|
255
|
-
if (returned && copy.erasing === undefined)
|
|
255
|
+
if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
|
|
256
256
|
continue;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
said.push(`${rootOf(copy.room_handle)}:
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
refused = true;
|
|
263
|
-
said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
|
|
264
|
-
}
|
|
257
|
+
await this.save({ ...copy, unfollowed: mark });
|
|
258
|
+
if (refreshing)
|
|
259
|
+
said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
|
|
265
260
|
}
|
|
266
261
|
const current = [];
|
|
267
262
|
for (const [key, handle] of synced) {
|
|
268
263
|
try {
|
|
269
264
|
const held = this.copies.get(key);
|
|
270
|
-
if (held?.erasing)
|
|
271
|
-
continue;
|
|
272
265
|
if (held === undefined || !(await exists(held.repository))) {
|
|
273
266
|
said.push(await this.materialize(key, handle));
|
|
274
267
|
continue;
|
|
275
268
|
}
|
|
276
|
-
|
|
269
|
+
const { unfollowed, ...followed } = held;
|
|
270
|
+
current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
|
|
277
271
|
}
|
|
278
272
|
catch (error) {
|
|
279
273
|
refused = true;
|
|
@@ -298,8 +292,6 @@ export class LocalCopies {
|
|
|
298
292
|
return;
|
|
299
293
|
await this.exclusive(key, async () => {
|
|
300
294
|
const copy = this.copies.get(key);
|
|
301
|
-
if (copy === undefined || copy.erasing)
|
|
302
|
-
return;
|
|
303
295
|
let moves = copy.moves;
|
|
304
296
|
for (const { from, to } of report.moves) {
|
|
305
297
|
const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
|
|
@@ -355,12 +347,6 @@ export class LocalCopies {
|
|
|
355
347
|
this.copies.set(copy.key, copy);
|
|
356
348
|
return copy;
|
|
357
349
|
}
|
|
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
|
-
}
|
|
364
350
|
async read(handle) {
|
|
365
351
|
const prefixes = [rootOf(handle)];
|
|
366
352
|
const discovery = await this.house.post('/kit/door/discover', { prefixes });
|
|
@@ -402,7 +388,7 @@ export class LocalCopies {
|
|
|
402
388
|
throw new Error('House serves nothing of it to copy');
|
|
403
389
|
const repository = join(localCopies(), 'rooms', read.anchor);
|
|
404
390
|
if (await exists(repository))
|
|
405
|
-
throw new Error(`${repository} already exists and
|
|
391
|
+
throw new Error(`${repository} already exists and Kit leaves it as it is`);
|
|
406
392
|
const staging = join(dirname(repository), `.${randomUUID()}`);
|
|
407
393
|
await mkdir(staging, { recursive: true });
|
|
408
394
|
let house;
|
|
@@ -560,8 +546,6 @@ export class LocalCopies {
|
|
|
560
546
|
}
|
|
561
547
|
async pushed(key, worktree, selector, caller) {
|
|
562
548
|
let copy = this.copies.get(key);
|
|
563
|
-
if (copy === undefined || copy.erasing)
|
|
564
|
-
return outside(worktree);
|
|
565
549
|
if (copy.push?.state === 'submitted')
|
|
566
550
|
return this.settle(copy, caller);
|
|
567
551
|
const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
|
|
@@ -676,8 +660,10 @@ export class LocalCopies {
|
|
|
676
660
|
}
|
|
677
661
|
if (outcome.refused.code === 'expired')
|
|
678
662
|
return this.recovered(copy);
|
|
679
|
-
const rebased = rebaseFixes(outcome.refused);
|
|
680
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);
|
|
681
667
|
try {
|
|
682
668
|
settled = (await this.caughtUp([settled], true))[0];
|
|
683
669
|
}
|
package/dist/end.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
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);
|
|
@@ -17,6 +18,9 @@ export function updateRequired() {
|
|
|
17
18
|
const command = installCommand();
|
|
18
19
|
end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
|
|
19
20
|
}
|
|
21
|
+
export function newerThanHouse(release, command) {
|
|
22
|
+
end(`Kit ${KIT_RELEASE} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
|
|
23
|
+
}
|
|
20
24
|
export async function signInAgain(rejected) {
|
|
21
25
|
await forgetRejected(rejected);
|
|
22
26
|
restart('sign in again');
|
package/dist/events.js
CHANGED
|
@@ -2,7 +2,7 @@ const COMMAND = { shown: 'command' };
|
|
|
2
2
|
const HIDDEN = { shown: 'hidden' };
|
|
3
3
|
const FINISHED = new Set(['completed', 'failed']);
|
|
4
4
|
export const RUNNING_JOB = new Set(['running', 'paused']);
|
|
5
|
-
const
|
|
5
|
+
const MESSAGE_TARGETS = { send_routine_request: 'access', cancel_routine_request: 'request', send_mail: 'request' };
|
|
6
6
|
function words(line) {
|
|
7
7
|
const found = [];
|
|
8
8
|
let word = null;
|
|
@@ -48,7 +48,7 @@ export function houseMessage(line) {
|
|
|
48
48
|
catch {
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
|
-
const argument =
|
|
51
|
+
const argument = MESSAGE_TARGETS[call.command];
|
|
52
52
|
if (argument === undefined)
|
|
53
53
|
return null;
|
|
54
54
|
const target = call.arguments?.[argument];
|
package/dist/files.js
CHANGED
|
@@ -1,42 +1,128 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return
|
|
10
|
-
}
|
|
11
|
-
export
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
const STALLED_ROUNDS = 8;
|
|
12
|
+
const LINK = /\]\((\/rooms\/[^/)\s]+\/[^)\s]+)\)/gu;
|
|
13
|
+
export function linkedPaths(text) {
|
|
14
|
+
return [...new Set(Array.from(text.matchAll(LINK), (link) => link[1]))];
|
|
15
|
+
}
|
|
16
|
+
export function inspectedFile(answer) {
|
|
17
|
+
const document = parseDocument(answer);
|
|
18
|
+
if (document.errors.length > 0)
|
|
19
|
+
return null;
|
|
20
|
+
const inspected = document.toJS();
|
|
21
|
+
if (typeof inspected?.download !== 'string')
|
|
22
|
+
return null;
|
|
23
|
+
return {
|
|
24
|
+
download: inspected.download,
|
|
25
|
+
sha256: String(inspected.sha256),
|
|
26
|
+
media_type: typeof inspected.media_type === 'string' ? inspected.media_type : null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function paused(milliseconds) {
|
|
30
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
31
|
+
}
|
|
32
|
+
export function storeRefusal(act, status) {
|
|
33
|
+
return new Error(`the ${act} answered ${status}; ${act} the file again`);
|
|
34
|
+
}
|
|
35
|
+
export async function uploaded(declared, uploading) {
|
|
36
|
+
let upload = declared;
|
|
37
|
+
let idle = 0;
|
|
38
|
+
let stalled = 0;
|
|
39
|
+
for (;;) {
|
|
40
|
+
let sent = 0;
|
|
41
|
+
let failure = null;
|
|
42
|
+
for (const { part, url } of upload.parts) {
|
|
43
|
+
const start = (part - 1) * upload.part_bytes;
|
|
44
|
+
try {
|
|
45
|
+
await uploading.send(url, start, Math.min(start + upload.part_bytes, uploading.bytes));
|
|
46
|
+
sent++;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
failure = error;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (sent > 0) {
|
|
54
|
+
idle = 0;
|
|
55
|
+
stalled = 0;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
if (failure !== null && ++stalled >= STALLED_ROUNDS)
|
|
59
|
+
throw failure;
|
|
60
|
+
// A round that sent no part, its store unreachable or not yet started, is followed by the next finishing call on the jittered curve.
|
|
61
|
+
await paused(retryDelay(idle++));
|
|
62
|
+
}
|
|
63
|
+
let answer = await finishing(uploading, upload.id);
|
|
64
|
+
while ('checking' in answer) {
|
|
65
|
+
// A finishing call that finds House still checking the stored bytes is sent again after five seconds, the cadence at which House takes up a check it handed over.
|
|
66
|
+
await paused(5_000);
|
|
67
|
+
answer = await finishing(uploading, upload.id);
|
|
68
|
+
}
|
|
69
|
+
if (!('upload' in answer))
|
|
70
|
+
return answer;
|
|
71
|
+
upload = answer.upload;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function finishing(uploading, upload) {
|
|
75
|
+
for (let unsettled = 1;; unsettled++) {
|
|
76
|
+
try {
|
|
77
|
+
return await uploading.finish(upload);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (!(error instanceof Unsettled) || unsettled === STALLED_ROUNDS)
|
|
21
81
|
throw error;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
await writeFile(path, verified(Buffer.from(await answer.arrayBuffer()), file.sha256, file.name));
|
|
26
|
-
paths.push(path);
|
|
82
|
+
}
|
|
83
|
+
// 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.
|
|
84
|
+
await paused(retryDelay(unsettled - 1));
|
|
27
85
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
86
|
+
}
|
|
87
|
+
async function drained(body) {
|
|
88
|
+
for await (const _chunk of body)
|
|
89
|
+
;
|
|
90
|
+
}
|
|
91
|
+
export async function downloaded(address, fetching, destination, name) {
|
|
92
|
+
for (let asked = 1;; asked++) {
|
|
93
|
+
const { download, sha256 } = await address();
|
|
94
|
+
const answer = await fetching(download);
|
|
95
|
+
if (answer.status === 403 && asked === 1) {
|
|
96
|
+
await drained(answer.body);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (answer.status !== 200) {
|
|
100
|
+
await drained(answer.body);
|
|
101
|
+
throw storeRefusal('download', answer.status);
|
|
102
|
+
}
|
|
103
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
104
|
+
const partial = join(dirname(destination), `.${basename(destination)}.${randomUUID()}`);
|
|
105
|
+
const digest = createHash('sha256');
|
|
106
|
+
const hashed = new Transform({
|
|
107
|
+
transform(chunk, _encoding, done) {
|
|
108
|
+
digest.update(chunk);
|
|
109
|
+
done(null, chunk);
|
|
110
|
+
},
|
|
36
111
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
112
|
+
try {
|
|
113
|
+
await pipeline(answer.body, hashed, createWriteStream(partial, { flags: 'wx' }));
|
|
114
|
+
if (digest.digest('hex') !== sha256)
|
|
115
|
+
throw new Error(`${name} did not arrive whole; download it again`);
|
|
116
|
+
await rename(partial, destination);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
await rm(partial, { force: true });
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export async function fetched(url, signal) {
|
|
126
|
+
const answer = await fetch(relayed(url), { signal });
|
|
127
|
+
return { status: answer.status, body: answer.body ?? Readable.from([]) };
|
|
42
128
|
}
|
package/dist/git.js
CHANGED
|
@@ -8,7 +8,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 = '
|
|
11
|
+
const COMMITTER = 'Kit <house-kit@localhost>';
|
|
12
12
|
export function run(cwd, args, input, env = {}) {
|
|
13
13
|
return new Promise((resolve, reject) => {
|
|
14
14
|
const child = spawn('git', args, { cwd, env: { ...withoutProxy(), ...env } });
|
package/dist/house-cli.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { request, setGlobalProxyFromEnv } from 'node:http';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { request as httpsRequest } from 'node:https';
|
|
6
|
+
import { basename, extname, join, resolve } from 'node:path';
|
|
7
|
+
import { Readable } from 'node:stream';
|
|
8
|
+
import { pipeline } from 'node:stream/promises';
|
|
9
|
+
import { downloaded, inspectedFile, storeRefusal, uploaded } from './files.js';
|
|
6
10
|
import { HOUSE_REF } from './git.js';
|
|
7
11
|
import { ownerSocket, readOwnAgent } from './home.js';
|
|
8
|
-
import { callHouse, rpc, UNREACHABLE } from './mcp.js';
|
|
9
|
-
import { refusalOf } from './refusals.js';
|
|
12
|
+
import { callHouse, rpc, UNREACHABLE, Unsettled } from './mcp.js';
|
|
13
|
+
import { refusalOf, retryable } from './refusals.js';
|
|
10
14
|
import { relayed, selectRelay } from './relay.js';
|
|
11
|
-
import { parse
|
|
15
|
+
import { parse } from 'yaml';
|
|
12
16
|
const USAGE = "usage: house <tool> ['<arguments as JSON>']";
|
|
13
17
|
const PUSH_USAGE = 'usage: house push [commit] [--owner]';
|
|
14
18
|
const PUSH = "push [commit]: sends the Local copy's committed changes to House";
|
|
@@ -51,15 +55,12 @@ const MEDIA_TYPES = new Map([
|
|
|
51
55
|
['xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
|
52
56
|
['zip', 'application/zip'],
|
|
53
57
|
]);
|
|
54
|
-
function refused(path,
|
|
58
|
+
function refused(path, status, text, local) {
|
|
55
59
|
const refusal = refusalOf(text);
|
|
56
60
|
if (refusal !== null)
|
|
57
61
|
return refusal.text;
|
|
58
62
|
if (status === 401)
|
|
59
63
|
return REVOKED;
|
|
60
|
-
const act = method === 'GET' ? 'download' : 'upload';
|
|
61
|
-
if (URL.canParse(path))
|
|
62
|
-
return `the ${act} answered ${status}; ${act} the file again`;
|
|
63
64
|
if (status === 502 && local)
|
|
64
65
|
return text;
|
|
65
66
|
return path === '/' ? unconfirmed(status) : `${path} answered ${status}`;
|
|
@@ -67,26 +68,23 @@ function refused(path, method, status, text, local) {
|
|
|
67
68
|
function unconfirmed(status) {
|
|
68
69
|
return `House answered ${status} ${UNCONFIRMED}`;
|
|
69
70
|
}
|
|
70
|
-
function
|
|
71
|
+
function bridged(path, body, to = socketPath) {
|
|
71
72
|
return new Promise((resolve, reject) => {
|
|
72
|
-
const sent = request({ socketPath: to, path, method, headers }, (answer) => {
|
|
73
|
+
const sent = request({ socketPath: to, path, method: 'POST', headers: { 'content-type': 'application/json' } }, (answer) => {
|
|
73
74
|
const chunks = [];
|
|
74
75
|
answer.on('data', (chunk) => chunks.push(chunk));
|
|
75
76
|
answer.on('end', () => {
|
|
76
|
-
const
|
|
77
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
77
78
|
if (answer.statusCode === 200)
|
|
78
|
-
resolve(
|
|
79
|
+
resolve(text);
|
|
79
80
|
else
|
|
80
|
-
reject(new Error(refused(path,
|
|
81
|
+
reject(new Error(refused(path, answer.statusCode, text, answer.headers['content-type'] === undefined)));
|
|
81
82
|
});
|
|
82
83
|
});
|
|
83
84
|
sent.on('error', (error) => reject(new Error(error.code === 'EPERM' || error.code === 'EACCES' ? SANDBOXED : CLOSED)));
|
|
84
|
-
sent.end(
|
|
85
|
+
sent.end(JSON.stringify(body));
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
|
-
async function bridged(path, body, headers, to = socketPath) {
|
|
88
|
-
return (await exchanged(path, 'POST', body, headers, to)).toString('utf8');
|
|
89
|
-
}
|
|
90
88
|
async function direct(message) {
|
|
91
89
|
const ownAgent = await readOwnAgent();
|
|
92
90
|
if (ownAgent === null)
|
|
@@ -94,11 +92,14 @@ async function direct(message) {
|
|
|
94
92
|
selectRelay(process.env.HOUSE_KIT_RELAY || ownAgent.relay);
|
|
95
93
|
setGlobalProxyFromEnv();
|
|
96
94
|
const answer = await callHouse(ownAgent.house, ownAgent.credential, message).catch(() => {
|
|
97
|
-
throw new
|
|
95
|
+
throw new Unsettled(UNREACHABLE);
|
|
98
96
|
});
|
|
99
97
|
if (answer.status === 200)
|
|
100
98
|
return answer.text;
|
|
101
|
-
|
|
99
|
+
if (answer.status === 401)
|
|
100
|
+
throw new Error(DISCONNECTED);
|
|
101
|
+
const refusal = refusalOf(answer.text)?.text ?? unconfirmed(answer.status);
|
|
102
|
+
throw retryable(answer.text) === false ? new Error(refusal) : new Unsettled(refusal);
|
|
102
103
|
}
|
|
103
104
|
async function mcp(method, params) {
|
|
104
105
|
const message = rpc(method, params);
|
|
@@ -110,31 +111,51 @@ async function mcp(method, params) {
|
|
|
110
111
|
async function tools() {
|
|
111
112
|
return (await mcp('tools/list', {})).tools;
|
|
112
113
|
}
|
|
114
|
+
function unreadable(path, error) {
|
|
115
|
+
return new Error(error.code === 'EISDIR'
|
|
116
|
+
? `${path} is a folder; upload one file at a time`
|
|
117
|
+
: `${path} is not a readable file`);
|
|
118
|
+
}
|
|
113
119
|
async function localFile(path) {
|
|
114
120
|
try {
|
|
115
121
|
return await readFile(path);
|
|
116
122
|
}
|
|
117
123
|
catch (error) {
|
|
118
|
-
throw
|
|
119
|
-
? `${path} is a folder; upload one file at a time`
|
|
120
|
-
: `${path} is not a readable file`);
|
|
124
|
+
throw unreadable(path, error);
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
function opened(url, method, headers, body) {
|
|
128
|
+
return new Promise((answered, failed) => {
|
|
129
|
+
const local = socketPath !== undefined;
|
|
130
|
+
const target = local ? null : relayed(url);
|
|
131
|
+
if (!local)
|
|
132
|
+
setGlobalProxyFromEnv();
|
|
133
|
+
const sent = target === null
|
|
134
|
+
? request({ socketPath, path: url, method, headers }, answered)
|
|
135
|
+
: (target.protocol === 'https:' ? httpsRequest : request)(target, { method, headers }, answered);
|
|
136
|
+
sent.on('error', (error) => failed(new Error(!local ? UNREACHABLE : error.code === 'EPERM' || error.code === 'EACCES' ? SANDBOXED : CLOSED)));
|
|
137
|
+
if (body instanceof Readable)
|
|
138
|
+
pipeline(body, sent).catch(() => undefined);
|
|
139
|
+
else
|
|
140
|
+
sent.end(body);
|
|
129
141
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
throw new Error(refused(url, method, answer.status, received.toString('utf8'), false));
|
|
142
|
+
}
|
|
143
|
+
async function textOf(answer) {
|
|
144
|
+
return Buffer.concat(await answer.toArray()).toString('utf8');
|
|
134
145
|
}
|
|
135
146
|
async function sent(url, bytes, operation) {
|
|
136
147
|
const headers = { 'content-type': 'application/octet-stream', 'x-house-byte-operation': operation };
|
|
137
|
-
|
|
148
|
+
const answer = await opened(url, 'POST', headers, bytes);
|
|
149
|
+
const text = await textOf(answer);
|
|
150
|
+
if (answer.statusCode === 200)
|
|
151
|
+
return text;
|
|
152
|
+
throw new Error(refusalOf(text)?.text ?? storeRefusal('upload', answer.statusCode).message);
|
|
153
|
+
}
|
|
154
|
+
async function sentPart(url, file, start, end) {
|
|
155
|
+
const answer = await opened(url, 'PUT', { 'content-length': String(end - start) }, end === start ? undefined : createReadStream(file, { start, end: end - 1 }));
|
|
156
|
+
await answer.toArray();
|
|
157
|
+
if (answer.statusCode !== 200)
|
|
158
|
+
throw storeRefusal('upload', answer.statusCode);
|
|
138
159
|
}
|
|
139
160
|
async function download(args) {
|
|
140
161
|
const { flags, positionals: [argument] } = accepted(args, ['--help'], 1, DOWNLOAD_USAGE);
|
|
@@ -143,37 +164,52 @@ async function download(args) {
|
|
|
143
164
|
const { file, ...selected } = argumentsOf(argument);
|
|
144
165
|
if (typeof file !== 'string' || file === '')
|
|
145
166
|
throw new Error('download needs "file", the local file to write');
|
|
146
|
-
const inspected = parse(await called('inspect', selected));
|
|
147
|
-
if (typeof inspected?.download !== 'string')
|
|
148
|
-
throw new Error(`${String(selected.path)} holds no binary file; shell reads its text`);
|
|
149
|
-
const bytes = verified(await transferred(inspected.download, 'GET'), String(inspected.sha256), String(selected.path));
|
|
150
167
|
const written = resolve(file);
|
|
151
|
-
await
|
|
152
|
-
|
|
168
|
+
await downloaded(async () => {
|
|
169
|
+
const file = inspectedFile(await called('inspect', selected));
|
|
170
|
+
if (file === null)
|
|
171
|
+
throw new Error(`${String(selected.path)} holds no binary file; shell reads its text`);
|
|
172
|
+
return file;
|
|
173
|
+
}, async (url) => {
|
|
174
|
+
const answer = await opened(url, 'GET', {});
|
|
175
|
+
return { status: answer.statusCode, body: answer };
|
|
176
|
+
}, written, String(selected.path));
|
|
153
177
|
return written;
|
|
154
178
|
}
|
|
155
179
|
async function described(file) {
|
|
156
|
-
const
|
|
180
|
+
const digest = createHash('sha256');
|
|
181
|
+
let bytes = 0;
|
|
182
|
+
try {
|
|
183
|
+
for await (const chunk of createReadStream(file)) {
|
|
184
|
+
digest.update(chunk);
|
|
185
|
+
bytes += chunk.length;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw unreadable(file, error);
|
|
190
|
+
}
|
|
157
191
|
const mediaType = MEDIA_TYPES.get(extname(file).slice(1).toLowerCase());
|
|
158
|
-
return {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
},
|
|
165
|
-
};
|
|
192
|
+
return { bytes, sha256: digest.digest('hex'), ...(mediaType === undefined ? {} : { media_type: mediaType }) };
|
|
193
|
+
}
|
|
194
|
+
async function finished(upload, room) {
|
|
195
|
+
const text = await called('upload', { upload, room_ref: room });
|
|
196
|
+
const answer = parse(text);
|
|
197
|
+
return answer !== null && ('upload' in answer || 'checking' in answer) ? answer : { text };
|
|
166
198
|
}
|
|
167
199
|
async function upload({ file, ...destination }) {
|
|
168
200
|
if (typeof file !== 'string' || file === '')
|
|
169
201
|
throw new Error('upload needs "file", a local file');
|
|
170
|
-
const
|
|
202
|
+
const description = await described(file);
|
|
171
203
|
const answer = await called('upload', { ...destination, ...description });
|
|
172
204
|
const declared = parse(answer);
|
|
173
|
-
if (declared
|
|
205
|
+
if (declared?.upload === undefined)
|
|
174
206
|
return answer;
|
|
175
|
-
const
|
|
176
|
-
|
|
207
|
+
const accepted = await uploaded(declared.upload, {
|
|
208
|
+
bytes: description.bytes,
|
|
209
|
+
send: (url, start, end) => sentPart(url, file, start, end),
|
|
210
|
+
finish: (id) => finished(id, destination.room_ref),
|
|
211
|
+
});
|
|
212
|
+
return accepted.text;
|
|
177
213
|
}
|
|
178
214
|
async function appendRecord(args) {
|
|
179
215
|
const held = new Map();
|
|
@@ -181,8 +217,8 @@ async function appendRecord(args) {
|
|
|
181
217
|
? await Promise.all(args.attachments.map(async (attachment) => {
|
|
182
218
|
if (typeof attachment !== 'string')
|
|
183
219
|
return attachment;
|
|
184
|
-
const
|
|
185
|
-
held.set(description.sha256,
|
|
220
|
+
const description = await described(attachment);
|
|
221
|
+
held.set(description.sha256, attachment);
|
|
186
222
|
return { name: basename(attachment), ...description };
|
|
187
223
|
}))
|
|
188
224
|
: args.attachments;
|
|
@@ -193,7 +229,7 @@ async function appendRecord(args) {
|
|
|
193
229
|
if (upload === undefined)
|
|
194
230
|
return answer;
|
|
195
231
|
for (const grant of upload)
|
|
196
|
-
await sent(grant.url, held.get(grant.sha256), grant.operation);
|
|
232
|
+
await sent(grant.url, await localFile(held.get(grant.sha256)), grant.operation);
|
|
197
233
|
}
|
|
198
234
|
}
|
|
199
235
|
function unexpected(argument, usage) {
|
|
@@ -220,7 +256,7 @@ async function push(args) {
|
|
|
220
256
|
throw new Error('--owner is not for Agent conversations; run house push without it');
|
|
221
257
|
if (!owner && socketPath === undefined)
|
|
222
258
|
throw new Error('outside an Agent conversation, house push needs --owner');
|
|
223
|
-
const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit },
|
|
259
|
+
const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit }, owner ? ownerSocket() : socketPath));
|
|
224
260
|
if (pushed.refused)
|
|
225
261
|
throw new Error(pushed.text);
|
|
226
262
|
return pushed.text;
|
|
@@ -228,7 +264,7 @@ async function push(args) {
|
|
|
228
264
|
async function fetched(args) {
|
|
229
265
|
if (accepted(args, ['--help'], 0, FETCH_USAGE).flags.has('--help'))
|
|
230
266
|
return FETCH_USAGE;
|
|
231
|
-
const answer = JSON.parse(await bridged('/git/fetch', {},
|
|
267
|
+
const answer = JSON.parse(await bridged('/git/fetch', {}, socketPath ?? ownerSocket()));
|
|
232
268
|
if (answer.refused)
|
|
233
269
|
throw new Error(answer.text);
|
|
234
270
|
return answer.text;
|
package/dist/instructions.js
CHANGED
|
@@ -4,9 +4,9 @@ const SOURCE_LINE = "From House:";
|
|
|
4
4
|
const HOUSE_LINE = "Work with House through the `house` CLI: run `house --help`, and `house find_command` for House's guides and formats.";
|
|
5
5
|
const FILE_LINE = 'To give the User a file, upload it with `house upload` and link it; for an App, a Site or a website, follow `house find_command \'{"query":"app"}\'`. Never start a server.';
|
|
6
6
|
const READ_LINE = "Optional per-Room instructions: `/rooms/private/rooms/<handle>/AGENTS.md`.";
|
|
7
|
-
const
|
|
7
|
+
const REQUEST_LINE = "For another Agent's help, `send_routine_request`; its mail comes back into this conversation. If an Agent of your User has no Routine to call, write one in the Private Room and `create_routine_access` with your User as its only caller.";
|
|
8
8
|
const COPIES_LINE = `House Local copies; before work run \`house fetch\` and rebase onto \`${HOUSE_REF}\`; after it commit, then \`house push\`:`;
|
|
9
9
|
export async function instructions(bridge, base, copies) {
|
|
10
|
-
const lines = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE,
|
|
10
|
+
const lines = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE, REQUEST_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
|
|
11
11
|
return [lines.join('\n'), base, (await readHowWeWork(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
|
|
12
12
|
}
|
package/dist/kit-cli.js
CHANGED
|
@@ -11,7 +11,7 @@ const COMMANDS = new Map([
|
|
|
11
11
|
'logout',
|
|
12
12
|
{
|
|
13
13
|
usage: 'usage: kit logout',
|
|
14
|
-
about: "disconnects this computer, stops
|
|
14
|
+
about: "disconnects this computer, stops the Kit process and ends your own agent's connection",
|
|
15
15
|
operands: 0,
|
|
16
16
|
run: logout,
|
|
17
17
|
},
|
|
@@ -20,7 +20,7 @@ const COMMANDS = new Map([
|
|
|
20
20
|
'resident',
|
|
21
21
|
{
|
|
22
22
|
usage: 'usage: kit resident',
|
|
23
|
-
about: "keeps this Environment connected;
|
|
23
|
+
about: "keeps this Environment connected; Kit's service runs it",
|
|
24
24
|
operands: 0,
|
|
25
25
|
run: resident,
|
|
26
26
|
},
|