@agentshouse/kit 0.2.1-c96543a4c8b15 → 0.2.1-c9a26041e119a
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 +11 -10
- package/dist/agents.js +58 -4
- package/dist/api.js +5 -1
- package/dist/bootstrap.json +4 -4
- package/dist/chatgpt.js +26 -0
- package/dist/clis.js +10 -3
- package/dist/conversations.js +1 -0
- package/dist/copies.js +18 -8
- package/dist/end.js +4 -0
- package/dist/files.js +7 -1
- package/dist/git.js +11 -8
- package/dist/home.js +41 -4
- package/dist/house-main.js +99 -38
- package/dist/instructions.js +2 -27
- package/dist/kit-main.js +4 -1
- package/dist/login.js +34 -26
- package/dist/placement.js +48 -0
- package/dist/relay.js +1 -1
- package/dist/resident.js +8 -4
- package/dist/skills.js +81 -12
- package/dist/stream.js +62 -13
- package/dist/transcription.js +3 -3
- package/dist/update.js +28 -6
- package/dist/wire.js +6 -0
- package/how-we-work.md +13 -3
- package/package.json +2 -1
- package/skills/ask-matt/SKILL.md +17 -11
- package/skills/ask-matt/agents/openai.yaml +1 -0
- package/skills/code-review/SKILL.md +5 -5
- package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
- package/skills/codebase-design/SKILL.md +0 -2
- package/skills/diagnosing-bugs/SKILL.md +2 -4
- package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
- package/skills/domain-modeling/SKILL.md +12 -14
- package/skills/grill-me/SKILL.md +1 -2
- package/skills/grill-me/agents/openai.yaml +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -2
- package/skills/grill-with-docs/agents/openai.yaml +1 -0
- package/skills/grilling/SKILL.md +2 -2
- package/skills/handoff/SKILL.md +2 -3
- package/skills/handoff/agents/openai.yaml +1 -0
- package/skills/implement/SKILL.md +5 -4
- package/skills/implement/agents/openai.yaml +1 -0
- package/skills/implement-spec/SKILL.md +40 -0
- package/skills/implement-spec/agents/openai.yaml +5 -0
- package/skills/improve-codebase-architecture/SKILL.md +6 -7
- package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
- package/skills/pr/CREDITS.md +3 -0
- package/skills/pr/LICENSE +21 -0
- package/skills/pr/SKILL.md +170 -0
- package/skills/pr/agents/openai.yaml +3 -0
- package/skills/prototype/SKILL.md +0 -2
- package/skills/research/SKILL.md +0 -2
- package/skills/retro/LICENSE +21 -0
- package/skills/retro/SKILL.md +44 -0
- package/skills/retro/agents/openai.yaml +5 -0
- package/skills/tdd/SKILL.md +2 -4
- package/skills/teach/SKILL.md +3 -4
- package/skills/teach/agents/openai.yaml +1 -0
- package/skills/to-questionnaire/SKILL.md +1 -2
- package/skills/to-questionnaire/agents/openai.yaml +1 -0
- package/skills/to-spec/SKILL.md +1 -2
- package/skills/to-spec/agents/openai.yaml +1 -0
- package/skills/to-tickets/SKILL.md +3 -4
- package/skills/to-tickets/agents/openai.yaml +1 -0
- package/skills/triage/AGENT-BRIEF.md +207 -0
- package/skills/triage/LICENSE +21 -0
- package/skills/triage/OUT-OF-SCOPE.md +105 -0
- package/skills/triage/SKILL.md +112 -0
- package/skills/triage/agents/openai.yaml +5 -0
- package/skills/wait-what/SKILL.md +2 -3
- package/skills/wait-what/agents/openai.yaml +1 -0
- package/skills/wayfinder/SKILL.md +5 -6
- package/skills/wayfinder/agents/openai.yaml +1 -0
- package/skills/wizard/LICENSE +21 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/wizard/agents/openai.yaml +3 -0
- package/skills/wizard/template.sh +223 -0
- package/skills/writing-for-agents/SKILL.md +0 -2
- package/skills/resolving-merge-conflicts/SKILL.md +0 -16
- package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
- /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/house-main.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { request, setGlobalProxyFromEnv } from 'node:http';
|
|
5
|
-
import { extname, join } from 'node:path';
|
|
5
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
6
|
+
import { verified } from './files.js';
|
|
6
7
|
import { HOUSE_REF } from './git.js';
|
|
7
|
-
import { kitHome,
|
|
8
|
+
import { kitHome, readOwnAgent } from './home.js';
|
|
8
9
|
import { callHouse, rpc, UNREACHABLE } from './mcp.js';
|
|
9
10
|
import { refusalOf } from './refusals.js';
|
|
10
|
-
import { relayed } from './relay.js';
|
|
11
|
+
import { relayed, selectRelay } from './relay.js';
|
|
11
12
|
import { parse, stringify } from 'yaml';
|
|
12
13
|
const USAGE = "usage: house <tool> ['<arguments as JSON>']";
|
|
13
14
|
const PUSH_USAGE = 'usage: house push [commit] [--owner]';
|
|
14
15
|
const PUSH = "push [commit]: sends the Local copy's committed changes to House";
|
|
15
16
|
const FETCH_USAGE = 'usage: house fetch';
|
|
16
17
|
const FETCH = `fetch: records House's changes on each Local copy's ${HOUSE_REF}`;
|
|
18
|
+
const DOWNLOAD_USAGE = `usage: house download '{"path":"/rooms/<handle>/<path>","file":"<local file>"}', adding "ref" for a revision`;
|
|
19
|
+
const DOWNLOAD = 'download: writes a binary file of a Room to the local file named by file';
|
|
17
20
|
const ARGUMENTS = `the arguments are one JSON object in single quotes, like house search '{"query":"invoice"}'`;
|
|
18
21
|
const CLOSED = "this conversation's House connection is closed; nothing to do from here";
|
|
19
22
|
const SANDBOXED = "your CLI's sandbox kept house from House; run house directly, not through another program";
|
|
20
23
|
const REVOKED = 'this conversation no longer has House access; nothing to do from here';
|
|
21
24
|
const UNCONNECTED = "this computer's own agent has no House connection; run kit login";
|
|
22
25
|
const DISCONNECTED = "House no longer accepts this computer's own agent; run kit login";
|
|
26
|
+
const UNCONFIRMED = 'with no result; a write may or may not have taken effect, so read before you repeat it';
|
|
23
27
|
const socketPath = process.env.HOUSE_BRIDGE;
|
|
24
28
|
const MEDIA_TYPES = new Map([
|
|
25
29
|
['avif', 'image/avif'],
|
|
@@ -48,49 +52,54 @@ const MEDIA_TYPES = new Map([
|
|
|
48
52
|
['xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
|
49
53
|
['zip', 'application/zip'],
|
|
50
54
|
]);
|
|
51
|
-
function refused(path, status, text) {
|
|
55
|
+
function refused(path, method, status, text, local) {
|
|
52
56
|
const refusal = refusalOf(text);
|
|
53
57
|
if (refusal !== null)
|
|
54
58
|
return refusal.text;
|
|
55
59
|
if (status === 401)
|
|
56
60
|
return REVOKED;
|
|
61
|
+
const act = method === 'GET' ? 'download' : 'upload';
|
|
57
62
|
if (URL.canParse(path))
|
|
58
|
-
return `the
|
|
59
|
-
if (status === 502 &&
|
|
63
|
+
return `the ${act} answered ${status}; ${act} the file again`;
|
|
64
|
+
if (status === 502 && local)
|
|
60
65
|
return text;
|
|
61
|
-
return `${path} answered ${status}`;
|
|
66
|
+
return path === '/' ? unconfirmed(status) : `${path} answered ${status}`;
|
|
62
67
|
}
|
|
63
|
-
function
|
|
68
|
+
function unconfirmed(status) {
|
|
69
|
+
return `House answered ${status} ${UNCONFIRMED}`;
|
|
70
|
+
}
|
|
71
|
+
function exchanged(path, method, body, headers = { 'content-type': 'application/json' }, to = socketPath) {
|
|
64
72
|
return new Promise((resolve, reject) => {
|
|
65
|
-
const sent = request({ socketPath: to, path, method
|
|
66
|
-
|
|
67
|
-
answer.
|
|
68
|
-
answer.on('data', (chunk) => {
|
|
69
|
-
text += chunk;
|
|
70
|
-
});
|
|
73
|
+
const sent = request({ socketPath: to, path, method, headers }, (answer) => {
|
|
74
|
+
const chunks = [];
|
|
75
|
+
answer.on('data', (chunk) => chunks.push(chunk));
|
|
71
76
|
answer.on('end', () => {
|
|
77
|
+
const bytes = Buffer.concat(chunks);
|
|
72
78
|
if (answer.statusCode === 200)
|
|
73
|
-
resolve(
|
|
79
|
+
resolve(bytes);
|
|
74
80
|
else
|
|
75
|
-
reject(new Error(refused(path, answer.statusCode,
|
|
81
|
+
reject(new Error(refused(path, method, answer.statusCode, bytes.toString('utf8'), answer.headers['content-type'] === undefined)));
|
|
76
82
|
});
|
|
77
83
|
});
|
|
78
84
|
sent.on('error', (error) => reject(new Error(error.code === 'EPERM' || error.code === 'EACCES' ? SANDBOXED : CLOSED)));
|
|
79
|
-
sent.end(body instanceof Buffer ? body : JSON.stringify(body));
|
|
85
|
+
sent.end(body === undefined ? undefined : body instanceof Buffer ? body : JSON.stringify(body));
|
|
80
86
|
});
|
|
81
87
|
}
|
|
88
|
+
async function bridged(path, body, headers, to = socketPath) {
|
|
89
|
+
return (await exchanged(path, 'POST', body, headers, to)).toString('utf8');
|
|
90
|
+
}
|
|
82
91
|
async function direct(message) {
|
|
83
92
|
const ownAgent = await readOwnAgent();
|
|
84
93
|
if (ownAgent === null)
|
|
85
94
|
throw new Error(UNCONNECTED);
|
|
86
|
-
|
|
95
|
+
selectRelay(process.env.HOUSE_KIT_RELAY || ownAgent.relay);
|
|
87
96
|
setGlobalProxyFromEnv();
|
|
88
97
|
const answer = await callHouse(ownAgent.house, ownAgent.credential, message).catch(() => {
|
|
89
98
|
throw new Error(UNREACHABLE);
|
|
90
99
|
});
|
|
91
100
|
if (answer.status === 200)
|
|
92
101
|
return answer.text;
|
|
93
|
-
throw new Error(answer.status === 401 ? DISCONNECTED : (refusalOf(answer.text)?.text ??
|
|
102
|
+
throw new Error(answer.status === 401 ? DISCONNECTED : (refusalOf(answer.text)?.text ?? unconfirmed(answer.status)));
|
|
94
103
|
}
|
|
95
104
|
async function mcp(method, params) {
|
|
96
105
|
const message = rpc(method, params);
|
|
@@ -112,36 +121,82 @@ async function localFile(path) {
|
|
|
112
121
|
: `${path} is not a readable file`);
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
|
-
async function
|
|
116
|
-
const headers = { 'content-type': 'application/octet-stream', 'x-house-byte-operation': operation };
|
|
124
|
+
async function transferred(url, method, bytes, headers) {
|
|
117
125
|
if (socketPath !== undefined)
|
|
118
|
-
return
|
|
126
|
+
return exchanged(url, method, bytes, headers);
|
|
119
127
|
setGlobalProxyFromEnv();
|
|
120
|
-
const answer = await fetch(relayed(url), { method:
|
|
128
|
+
const answer = await fetch(relayed(url), { method, ...(headers === undefined ? {} : { headers }), ...(bytes === undefined ? {} : { body: bytes }) }).catch(() => {
|
|
121
129
|
throw new Error(UNREACHABLE);
|
|
122
130
|
});
|
|
123
|
-
const
|
|
131
|
+
const received = Buffer.from(await answer.arrayBuffer());
|
|
124
132
|
if (answer.status === 200)
|
|
125
|
-
return
|
|
126
|
-
throw new Error(refused(url, answer.status,
|
|
133
|
+
return received;
|
|
134
|
+
throw new Error(refused(url, method, answer.status, received.toString('utf8'), false));
|
|
127
135
|
}
|
|
128
|
-
async function
|
|
136
|
+
async function sent(url, bytes, operation) {
|
|
137
|
+
const headers = { 'content-type': 'application/octet-stream', 'x-house-byte-operation': operation };
|
|
138
|
+
return (await transferred(url, 'POST', bytes, headers)).toString('utf8');
|
|
139
|
+
}
|
|
140
|
+
async function download(args) {
|
|
141
|
+
const { flags, positionals: [argument] } = accepted(args, ['--help'], 1, DOWNLOAD_USAGE);
|
|
142
|
+
if (flags.has('--help'))
|
|
143
|
+
return DOWNLOAD_USAGE;
|
|
144
|
+
const { file, ...selected } = argumentsOf(argument);
|
|
129
145
|
if (typeof file !== 'string' || file === '')
|
|
130
|
-
throw new Error('
|
|
146
|
+
throw new Error('download needs "file", the local file to write');
|
|
147
|
+
const inspected = parse(await called('inspect', selected));
|
|
148
|
+
if (typeof inspected?.download !== 'string')
|
|
149
|
+
throw new Error(`${String(selected.path)} holds no binary file; shell reads its text`);
|
|
150
|
+
const bytes = verified(await transferred(inspected.download, 'GET'), String(inspected.sha256), String(selected.path));
|
|
151
|
+
const written = resolve(file);
|
|
152
|
+
await mkdir(dirname(written), { recursive: true });
|
|
153
|
+
await writeFile(written, bytes);
|
|
154
|
+
return written;
|
|
155
|
+
}
|
|
156
|
+
async function described(file) {
|
|
131
157
|
const bytes = await localFile(file);
|
|
132
158
|
const mediaType = MEDIA_TYPES.get(extname(file).slice(1).toLowerCase());
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
159
|
+
return {
|
|
160
|
+
bytes,
|
|
161
|
+
description: {
|
|
162
|
+
bytes: bytes.length,
|
|
163
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
164
|
+
...(mediaType === undefined ? {} : { media_type: mediaType }),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async function upload({ file, ...destination }) {
|
|
169
|
+
if (typeof file !== 'string' || file === '')
|
|
170
|
+
throw new Error('upload needs "file", a local file');
|
|
171
|
+
const { bytes, description } = await described(file);
|
|
172
|
+
const answer = await called('upload', { ...destination, ...description });
|
|
139
173
|
const declared = parse(answer);
|
|
140
174
|
if (declared.upload === undefined)
|
|
141
175
|
return answer;
|
|
142
176
|
const written = JSON.parse(await sent(declared.upload.url, bytes, declared.upload.operation));
|
|
143
177
|
return stringify(written, { lineWidth: 0 }).trimEnd();
|
|
144
178
|
}
|
|
179
|
+
async function appendRecord(args) {
|
|
180
|
+
const held = new Map();
|
|
181
|
+
const attachments = Array.isArray(args.attachments)
|
|
182
|
+
? await Promise.all(args.attachments.map(async (attachment) => {
|
|
183
|
+
if (typeof attachment !== 'string')
|
|
184
|
+
return attachment;
|
|
185
|
+
const { bytes, description } = await described(attachment);
|
|
186
|
+
held.set(description.sha256, bytes);
|
|
187
|
+
return { name: basename(attachment), ...description };
|
|
188
|
+
}))
|
|
189
|
+
: args.attachments;
|
|
190
|
+
const record = { ...args, attachments };
|
|
191
|
+
for (;;) {
|
|
192
|
+
const answer = await called('append_record', record);
|
|
193
|
+
const { upload } = parse(answer);
|
|
194
|
+
if (upload === undefined)
|
|
195
|
+
return answer;
|
|
196
|
+
for (const grant of upload)
|
|
197
|
+
await sent(grant.url, held.get(grant.sha256), grant.operation);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
145
200
|
function unexpected(argument, usage) {
|
|
146
201
|
return new Error(`unexpected argument ${argument}; ${usage}`);
|
|
147
202
|
}
|
|
@@ -200,7 +255,7 @@ async function house(args) {
|
|
|
200
255
|
if (flags.has('--help') && argument !== undefined)
|
|
201
256
|
throw unexpected('--help', USAGE);
|
|
202
257
|
if (verb === undefined || verb === 'help') {
|
|
203
|
-
return [USAGE, 'help', PUSH, FETCH, ...(await tools()).map((tool) => (tool.description === undefined ? tool.name : `${tool.name}: ${tool.description}`))].join('\n');
|
|
258
|
+
return [USAGE, 'help', PUSH, FETCH, DOWNLOAD, ...(await tools()).map((tool) => (tool.description === undefined ? tool.name : `${tool.name}: ${tool.description}`))].join('\n');
|
|
204
259
|
}
|
|
205
260
|
if (flags.has('--help')) {
|
|
206
261
|
const tool = (await tools()).find((listed) => listed.name === verb);
|
|
@@ -209,7 +264,7 @@ async function house(args) {
|
|
|
209
264
|
return [...(tool.description === undefined ? [] : [tool.description]), JSON.stringify(tool.inputSchema)].join('\n');
|
|
210
265
|
}
|
|
211
266
|
const parsed = argumentsOf(argument);
|
|
212
|
-
return verb === 'upload' ? upload(parsed) : called(verb, parsed);
|
|
267
|
+
return verb === 'upload' ? upload(parsed) : verb === 'append_record' ? appendRecord(parsed) : called(verb, parsed);
|
|
213
268
|
}
|
|
214
269
|
async function called(name, args) {
|
|
215
270
|
const result = await mcp('tools/call', { name, arguments: args });
|
|
@@ -221,7 +276,13 @@ async function called(name, args) {
|
|
|
221
276
|
const argv = process.argv.slice(2);
|
|
222
277
|
const named = argv.findIndex((arg) => !arg.startsWith('-'));
|
|
223
278
|
const rest = argv.filter((_arg, index) => index !== named);
|
|
224
|
-
(argv[named] === 'push'
|
|
279
|
+
(argv[named] === 'push'
|
|
280
|
+
? push(rest)
|
|
281
|
+
: argv[named] === 'fetch'
|
|
282
|
+
? fetched(rest)
|
|
283
|
+
: argv[named] === 'download'
|
|
284
|
+
? download(rest)
|
|
285
|
+
: house(argv)).then((text) => {
|
|
225
286
|
process.stdout.write(`${text}\n`);
|
|
226
287
|
if (argv[0] === 'shell')
|
|
227
288
|
process.exitCode = Number(/^exit: (\d+)(?:\n|$)/.exec(text)?.[1] ?? 0);
|
package/dist/instructions.js
CHANGED
|
@@ -1,36 +1,11 @@
|
|
|
1
1
|
import { HOUSE_REF } from './git.js';
|
|
2
|
-
import {
|
|
2
|
+
import { readHowWeWork } from './skills.js';
|
|
3
3
|
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
7
|
const COPIES_LINE = `House Local copies; before work run \`house fetch\` and rebase onto \`${HOUSE_REF}\`; after it commit, then \`house push\`:`;
|
|
8
|
-
const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
|
|
9
|
-
const FAILED = /^exit: [1-9]\d*$/;
|
|
10
|
-
const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
|
|
11
|
-
function cutOf(lines) {
|
|
12
|
-
const cut = CUT.exec(lines[0] ?? '');
|
|
13
|
-
return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
|
|
14
|
-
}
|
|
15
|
-
async function howWeWork(bridge) {
|
|
16
|
-
let document = '';
|
|
17
|
-
for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
|
|
18
|
-
const read = await bridge.tool('shell', { command });
|
|
19
|
-
const text = read.content.map((content) => content.text).join('\n');
|
|
20
|
-
const lines = text.split('\n');
|
|
21
|
-
if (read.isError === true || (FAILED.test(lines[0] ?? '') && lines[1]?.startsWith('stderr: '))) {
|
|
22
|
-
const refusal = read.isError === true ? text : lines.slice(1).join('\n').replace(/^stderr: /gm, '');
|
|
23
|
-
if (UNREADABLE.test(refusal))
|
|
24
|
-
return '';
|
|
25
|
-
throw new Error(refusal);
|
|
26
|
-
}
|
|
27
|
-
const cut = cutOf(lines);
|
|
28
|
-
document += lines.slice(cut === null ? 0 : 1).join('\n');
|
|
29
|
-
command = cut?.[2];
|
|
30
|
-
}
|
|
31
|
-
return document.replace(/\n$/, '');
|
|
32
|
-
}
|
|
33
8
|
export async function instructions(bridge, base, copies) {
|
|
34
9
|
const lines = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
|
|
35
|
-
return [lines.join('\n'), base, await
|
|
10
|
+
return [lines.join('\n'), base, (await readHowWeWork(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
|
|
36
11
|
}
|
package/dist/kit-main.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import { setGlobalProxyFromEnv } from 'node:http';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import { answerGitCredential } from './app-git.js';
|
|
5
|
+
import { chatgpt } from './chatgpt.js';
|
|
5
6
|
import { login } from './login.js';
|
|
6
7
|
import { logout } from './logout.js';
|
|
7
8
|
import { resident } from './resident.js';
|
|
8
|
-
const USAGE = 'usage: kit <login|logout|resident>\n';
|
|
9
|
+
const USAGE = 'usage: kit <login|logout|resident|chatgpt>\n';
|
|
9
10
|
const COMMANDS = new Map([
|
|
10
11
|
[
|
|
11
12
|
'logout',
|
|
@@ -40,6 +41,8 @@ setGlobalProxyFromEnv();
|
|
|
40
41
|
async function run() {
|
|
41
42
|
if (command === 'login')
|
|
42
43
|
return login(argv);
|
|
44
|
+
if (command === 'chatgpt')
|
|
45
|
+
return chatgpt(argv);
|
|
43
46
|
if (command === '--help') {
|
|
44
47
|
parseArgs({ args: argv });
|
|
45
48
|
process.stdout.write(USAGE);
|
package/dist/login.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createHash, randomBytes } from 'node:crypto';
|
|
3
3
|
import { hostname } from 'node:os';
|
|
4
|
-
import { delimiter, dirname } from 'node:path';
|
|
5
4
|
import { parseArgs } from 'node:util';
|
|
6
|
-
import { post, retrying } from './api.js';
|
|
5
|
+
import { HouseRefusal, post, retrying } from './api.js';
|
|
7
6
|
import { placeAppGit } from './app-git.js';
|
|
8
|
-
import {
|
|
9
|
-
import { forgetOwnAgent, readDisconnected, readEnrolment, readOwnAgent, writeEnrolment, writeOwnAgent } from './home.js';
|
|
7
|
+
import { writeCodexRule } from './clis.js';
|
|
8
|
+
import { forgetDisconnected, forgetOwnAgent, readDisconnected, readEnrolment, readOwnAgent, writeEnrolment, writeOwnAgent, } from './home.js';
|
|
10
9
|
import { callHouse, rpc } from './mcp.js';
|
|
10
|
+
import { refusalOf } from './refusals.js';
|
|
11
11
|
import { selectRelay } from './relay.js';
|
|
12
12
|
import { startService } from './service.js';
|
|
13
13
|
import { placeSkillSet } from './skills.js';
|
|
14
|
-
const DEFAULT_HOUSE = 'https://agents.house';
|
|
14
|
+
export const DEFAULT_HOUSE = 'https://agents.house';
|
|
15
15
|
const OPENER = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
16
16
|
// Polling every two seconds brings the User's confirmation to the waiting Kit moments after they give it.
|
|
17
17
|
const POLL_INTERVAL_MS = 2000;
|
|
@@ -20,7 +20,7 @@ function open(link) {
|
|
|
20
20
|
.on('error', () => undefined)
|
|
21
21
|
.unref();
|
|
22
22
|
}
|
|
23
|
-
async function accepted(house, credential) {
|
|
23
|
+
export async function accepted(house, credential) {
|
|
24
24
|
const checked = await callHouse(house, credential, rpc('tools/list', {}));
|
|
25
25
|
if (checked.status === 200)
|
|
26
26
|
return true;
|
|
@@ -36,6 +36,9 @@ const USAGE = `usage: kit login [--house <origin>] [--environment <id>] [--rooms
|
|
|
36
36
|
--no-private give this Environment every Room but your Private Room
|
|
37
37
|
--code <code> connect this machine with a login code
|
|
38
38
|
`;
|
|
39
|
+
function gone(error) {
|
|
40
|
+
return error instanceof HouseRefusal && refusalOf(error.text)?.code === 'kit_environment_not_found';
|
|
41
|
+
}
|
|
39
42
|
function chosenRooms(rooms, noPrivate) {
|
|
40
43
|
if (rooms !== undefined && noPrivate)
|
|
41
44
|
throw new Error('--rooms and --no-private are two choices of Rooms; pass one');
|
|
@@ -74,7 +77,8 @@ export async function login(argv) {
|
|
|
74
77
|
selectRelay(relay);
|
|
75
78
|
const house = values.house ?? enrolled?.house ?? DEFAULT_HOUSE;
|
|
76
79
|
const replaces = process.env.HOUSE_KIT_REPLACES;
|
|
77
|
-
const
|
|
80
|
+
const remembered = replaces === undefined && values.environment === undefined && enrolled?.house === house ? enrolled.environment : undefined;
|
|
81
|
+
const environment = replaces === undefined ? (values.environment ?? remembered) : undefined;
|
|
78
82
|
const ownAgent = await readOwnAgent();
|
|
79
83
|
let held = false;
|
|
80
84
|
let exchanged;
|
|
@@ -84,47 +88,51 @@ export async function login(argv) {
|
|
|
84
88
|
else {
|
|
85
89
|
held = ownAgent !== null && ownAgent.house === house && (await accepted(house, ownAgent.credential));
|
|
86
90
|
const verifier = randomBytes(32).toString('base64url');
|
|
87
|
-
const
|
|
91
|
+
const start = (named) => retrying(() => post(house, '/kit', {
|
|
88
92
|
label: hostname(),
|
|
89
|
-
...(
|
|
93
|
+
...(named === undefined
|
|
90
94
|
? { act: 'new_environment', ...(replaces === undefined ? {} : { replaces }) }
|
|
91
|
-
: { act: 'existing_environment', environment_id:
|
|
95
|
+
: { act: 'existing_environment', environment_id: named }),
|
|
92
96
|
...rooms,
|
|
93
97
|
code_challenge: createHash('sha256').update(verifier).digest('base64url'),
|
|
94
98
|
...(held ? {} : { headless: true }),
|
|
95
99
|
}));
|
|
100
|
+
let started;
|
|
101
|
+
try {
|
|
102
|
+
started = await start(environment);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (remembered === undefined || !gone(error))
|
|
106
|
+
throw error;
|
|
107
|
+
await forgetDisconnected();
|
|
108
|
+
process.stdout.write(`House no longer holds Environment ${remembered}; connecting this computer as a new one.\n`);
|
|
109
|
+
started = await start(undefined);
|
|
110
|
+
}
|
|
96
111
|
process.stdout.write(`Open this link and confirm: ${started.start_url}\n`);
|
|
97
112
|
open(started.start_url);
|
|
98
113
|
exchanged = await retrying(() => post(house, '/kit/token', { login: started.login, code_verifier: verifier }), undefined, () => POLL_INTERVAL_MS);
|
|
99
114
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
process.stderr.write(`kit: the skills could not be placed: ${error.message}\n`);
|
|
105
|
-
}
|
|
106
|
-
try {
|
|
107
|
-
const shell = await loginPath().catch(() => '');
|
|
108
|
-
await allowHouseInCodex([dirname(process.execPath), process.env.PATH ?? '', shell].join(delimiter));
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
process.stderr.write(`kit: Codex's rule for house could not be written: ${error.message}\n`);
|
|
112
|
-
}
|
|
115
|
+
await placeSkillSet();
|
|
116
|
+
await writeCodexRule().catch((error) => process.stderr.write(`kit: ${error.message}\n`));
|
|
117
|
+
const relayRecord = relay === undefined ? {} : { relay };
|
|
113
118
|
await writeEnrolment({
|
|
114
119
|
house,
|
|
115
|
-
...
|
|
120
|
+
...relayRecord,
|
|
116
121
|
environment: exchanged.environment,
|
|
117
122
|
credential: exchanged.credential,
|
|
118
123
|
});
|
|
119
124
|
process.stdout.write(`Environment ${exchanged.environment} is connected to ${house}.\n`);
|
|
120
125
|
if (exchanged.headless_credential !== undefined) {
|
|
121
|
-
await writeOwnAgent({ house, user: exchanged.user, credential: exchanged.headless_credential });
|
|
126
|
+
await writeOwnAgent({ house, ...relayRecord, user: exchanged.user, credential: exchanged.headless_credential });
|
|
122
127
|
}
|
|
123
128
|
else if (held && ownAgent !== null && ownAgent.user !== exchanged.user) {
|
|
124
129
|
await forgetOwnAgent();
|
|
125
130
|
process.stdout.write("This computer's own agent was another User's; run kit login again to connect it to you.\n");
|
|
126
131
|
}
|
|
127
|
-
|
|
132
|
+
else if (held && ownAgent !== null) {
|
|
133
|
+
await writeOwnAgent({ house, ...relayRecord, user: ownAgent.user, credential: ownAgent.credential });
|
|
134
|
+
}
|
|
135
|
+
await placeAppGit(ownAgent, await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
|
|
128
136
|
try {
|
|
129
137
|
startService();
|
|
130
138
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { run } from './clis.js';
|
|
4
|
+
import { kitHome } from './home.js';
|
|
5
|
+
import { relayOrigin } from './relay.js';
|
|
6
|
+
export function service() {
|
|
7
|
+
if (process.platform !== 'linux' || !process.env.INVOCATION_ID)
|
|
8
|
+
return null;
|
|
9
|
+
return process.env.MANAGERPID ? 'user' : 'system';
|
|
10
|
+
}
|
|
11
|
+
export function throughSudo() {
|
|
12
|
+
return service() === 'system' && process.getuid() !== 0;
|
|
13
|
+
}
|
|
14
|
+
export async function updatesItself() {
|
|
15
|
+
if (service() === null)
|
|
16
|
+
return false;
|
|
17
|
+
if (!throughSudo())
|
|
18
|
+
return true;
|
|
19
|
+
// sudo answers at once, so ten seconds only bounds a stuck sudo.
|
|
20
|
+
return (await run('sudo', ['-n', 'true'], 10_000)).status === 0;
|
|
21
|
+
}
|
|
22
|
+
function installed() {
|
|
23
|
+
try {
|
|
24
|
+
return readFileSync(join(kitHome(), 'placement'), 'utf8').trim();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT')
|
|
28
|
+
return null;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function entries(house) {
|
|
33
|
+
const host = new URL(house).host;
|
|
34
|
+
const relay = relayOrigin();
|
|
35
|
+
if (relay === undefined)
|
|
36
|
+
return [`curl -fsSL ${host}/kit | sh`, `irm ${host}/kit.ps1 | iex`];
|
|
37
|
+
const entry = `${relay}/${host}/kit`;
|
|
38
|
+
return [`curl -fsSL ${entry} | HOUSE_KIT_RELAY=${relay} sh`, `$env:HOUSE_KIT_RELAY='${relay}'; irm ${entry}.ps1 | iex`];
|
|
39
|
+
}
|
|
40
|
+
export function installCommand(house) {
|
|
41
|
+
const [shell, windows] = entries(house);
|
|
42
|
+
const placement = installed();
|
|
43
|
+
if (placement === 'native')
|
|
44
|
+
return shell;
|
|
45
|
+
if (placement === 'server')
|
|
46
|
+
return `${shell} -s -- --linux`;
|
|
47
|
+
return `${shell} -s -- --container, or ${windows} on Windows`;
|
|
48
|
+
}
|
package/dist/relay.js
CHANGED
|
@@ -2,7 +2,7 @@ let selected;
|
|
|
2
2
|
export function selectRelay(relay) {
|
|
3
3
|
selected = relay === undefined || relay === '' ? null : relay;
|
|
4
4
|
}
|
|
5
|
-
function relayOrigin() {
|
|
5
|
+
export function relayOrigin() {
|
|
6
6
|
if (selected !== undefined)
|
|
7
7
|
return selected ?? undefined;
|
|
8
8
|
const ambient = process.env.HOUSE_KIT_RELAY;
|
package/dist/resident.js
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { Agents } from './agents.js';
|
|
8
8
|
import { house } from './api.js';
|
|
9
9
|
import { received } from './bridge.js';
|
|
10
|
-
import { Conversations } from './conversations.js';
|
|
10
|
+
import { CONVERSATION_INPUTS, Conversations } from './conversations.js';
|
|
11
11
|
import { LocalCopies } from './copies.js';
|
|
12
12
|
import { restart } from './end.js';
|
|
13
13
|
import { agentBase, enrolled, forgetRestart, kitHome, restartAsked } from './home.js';
|
|
@@ -84,7 +84,7 @@ export async function resident() {
|
|
|
84
84
|
await placeSkillSet();
|
|
85
85
|
await mkdir(agentBase(), { recursive: true });
|
|
86
86
|
process.env.HOUSE_KIT_RESIDENT = String(process.pid);
|
|
87
|
-
const agents = new Agents(house);
|
|
87
|
+
const agents = new Agents(house, () => changed());
|
|
88
88
|
const copies = new LocalCopies(house);
|
|
89
89
|
await copies.load();
|
|
90
90
|
await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
|
|
@@ -92,7 +92,7 @@ export async function resident() {
|
|
|
92
92
|
await serveOwner(copies);
|
|
93
93
|
let stream = null;
|
|
94
94
|
let reported = false;
|
|
95
|
-
const idle = () => conversations.idle() && signIns.idle();
|
|
95
|
+
const idle = () => conversations.idle() && signIns.idle() && agents.idle();
|
|
96
96
|
const changed = () => {
|
|
97
97
|
if (!idle()) {
|
|
98
98
|
reported = false;
|
|
@@ -140,8 +140,12 @@ export async function resident() {
|
|
|
140
140
|
signIns.start(arrived);
|
|
141
141
|
else if (arrived.kind === 'update')
|
|
142
142
|
updates.start(arrived);
|
|
143
|
-
else
|
|
143
|
+
else if (CONVERSATION_INPUTS.has(arrived.kind))
|
|
144
144
|
conversations.input(arrived);
|
|
145
|
+
else {
|
|
146
|
+
const refused = { refused: `This Kit does not know ${arrived.kind}; update the Kit` };
|
|
147
|
+
logged(house.deliver(`/kit/inputs/${arrived.input_id}/ack`, refused).finally(changed));
|
|
148
|
+
}
|
|
145
149
|
}
|
|
146
150
|
},
|
|
147
151
|
});
|
package/dist/skills.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { cp, lstat, mkdir, readdir, realpath, rm, symlink
|
|
3
|
+
import { cp, lstat, mkdir, readdir, realpath, rm, symlink } from 'node:fs/promises';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { join, relative } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { parse } from 'yaml';
|
|
6
8
|
import { KIT_VERSION } from './clis.js';
|
|
7
9
|
import { kitHome, readConfiguration, readHome, writeHome } from './home.js';
|
|
8
10
|
import { refusalOf } from './refusals.js';
|
|
9
|
-
|
|
11
|
+
const HOW_WE_WORK = '/rooms/private/how-we-work.md';
|
|
10
12
|
const SKILL_SET = fileURLToPath(new URL('../skills/', import.meta.url));
|
|
11
13
|
const HOW_WE_WORK_TEXT = readFileSync(new URL('../how-we-work.md', import.meta.url), 'utf8');
|
|
14
|
+
const SHIPPED_HOW_WE_WORK = [
|
|
15
|
+
'ae4dda60da7b174f22dc5fa9bf07fc27c25864932a74230de4f9f509ddd91f41',
|
|
16
|
+
'0bdcd43e1603ba6673a3cae56d70bbf90657cbeefd124e91a47be2ca1cbd6c93',
|
|
17
|
+
'4106a87ec66da6f7b2a10e74d18e6559d68d5f4fe7415dae68f5c7bdb9081968',
|
|
18
|
+
'ac814a2197038f037b657bb719340395f89f319ac9fcdd386629c3fe2cec68f1',
|
|
19
|
+
'29951ee394aad9979eecfed3e5dc32c2c86e2226b6a380db5dbd4f305f4d1156',
|
|
20
|
+
];
|
|
12
21
|
const RECORD = 'skill-set.json';
|
|
13
|
-
const
|
|
22
|
+
const WRITTEN = 'how-we-work.json';
|
|
14
23
|
const UNWRITABLE = new Set(['room_not_found', 'operation_denied']);
|
|
24
|
+
const PRESENT = new Set(['path_exists']);
|
|
25
|
+
const CHANGED = new Set(['base_revision_stale', 'path_absent']);
|
|
26
|
+
const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
|
|
27
|
+
const FAILED = /^exit: [1-9]\d*$/;
|
|
28
|
+
const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
|
|
15
29
|
function skillsDirectory() {
|
|
16
30
|
return join(homedir(), '.agents', 'skills');
|
|
17
31
|
}
|
|
@@ -49,6 +63,14 @@ async function install() {
|
|
|
49
63
|
await writeHome(RECORD, { version: KIT_VERSION, names });
|
|
50
64
|
}
|
|
51
65
|
export async function placeSkillSet() {
|
|
66
|
+
try {
|
|
67
|
+
await replaceSkillSet();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
process.stderr.write(`kit: the skills could not be placed: ${error.message}\n`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function replaceSkillSet() {
|
|
52
74
|
const accepted = (await readConfiguration()).skills;
|
|
53
75
|
const installed = await readHome(RECORD);
|
|
54
76
|
if (accepted && installed?.version === KIT_VERSION)
|
|
@@ -62,17 +84,64 @@ export async function placeSkillSet() {
|
|
|
62
84
|
else
|
|
63
85
|
await rm(join(kitHome(), RECORD), { force: true });
|
|
64
86
|
}
|
|
87
|
+
function sha256(text) {
|
|
88
|
+
return createHash('sha256').update(text).digest('hex');
|
|
89
|
+
}
|
|
90
|
+
function cutOf(lines) {
|
|
91
|
+
const cut = CUT.exec(lines[0] ?? '');
|
|
92
|
+
return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
|
|
93
|
+
}
|
|
94
|
+
export async function readHowWeWork(bridge) {
|
|
95
|
+
let document = '';
|
|
96
|
+
for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
|
|
97
|
+
const read = await bridge.tool('shell', { command });
|
|
98
|
+
const text = read.content.map((content) => content.text).join('\n');
|
|
99
|
+
const lines = text.split('\n');
|
|
100
|
+
if (read.isError === true || (FAILED.test(lines[0] ?? '') && lines[1]?.startsWith('stderr: '))) {
|
|
101
|
+
const refusal = read.isError === true ? text : lines.slice(1).join('\n').replace(/^stderr: /gm, '');
|
|
102
|
+
if (UNREADABLE.test(refusal))
|
|
103
|
+
return '';
|
|
104
|
+
throw new Error(refusal);
|
|
105
|
+
}
|
|
106
|
+
const cut = cutOf(lines);
|
|
107
|
+
document += lines.slice(cut === null ? 0 : 1).join('\n');
|
|
108
|
+
command = cut?.[2];
|
|
109
|
+
}
|
|
110
|
+
return document;
|
|
111
|
+
}
|
|
112
|
+
async function currentRevision(bridge) {
|
|
113
|
+
const inspected = await bridge.tool('inspect', { path: HOW_WE_WORK });
|
|
114
|
+
const text = inspected.content.map((content) => content.text).join('\n');
|
|
115
|
+
if (inspected.isError === true)
|
|
116
|
+
throw new Error(text);
|
|
117
|
+
return parse(text).revisions[0].revision;
|
|
118
|
+
}
|
|
119
|
+
async function edit(bridge, change, kept) {
|
|
120
|
+
const edited = await bridge.tool('edit', { changes: [{ ...change, path: HOW_WE_WORK, content: HOW_WE_WORK_TEXT }] });
|
|
121
|
+
if (edited.isError !== true)
|
|
122
|
+
return 'written';
|
|
123
|
+
const text = edited.content.map((content) => content.text).join('\n');
|
|
124
|
+
const refusal = refusalOf(text);
|
|
125
|
+
if (UNWRITABLE.has(refusal?.code ?? ''))
|
|
126
|
+
return 'unwritable';
|
|
127
|
+
if (refusal?.code === 'edit_conflict' && refusal.conflicts.some((conflict) => kept.has(conflict)))
|
|
128
|
+
return 'kept';
|
|
129
|
+
throw new Error(text);
|
|
130
|
+
}
|
|
65
131
|
export async function createHowWeWork(bridge) {
|
|
66
|
-
if ((await
|
|
132
|
+
if (!(await readConfiguration()).skills || (await readHome(RECORD))?.version === undefined)
|
|
133
|
+
return;
|
|
134
|
+
const current = sha256(HOW_WE_WORK_TEXT);
|
|
135
|
+
if ((await readHome(WRITTEN)) === current)
|
|
136
|
+
return;
|
|
137
|
+
const created = await edit(bridge, { op: 'create' }, PRESENT);
|
|
138
|
+
if (created === 'unwritable')
|
|
67
139
|
return;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
if (UNWRITABLE.has(refusal?.code ?? ''))
|
|
140
|
+
if (created === 'kept') {
|
|
141
|
+
const base = await currentRevision(bridge);
|
|
142
|
+
const held = sha256(await readHowWeWork(bridge));
|
|
143
|
+
if (held !== current && SHIPPED_HOW_WE_WORK.includes(held) && (await edit(bridge, { op: 'replace', base }, CHANGED)) === 'unwritable')
|
|
73
144
|
return;
|
|
74
|
-
if (refusal?.code !== 'edit_conflict' || !refusal.conflicts.includes('path_exists'))
|
|
75
|
-
throw new Error(text);
|
|
76
145
|
}
|
|
77
|
-
await
|
|
146
|
+
await writeHome(WRITTEN, current);
|
|
78
147
|
}
|