@agentshouse/kit 0.2.1-c96543a4c8b15 → 0.2.1-ca18b01951c3b

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.
Files changed (82) hide show
  1. package/README.md +11 -10
  2. package/dist/agents.js +58 -4
  3. package/dist/api.js +5 -1
  4. package/dist/bootstrap.json +4 -4
  5. package/dist/chatgpt.js +29 -0
  6. package/dist/clis.js +10 -3
  7. package/dist/conversations.js +11 -1
  8. package/dist/copies.js +18 -8
  9. package/dist/end.js +4 -0
  10. package/dist/files.js +7 -1
  11. package/dist/git.js +11 -8
  12. package/dist/home.js +41 -4
  13. package/dist/house-main.js +101 -40
  14. package/dist/instructions.js +2 -27
  15. package/dist/kit-main.js +4 -1
  16. package/dist/login.js +34 -26
  17. package/dist/placement.js +48 -0
  18. package/dist/relay.js +1 -1
  19. package/dist/resident.js +8 -4
  20. package/dist/skills.js +81 -12
  21. package/dist/stream.js +62 -13
  22. package/dist/transcription.js +3 -3
  23. package/dist/update.js +28 -6
  24. package/dist/wire.js +6 -0
  25. package/how-we-work.md +13 -3
  26. package/package.json +2 -1
  27. package/skills/ask-matt/SKILL.md +17 -11
  28. package/skills/ask-matt/agents/openai.yaml +1 -0
  29. package/skills/code-review/SKILL.md +5 -5
  30. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  31. package/skills/codebase-design/SKILL.md +0 -2
  32. package/skills/diagnosing-bugs/SKILL.md +2 -4
  33. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  34. package/skills/domain-modeling/SKILL.md +12 -14
  35. package/skills/grill-me/SKILL.md +1 -2
  36. package/skills/grill-me/agents/openai.yaml +1 -0
  37. package/skills/grill-with-docs/SKILL.md +1 -2
  38. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  39. package/skills/grilling/SKILL.md +2 -2
  40. package/skills/handoff/SKILL.md +2 -3
  41. package/skills/handoff/agents/openai.yaml +1 -0
  42. package/skills/implement/SKILL.md +5 -4
  43. package/skills/implement/agents/openai.yaml +1 -0
  44. package/skills/implement-spec/SKILL.md +40 -0
  45. package/skills/implement-spec/agents/openai.yaml +5 -0
  46. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  47. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  48. package/skills/pr/CREDITS.md +3 -0
  49. package/skills/pr/LICENSE +21 -0
  50. package/skills/pr/SKILL.md +170 -0
  51. package/skills/pr/agents/openai.yaml +3 -0
  52. package/skills/prototype/SKILL.md +0 -2
  53. package/skills/research/SKILL.md +0 -2
  54. package/skills/retro/LICENSE +21 -0
  55. package/skills/retro/SKILL.md +44 -0
  56. package/skills/retro/agents/openai.yaml +5 -0
  57. package/skills/tdd/SKILL.md +2 -4
  58. package/skills/teach/SKILL.md +3 -4
  59. package/skills/teach/agents/openai.yaml +1 -0
  60. package/skills/to-questionnaire/SKILL.md +1 -2
  61. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  62. package/skills/to-spec/SKILL.md +1 -2
  63. package/skills/to-spec/agents/openai.yaml +1 -0
  64. package/skills/to-tickets/SKILL.md +3 -4
  65. package/skills/to-tickets/agents/openai.yaml +1 -0
  66. package/skills/triage/AGENT-BRIEF.md +207 -0
  67. package/skills/triage/LICENSE +21 -0
  68. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  69. package/skills/triage/SKILL.md +112 -0
  70. package/skills/triage/agents/openai.yaml +5 -0
  71. package/skills/wait-what/SKILL.md +2 -3
  72. package/skills/wait-what/agents/openai.yaml +1 -0
  73. package/skills/wayfinder/SKILL.md +5 -6
  74. package/skills/wayfinder/agents/openai.yaml +1 -0
  75. package/skills/wizard/LICENSE +21 -0
  76. package/skills/wizard/SKILL.md +44 -0
  77. package/skills/wizard/agents/openai.yaml +3 -0
  78. package/skills/wizard/template.sh +223 -0
  79. package/skills/writing-for-agents/SKILL.md +0 -2
  80. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  81. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  82. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
@@ -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, readEnrolment, readOwnAgent } from './home.js';
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
- const UNCONNECTED = "this computer's own agent has no House connection; run kit login";
22
- const DISCONNECTED = "House no longer accepts this computer's own agent; run kit login";
24
+ const UNCONNECTED = "this computer's own agent has no House connection; run kit login, or kit chatgpt --key <key> with a script key";
25
+ const DISCONNECTED = "House no longer accepts this computer's own agent; run kit login, or kit chatgpt --key <key> with a script key";
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 upload answered ${status}; upload the file again`;
59
- if (status === 502 && /^[A-Za-z]/.test(text))
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 bridged(path, body, headers = { 'content-type': 'application/json' }, to = socketPath) {
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: 'POST', headers }, (answer) => {
66
- let text = '';
67
- answer.setEncoding('utf8');
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(text);
79
+ resolve(bytes);
74
80
  else
75
- reject(new Error(refused(path, answer.statusCode, text)));
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
- await readEnrolment();
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 ?? `House answered ${answer.status}`));
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 sent(url, bytes, operation) {
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 bridged(url, bytes, headers);
126
+ return exchanged(url, method, bytes, headers);
119
127
  setGlobalProxyFromEnv();
120
- const answer = await fetch(relayed(url), { method: 'POST', headers, body: bytes }).catch(() => {
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 text = await answer.text();
131
+ const received = Buffer.from(await answer.arrayBuffer());
124
132
  if (answer.status === 200)
125
- return text;
126
- throw new Error(refused(url, answer.status, text));
133
+ return received;
134
+ throw new Error(refused(url, method, answer.status, received.toString('utf8'), false));
127
135
  }
128
- async function upload({ file, ...destination }) {
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('upload needs "file", a local file');
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
- const answer = await called('upload', {
134
- ...destination,
135
- bytes: bytes.length,
136
- sha256: createHash('sha256').update(bytes).digest('hex'),
137
- ...(mediaType === undefined ? {} : { media_type: mediaType }),
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' ? push(rest) : argv[named] === 'fetch' ? fetched(rest) : house(argv)).then((text) => {
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);
@@ -1,36 +1,11 @@
1
1
  import { HOUSE_REF } from './git.js';
2
- import { HOW_WE_WORK } from './skills.js';
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 howWeWork(bridge)].filter((part) => part !== '').join('\n\n');
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 { allowHouseInCodex, loginPath } from './clis.js';
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 environment = replaces === undefined ? (values.environment ?? (enrolled?.house === house ? enrolled.environment : undefined)) : undefined;
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 started = await retrying(() => post(house, '/kit', {
91
+ const start = (named) => retrying(() => post(house, '/kit', {
88
92
  label: hostname(),
89
- ...(environment === undefined
93
+ ...(named === undefined
90
94
  ? { act: 'new_environment', ...(replaces === undefined ? {} : { replaces }) }
91
- : { act: 'existing_environment', environment_id: environment }),
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
- try {
101
- await placeSkillSet();
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
- ...(relay === undefined ? {} : { relay }),
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
- await placeAppGit(ownAgent, await readOwnAgent());
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, writeFile } from 'node:fs/promises';
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
- export const HOW_WE_WORK = '/rooms/private/how-we-work.md';
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 CREATED = 'how-we-work-created';
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 readHome(RECORD)) === null || (await present(join(kitHome(), CREATED))))
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
- const created = await bridge.tool('edit', { changes: [{ op: 'create', path: HOW_WE_WORK, content: HOW_WE_WORK_TEXT }] });
69
- if (created.isError === true) {
70
- const text = created.content.map((content) => content.text).join('\n');
71
- const refusal = refusalOf(text);
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 writeFile(join(kitHome(), CREATED), '');
146
+ await writeHome(WRITTEN, current);
78
147
  }