@agentshouse/kit 0.2.1-c7a397cb0cce7 → 0.2.1-c7e034a9950ad
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 +22 -22
- 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/bridge.js +1 -1
- package/dist/{inputs.js → calls.js} +20 -20
- package/dist/clis.js +6 -3
- package/dist/{stream.js → connection.js} +30 -27
- package/dist/conversations.js +144 -84
- 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 +81 -86
- package/dist/instructions.js +28 -8
- 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 +38 -32
- package/dist/service.js +1 -1
- package/dist/sign-in.js +19 -19
- package/dist/skills.js +12 -93
- package/dist/transcription.js +51 -20
- package/dist/update.js +32 -32
- package/dist/web-wizard.js +100 -0
- package/dist/wire.js +19 -3
- package/package.json +6 -6
- package/how-we-work.md +0 -33
- package/skills/ask-matt/LICENSE +0 -21
- package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
- package/skills/ask-matt/SKILL.md +0 -97
- package/skills/ask-matt/agents/openai.yaml +0 -5
- package/skills/code-review/LICENSE +0 -21
- package/skills/code-review/SKILL.md +0 -89
- package/skills/code-review/agents/openai.yaml +0 -3
- package/skills/codebase-design/DEEPENING.md +0 -37
- package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
- package/skills/codebase-design/LICENSE +0 -21
- package/skills/codebase-design/SKILL.md +0 -114
- package/skills/codebase-design/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/LICENSE +0 -21
- package/skills/diagnosing-bugs/SKILL.md +0 -138
- package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
- package/skills/domain-modeling/ADR-FORMAT.md +0 -47
- package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
- package/skills/domain-modeling/LICENSE +0 -21
- package/skills/domain-modeling/SKILL.md +0 -74
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/LICENSE +0 -21
- package/skills/grill-me/SKILL.md +0 -7
- package/skills/grill-me/agents/openai.yaml +0 -5
- package/skills/grill-with-docs/LICENSE +0 -21
- package/skills/grill-with-docs/SKILL.md +0 -7
- package/skills/grill-with-docs/agents/openai.yaml +0 -5
- package/skills/grilling/LICENSE +0 -21
- package/skills/grilling/SKILL.md +0 -30
- package/skills/grilling/agents/openai.yaml +0 -3
- package/skills/handoff/LICENSE +0 -21
- package/skills/handoff/SKILL.md +0 -16
- package/skills/handoff/agents/openai.yaml +0 -5
- package/skills/implement/LICENSE +0 -21
- package/skills/implement/SKILL.md +0 -17
- package/skills/implement/agents/openai.yaml +0 -5
- package/skills/implement-spec/LICENSE +0 -21
- package/skills/implement-spec/SKILL.md +0 -40
- package/skills/implement-spec/agents/openai.yaml +0 -5
- package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
- package/skills/improve-codebase-architecture/LICENSE +0 -21
- package/skills/improve-codebase-architecture/SKILL.md +0 -71
- package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
- package/skills/pr/CREDITS.md +0 -3
- package/skills/pr/LICENSE +0 -21
- package/skills/pr/SKILL.md +0 -170
- package/skills/pr/agents/openai.yaml +0 -3
- package/skills/prototype/LICENSE +0 -21
- package/skills/prototype/LOGIC.md +0 -67
- package/skills/prototype/SKILL.md +0 -26
- package/skills/prototype/UI.md +0 -112
- package/skills/prototype/agents/openai.yaml +0 -3
- package/skills/research/LICENSE +0 -21
- package/skills/research/SKILL.md +0 -12
- package/skills/research/agents/openai.yaml +0 -3
- package/skills/retro/LICENSE +0 -21
- package/skills/retro/SKILL.md +0 -44
- package/skills/retro/agents/openai.yaml +0 -5
- package/skills/tdd/LICENSE +0 -21
- package/skills/tdd/SKILL.md +0 -38
- package/skills/tdd/agents/openai.yaml +0 -3
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/tests.md +0 -77
- package/skills/teach/GLOSSARY-FORMAT.md +0 -35
- package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
- package/skills/teach/LICENSE +0 -21
- package/skills/teach/MISSION-FORMAT.md +0 -31
- package/skills/teach/RESOURCES-FORMAT.md +0 -32
- package/skills/teach/SKILL.md +0 -140
- package/skills/teach/agents/openai.yaml +0 -5
- package/skills/to-questionnaire/LICENSE +0 -21
- package/skills/to-questionnaire/SKILL.md +0 -54
- package/skills/to-questionnaire/agents/openai.yaml +0 -5
- package/skills/to-spec/LICENSE +0 -21
- package/skills/to-spec/SKILL.md +0 -75
- package/skills/to-spec/agents/openai.yaml +0 -5
- package/skills/to-tickets/LICENSE +0 -21
- package/skills/to-tickets/SKILL.md +0 -105
- package/skills/to-tickets/agents/openai.yaml +0 -5
- package/skills/triage/AGENT-BRIEF.md +0 -207
- package/skills/triage/LICENSE +0 -21
- package/skills/triage/OUT-OF-SCOPE.md +0 -105
- package/skills/triage/SKILL.md +0 -112
- package/skills/triage/agents/openai.yaml +0 -5
- package/skills/wait-what/LICENSE +0 -21
- package/skills/wait-what/SKILL.md +0 -7
- package/skills/wait-what/agents/openai.yaml +0 -5
- package/skills/wayfinder/LICENSE +0 -21
- package/skills/wayfinder/SKILL.md +0 -128
- package/skills/wayfinder/agents/openai.yaml +0 -5
- package/skills/wizard/LICENSE +0 -21
- package/skills/wizard/SKILL.md +0 -44
- package/skills/wizard/agents/openai.yaml +0 -3
- package/skills/wizard/template.sh +0 -223
- package/skills/writing-for-agents/LICENSE +0 -21
- package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
- package/skills/writing-for-agents/SKILL.md +0 -81
- package/skills/writing-for-agents/agents/openai.yaml +0 -3
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,17 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
3
|
import { request, setGlobalProxyFromEnv } from 'node:http';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { request as httpsRequest } from 'node:https';
|
|
5
|
+
import { extname, join, resolve } from 'node:path';
|
|
6
|
+
import { Readable } from 'node:stream';
|
|
7
|
+
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import { downloaded, inspectedFile, storeRefusal, uploaded } from './files.js';
|
|
6
9
|
import { HOUSE_REF } from './git.js';
|
|
7
10
|
import { ownerSocket, readOwnAgent } from './home.js';
|
|
8
|
-
import { callHouse, rpc, UNREACHABLE } from './mcp.js';
|
|
9
|
-
import { refusalOf } from './refusals.js';
|
|
11
|
+
import { callHouse, rpc, UNREACHABLE, Unsettled } from './mcp.js';
|
|
12
|
+
import { refusalOf, retryable } from './refusals.js';
|
|
10
13
|
import { relayed, selectRelay } from './relay.js';
|
|
11
|
-
import { parse
|
|
14
|
+
import { parse } from 'yaml';
|
|
12
15
|
const USAGE = "usage: house <tool> ['<arguments as JSON>']";
|
|
13
16
|
const PUSH_USAGE = 'usage: house push [commit] [--owner]';
|
|
14
17
|
const PUSH = "push [commit]: sends the Local copy's committed changes to House";
|
|
@@ -51,15 +54,12 @@ const MEDIA_TYPES = new Map([
|
|
|
51
54
|
['xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
|
52
55
|
['zip', 'application/zip'],
|
|
53
56
|
]);
|
|
54
|
-
function refused(path,
|
|
57
|
+
function refused(path, status, text, local) {
|
|
55
58
|
const refusal = refusalOf(text);
|
|
56
59
|
if (refusal !== null)
|
|
57
60
|
return refusal.text;
|
|
58
61
|
if (status === 401)
|
|
59
62
|
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
63
|
if (status === 502 && local)
|
|
64
64
|
return text;
|
|
65
65
|
return path === '/' ? unconfirmed(status) : `${path} answered ${status}`;
|
|
@@ -67,26 +67,23 @@ function refused(path, method, status, text, local) {
|
|
|
67
67
|
function unconfirmed(status) {
|
|
68
68
|
return `House answered ${status} ${UNCONFIRMED}`;
|
|
69
69
|
}
|
|
70
|
-
function
|
|
70
|
+
function bridged(path, body, to = socketPath) {
|
|
71
71
|
return new Promise((resolve, reject) => {
|
|
72
|
-
const sent = request({ socketPath: to, path, method, headers }, (answer) => {
|
|
72
|
+
const sent = request({ socketPath: to, path, method: 'POST', headers: { 'content-type': 'application/json' } }, (answer) => {
|
|
73
73
|
const chunks = [];
|
|
74
74
|
answer.on('data', (chunk) => chunks.push(chunk));
|
|
75
75
|
answer.on('end', () => {
|
|
76
|
-
const
|
|
76
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
77
77
|
if (answer.statusCode === 200)
|
|
78
|
-
resolve(
|
|
78
|
+
resolve(text);
|
|
79
79
|
else
|
|
80
|
-
reject(new Error(refused(path,
|
|
80
|
+
reject(new Error(refused(path, answer.statusCode, text, answer.headers['content-type'] === undefined)));
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
83
|
sent.on('error', (error) => reject(new Error(error.code === 'EPERM' || error.code === 'EACCES' ? SANDBOXED : CLOSED)));
|
|
84
|
-
sent.end(
|
|
84
|
+
sent.end(JSON.stringify(body));
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
|
-
async function bridged(path, body, headers, to = socketPath) {
|
|
88
|
-
return (await exchanged(path, 'POST', body, headers, to)).toString('utf8');
|
|
89
|
-
}
|
|
90
87
|
async function direct(message) {
|
|
91
88
|
const ownAgent = await readOwnAgent();
|
|
92
89
|
if (ownAgent === null)
|
|
@@ -94,11 +91,14 @@ async function direct(message) {
|
|
|
94
91
|
selectRelay(process.env.HOUSE_KIT_RELAY || ownAgent.relay);
|
|
95
92
|
setGlobalProxyFromEnv();
|
|
96
93
|
const answer = await callHouse(ownAgent.house, ownAgent.credential, message).catch(() => {
|
|
97
|
-
throw new
|
|
94
|
+
throw new Unsettled(UNREACHABLE);
|
|
98
95
|
});
|
|
99
96
|
if (answer.status === 200)
|
|
100
97
|
return answer.text;
|
|
101
|
-
|
|
98
|
+
if (answer.status === 401)
|
|
99
|
+
throw new Error(DISCONNECTED);
|
|
100
|
+
const refusal = refusalOf(answer.text)?.text ?? unconfirmed(answer.status);
|
|
101
|
+
throw retryable(answer.text) === false ? new Error(refusal) : new Unsettled(refusal);
|
|
102
102
|
}
|
|
103
103
|
async function mcp(method, params) {
|
|
104
104
|
const message = rpc(method, params);
|
|
@@ -110,31 +110,32 @@ async function mcp(method, params) {
|
|
|
110
110
|
async function tools() {
|
|
111
111
|
return (await mcp('tools/list', {})).tools;
|
|
112
112
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
catch (error) {
|
|
118
|
-
throw new Error(error.code === 'EISDIR'
|
|
119
|
-
? `${path} is a folder; upload one file at a time`
|
|
120
|
-
: `${path} is not a readable file`);
|
|
121
|
-
}
|
|
113
|
+
function unreadable(path, error) {
|
|
114
|
+
return new Error(error.code === 'EISDIR'
|
|
115
|
+
? `${path} is a folder; upload one file at a time`
|
|
116
|
+
: `${path} is not a readable file`);
|
|
122
117
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
function opened(url, method, headers, body) {
|
|
119
|
+
return new Promise((answered, failed) => {
|
|
120
|
+
const local = socketPath !== undefined;
|
|
121
|
+
const target = local ? null : relayed(url);
|
|
122
|
+
if (!local)
|
|
123
|
+
setGlobalProxyFromEnv();
|
|
124
|
+
const sent = target === null
|
|
125
|
+
? request({ socketPath, path: url, method, headers }, answered)
|
|
126
|
+
: (target.protocol === 'https:' ? httpsRequest : request)(target, { method, headers }, answered);
|
|
127
|
+
sent.on('error', (error) => failed(new Error(!local ? UNREACHABLE : error.code === 'EPERM' || error.code === 'EACCES' ? SANDBOXED : CLOSED)));
|
|
128
|
+
if (body === undefined)
|
|
129
|
+
sent.end();
|
|
130
|
+
else
|
|
131
|
+
pipeline(body, sent).catch(() => undefined);
|
|
129
132
|
});
|
|
130
|
-
const received = Buffer.from(await answer.arrayBuffer());
|
|
131
|
-
if (answer.status === 200)
|
|
132
|
-
return received;
|
|
133
|
-
throw new Error(refused(url, method, answer.status, received.toString('utf8'), false));
|
|
134
133
|
}
|
|
135
|
-
async function
|
|
136
|
-
const
|
|
137
|
-
|
|
134
|
+
async function sentPart(url, file, start, end) {
|
|
135
|
+
const answer = await opened(url, 'PUT', { 'content-length': String(end - start) }, end === start ? undefined : createReadStream(file, { start, end: end - 1 }));
|
|
136
|
+
await answer.toArray();
|
|
137
|
+
if (answer.statusCode !== 200)
|
|
138
|
+
throw storeRefusal('upload', answer.statusCode);
|
|
138
139
|
}
|
|
139
140
|
async function download(args) {
|
|
140
141
|
const { flags, positionals: [argument] } = accepted(args, ['--help'], 1, DOWNLOAD_USAGE);
|
|
@@ -143,58 +144,52 @@ async function download(args) {
|
|
|
143
144
|
const { file, ...selected } = argumentsOf(argument);
|
|
144
145
|
if (typeof file !== 'string' || file === '')
|
|
145
146
|
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
147
|
const written = resolve(file);
|
|
151
|
-
await
|
|
152
|
-
|
|
148
|
+
await downloaded(async () => {
|
|
149
|
+
const file = inspectedFile(await called('inspect', selected));
|
|
150
|
+
if (file === null)
|
|
151
|
+
throw new Error(`${String(selected.path)} holds no binary file; shell reads its text`);
|
|
152
|
+
return file;
|
|
153
|
+
}, async (url) => {
|
|
154
|
+
const answer = await opened(url, 'GET', {});
|
|
155
|
+
return { status: answer.statusCode, body: answer };
|
|
156
|
+
}, written, String(selected.path));
|
|
153
157
|
return written;
|
|
154
158
|
}
|
|
155
159
|
async function described(file) {
|
|
156
|
-
const
|
|
160
|
+
const digest = createHash('sha256');
|
|
161
|
+
let bytes = 0;
|
|
162
|
+
try {
|
|
163
|
+
for await (const chunk of createReadStream(file)) {
|
|
164
|
+
digest.update(chunk);
|
|
165
|
+
bytes += chunk.length;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
throw unreadable(file, error);
|
|
170
|
+
}
|
|
157
171
|
const mediaType = MEDIA_TYPES.get(extname(file).slice(1).toLowerCase());
|
|
158
|
-
return {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
},
|
|
165
|
-
};
|
|
172
|
+
return { bytes, sha256: digest.digest('hex'), ...(mediaType === undefined ? {} : { media_type: mediaType }) };
|
|
173
|
+
}
|
|
174
|
+
async function finished(upload, room) {
|
|
175
|
+
const text = await called('upload', { upload, room_ref: room });
|
|
176
|
+
const answer = parse(text);
|
|
177
|
+
return answer !== null && ('upload' in answer || 'checking' in answer) ? answer : { text };
|
|
166
178
|
}
|
|
167
179
|
async function upload({ file, ...destination }) {
|
|
168
180
|
if (typeof file !== 'string' || file === '')
|
|
169
181
|
throw new Error('upload needs "file", a local file');
|
|
170
|
-
const
|
|
182
|
+
const description = await described(file);
|
|
171
183
|
const answer = await called('upload', { ...destination, ...description });
|
|
172
184
|
const declared = parse(answer);
|
|
173
|
-
if (declared
|
|
185
|
+
if (declared?.upload === undefined)
|
|
174
186
|
return answer;
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
? await Promise.all(args.attachments.map(async (attachment) => {
|
|
182
|
-
if (typeof attachment !== 'string')
|
|
183
|
-
return attachment;
|
|
184
|
-
const { bytes, description } = await described(attachment);
|
|
185
|
-
held.set(description.sha256, bytes);
|
|
186
|
-
return { name: basename(attachment), ...description };
|
|
187
|
-
}))
|
|
188
|
-
: args.attachments;
|
|
189
|
-
const record = { ...args, attachments };
|
|
190
|
-
for (;;) {
|
|
191
|
-
const answer = await called('append_record', record);
|
|
192
|
-
const { upload } = parse(answer);
|
|
193
|
-
if (upload === undefined)
|
|
194
|
-
return answer;
|
|
195
|
-
for (const grant of upload)
|
|
196
|
-
await sent(grant.url, held.get(grant.sha256), grant.operation);
|
|
197
|
-
}
|
|
187
|
+
const accepted = await uploaded(declared.upload, {
|
|
188
|
+
bytes: description.bytes,
|
|
189
|
+
send: (url, start, end) => sentPart(url, file, start, end),
|
|
190
|
+
finish: (id) => finished(id, destination.room_ref),
|
|
191
|
+
});
|
|
192
|
+
return accepted.text;
|
|
198
193
|
}
|
|
199
194
|
function unexpected(argument, usage) {
|
|
200
195
|
return new Error(`unexpected argument ${argument}; ${usage}`);
|
|
@@ -220,7 +215,7 @@ async function push(args) {
|
|
|
220
215
|
throw new Error('--owner is not for Agent conversations; run house push without it');
|
|
221
216
|
if (!owner && socketPath === undefined)
|
|
222
217
|
throw new Error('outside an Agent conversation, house push needs --owner');
|
|
223
|
-
const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit },
|
|
218
|
+
const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit }, owner ? ownerSocket() : socketPath));
|
|
224
219
|
if (pushed.refused)
|
|
225
220
|
throw new Error(pushed.text);
|
|
226
221
|
return pushed.text;
|
|
@@ -228,7 +223,7 @@ async function push(args) {
|
|
|
228
223
|
async function fetched(args) {
|
|
229
224
|
if (accepted(args, ['--help'], 0, FETCH_USAGE).flags.has('--help'))
|
|
230
225
|
return FETCH_USAGE;
|
|
231
|
-
const answer = JSON.parse(await bridged('/git/fetch', {},
|
|
226
|
+
const answer = JSON.parse(await bridged('/git/fetch', {}, socketPath ?? ownerSocket()));
|
|
232
227
|
if (answer.refused)
|
|
233
228
|
throw new Error(answer.text);
|
|
234
229
|
return answer.text;
|
|
@@ -263,7 +258,7 @@ async function house(args) {
|
|
|
263
258
|
return [...(tool.description === undefined ? [] : [tool.description]), JSON.stringify(tool.inputSchema)].join('\n');
|
|
264
259
|
}
|
|
265
260
|
const parsed = argumentsOf(argument);
|
|
266
|
-
return verb === 'upload' ? upload(parsed) :
|
|
261
|
+
return verb === 'upload' ? upload(parsed) : called(verb, parsed);
|
|
267
262
|
}
|
|
268
263
|
async function called(name, args) {
|
|
269
264
|
const result = await mcp('tools/call', { name, arguments: args });
|
package/dist/instructions.js
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
|
+
import { agentInstructions } from '@agentshouse/skills';
|
|
1
2
|
import { HOUSE_REF } from './git.js';
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
3
|
+
const HOW_WE_WORK = '/rooms/private/how-we-work.md';
|
|
4
|
+
const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
|
|
5
|
+
const FAILED = /^exit: [1-9]\d*$/;
|
|
6
|
+
const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
|
|
7
|
+
function cutOf(lines) {
|
|
8
|
+
const cut = CUT.exec(lines[0] ?? '');
|
|
9
|
+
return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
|
|
10
|
+
}
|
|
11
|
+
async function readHowWeWork(bridge) {
|
|
12
|
+
let document = '';
|
|
13
|
+
for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
|
|
14
|
+
const read = await bridge.tool('shell', { command });
|
|
15
|
+
const text = read.content.map((content) => content.text).join('\n');
|
|
16
|
+
const lines = text.split('\n');
|
|
17
|
+
if (read.isError === true || (FAILED.test(lines[0] ?? '') && lines[1]?.startsWith('stderr: '))) {
|
|
18
|
+
const refusal = read.isError === true ? text : lines.slice(1).join('\n').replace(/^stderr: /gm, '');
|
|
19
|
+
if (UNREADABLE.test(refusal))
|
|
20
|
+
return '';
|
|
21
|
+
throw new Error(refusal);
|
|
22
|
+
}
|
|
23
|
+
const cut = cutOf(lines);
|
|
24
|
+
document += lines.slice(cut === null ? 0 : 1).join('\n');
|
|
25
|
+
command = cut?.[2];
|
|
26
|
+
}
|
|
27
|
+
return document;
|
|
28
|
+
}
|
|
9
29
|
export async function instructions(bridge, base, copies) {
|
|
10
|
-
const lines =
|
|
30
|
+
const lines = agentInstructions(HOUSE_REF, copies);
|
|
11
31
|
return [lines.join('\n'), base, (await readHowWeWork(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
|
|
12
32
|
}
|
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
|
},
|
package/dist/login.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
-
import { hostname } from 'node:os';
|
|
4
3
|
import { parseArgs } from 'node:util';
|
|
5
4
|
import { HouseRefusal, post, retrying } from './api.js';
|
|
6
5
|
import { placeAppGit } from './app-git.js';
|
|
@@ -91,13 +90,12 @@ export async function login(argv) {
|
|
|
91
90
|
let named = environment;
|
|
92
91
|
let exchanged;
|
|
93
92
|
if (values.code !== undefined) {
|
|
94
|
-
exchanged = await post(house, '/kit/token', { code: values.code
|
|
93
|
+
exchanged = await post(house, '/kit/token', { code: values.code });
|
|
95
94
|
}
|
|
96
95
|
else {
|
|
97
96
|
held = ownAgent !== null && ownAgent.house === house && (await accepted(house, ownAgent.credential));
|
|
98
97
|
const verifier = randomBytes(32).toString('base64url');
|
|
99
98
|
const start = () => retrying(() => post(house, '/kit', {
|
|
100
|
-
label: hostname(),
|
|
101
99
|
...(named === undefined
|
|
102
100
|
? { act: 'new_environment', ...(replaces === undefined ? {} : { replaces }) }
|
|
103
101
|
: { act: 'existing_environment', environment_id: named }),
|
package/dist/mcp.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { KIT_RELEASE } from './clis.js';
|
|
3
3
|
import { relayed } from './relay.js';
|
|
4
4
|
const MCP_PROTOCOL_VERSION = '2026-07-28';
|
|
5
5
|
export const UNREACHABLE = 'House is unreachable; call again in a minute';
|
|
6
|
+
export class Unsettled extends Error {
|
|
7
|
+
}
|
|
6
8
|
export function rpc(method, params) {
|
|
7
9
|
return { jsonrpc: '2.0', id: randomUUID(), method, params };
|
|
8
10
|
}
|
|
@@ -13,7 +15,7 @@ export function mcpBody(message, operation) {
|
|
|
13
15
|
...message.params,
|
|
14
16
|
_meta: {
|
|
15
17
|
'io.modelcontextprotocol/protocolVersion': MCP_PROTOCOL_VERSION,
|
|
16
|
-
'io.modelcontextprotocol/clientInfo': { name: '@agentshouse/kit', version:
|
|
18
|
+
'io.modelcontextprotocol/clientInfo': { name: '@agentshouse/kit', version: KIT_RELEASE },
|
|
17
19
|
'io.modelcontextprotocol/clientCapabilities': {},
|
|
18
20
|
...(operation === null ? {} : { 'agents.house/agent-operation': operation }),
|
|
19
21
|
},
|
package/dist/placement.js
CHANGED
|
@@ -18,9 +18,9 @@ export async function updatesItself() {
|
|
|
18
18
|
// sudo answers at once, so ten seconds only bounds a stuck sudo.
|
|
19
19
|
return (await run('sudo', ['-n', 'true'], 10_000)).status === 0;
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
function recorded(name) {
|
|
22
22
|
try {
|
|
23
|
-
return readFileSync(join(kitHome(),
|
|
23
|
+
return readFileSync(join(kitHome(), name), 'utf8').trim();
|
|
24
24
|
}
|
|
25
25
|
catch (error) {
|
|
26
26
|
if (error.code === 'ENOENT')
|
|
@@ -28,3 +28,12 @@ export function installCommand() {
|
|
|
28
28
|
throw error;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
export function installCommand() {
|
|
32
|
+
return recorded('install-command');
|
|
33
|
+
}
|
|
34
|
+
export function machine() {
|
|
35
|
+
const placed = recorded('placement');
|
|
36
|
+
if (placed === 'container' || placed === 'server' || placed === 'windows')
|
|
37
|
+
return placed;
|
|
38
|
+
return process.platform === 'darwin' ? 'macos' : 'linux';
|
|
39
|
+
}
|