@agentshouse/kit 0.2.1-ce3d2bb67177c → 0.2.1-ceeaf32286734

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 (131) hide show
  1. package/README.md +6 -6
  2. package/dist/acp.js +57 -6
  3. package/dist/agents.js +6 -9
  4. package/dist/authority-main.js +2 -2
  5. package/dist/bridge.js +1 -1
  6. package/dist/building.js +46 -0
  7. package/dist/calls.js +11 -3
  8. package/dist/clis-main.js +2 -2
  9. package/dist/clis.js +39 -15
  10. package/dist/connection.js +20 -29
  11. package/dist/conversations.js +149 -47
  12. package/dist/end.js +8 -2
  13. package/dist/events.js +29 -8
  14. package/dist/files.js +33 -17
  15. package/dist/git.js +2 -2
  16. package/dist/house-cli.js +27 -55
  17. package/dist/house-main.js +3 -3
  18. package/dist/instructions.js +29 -9
  19. package/dist/kit-cli.js +2 -2
  20. package/dist/kit-main.js +3 -3
  21. package/dist/login.js +3 -1
  22. package/dist/mcp.js +2 -0
  23. package/dist/relay.js +21 -0
  24. package/dist/resident.js +17 -6
  25. package/dist/scripts.js +255 -0
  26. package/dist/sign-in.js +3 -2
  27. package/dist/silence.js +28 -0
  28. package/dist/skills.js +41 -96
  29. package/dist/transcription.js +32 -23
  30. package/dist/update.js +51 -8
  31. package/package.json +19 -20
  32. package/dist/bootstrap.json +0 -24
  33. package/how-we-work.md +0 -33
  34. package/skills/ask-matt/LICENSE +0 -21
  35. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  36. package/skills/ask-matt/SKILL.md +0 -97
  37. package/skills/ask-matt/agents/openai.yaml +0 -5
  38. package/skills/code-review/LICENSE +0 -21
  39. package/skills/code-review/SKILL.md +0 -89
  40. package/skills/code-review/agents/openai.yaml +0 -3
  41. package/skills/codebase-design/DEEPENING.md +0 -37
  42. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  43. package/skills/codebase-design/LICENSE +0 -21
  44. package/skills/codebase-design/SKILL.md +0 -114
  45. package/skills/codebase-design/agents/openai.yaml +0 -3
  46. package/skills/diagnosing-bugs/LICENSE +0 -21
  47. package/skills/diagnosing-bugs/SKILL.md +0 -138
  48. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  49. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  50. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  51. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  52. package/skills/domain-modeling/LICENSE +0 -21
  53. package/skills/domain-modeling/SKILL.md +0 -74
  54. package/skills/domain-modeling/agents/openai.yaml +0 -3
  55. package/skills/grill-me/LICENSE +0 -21
  56. package/skills/grill-me/SKILL.md +0 -7
  57. package/skills/grill-me/agents/openai.yaml +0 -5
  58. package/skills/grill-with-docs/LICENSE +0 -21
  59. package/skills/grill-with-docs/SKILL.md +0 -7
  60. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  61. package/skills/grilling/LICENSE +0 -21
  62. package/skills/grilling/SKILL.md +0 -30
  63. package/skills/grilling/agents/openai.yaml +0 -3
  64. package/skills/handoff/LICENSE +0 -21
  65. package/skills/handoff/SKILL.md +0 -16
  66. package/skills/handoff/agents/openai.yaml +0 -5
  67. package/skills/implement/LICENSE +0 -21
  68. package/skills/implement/SKILL.md +0 -17
  69. package/skills/implement/agents/openai.yaml +0 -5
  70. package/skills/implement-spec/LICENSE +0 -21
  71. package/skills/implement-spec/SKILL.md +0 -40
  72. package/skills/implement-spec/agents/openai.yaml +0 -5
  73. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  74. package/skills/improve-codebase-architecture/LICENSE +0 -21
  75. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  76. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  77. package/skills/pr/CREDITS.md +0 -3
  78. package/skills/pr/LICENSE +0 -21
  79. package/skills/pr/SKILL.md +0 -170
  80. package/skills/pr/agents/openai.yaml +0 -3
  81. package/skills/prototype/LICENSE +0 -21
  82. package/skills/prototype/LOGIC.md +0 -67
  83. package/skills/prototype/SKILL.md +0 -26
  84. package/skills/prototype/UI.md +0 -112
  85. package/skills/prototype/agents/openai.yaml +0 -3
  86. package/skills/research/LICENSE +0 -21
  87. package/skills/research/SKILL.md +0 -12
  88. package/skills/research/agents/openai.yaml +0 -3
  89. package/skills/retro/LICENSE +0 -21
  90. package/skills/retro/SKILL.md +0 -44
  91. package/skills/retro/agents/openai.yaml +0 -5
  92. package/skills/tdd/LICENSE +0 -21
  93. package/skills/tdd/SKILL.md +0 -38
  94. package/skills/tdd/agents/openai.yaml +0 -3
  95. package/skills/tdd/mocking.md +0 -59
  96. package/skills/tdd/tests.md +0 -77
  97. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  98. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  99. package/skills/teach/LICENSE +0 -21
  100. package/skills/teach/MISSION-FORMAT.md +0 -31
  101. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  102. package/skills/teach/SKILL.md +0 -140
  103. package/skills/teach/agents/openai.yaml +0 -5
  104. package/skills/to-questionnaire/LICENSE +0 -21
  105. package/skills/to-questionnaire/SKILL.md +0 -54
  106. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  107. package/skills/to-spec/LICENSE +0 -21
  108. package/skills/to-spec/SKILL.md +0 -75
  109. package/skills/to-spec/agents/openai.yaml +0 -5
  110. package/skills/to-tickets/LICENSE +0 -21
  111. package/skills/to-tickets/SKILL.md +0 -105
  112. package/skills/to-tickets/agents/openai.yaml +0 -5
  113. package/skills/triage/AGENT-BRIEF.md +0 -207
  114. package/skills/triage/LICENSE +0 -21
  115. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  116. package/skills/triage/SKILL.md +0 -112
  117. package/skills/triage/agents/openai.yaml +0 -5
  118. package/skills/wait-what/LICENSE +0 -21
  119. package/skills/wait-what/SKILL.md +0 -7
  120. package/skills/wait-what/agents/openai.yaml +0 -5
  121. package/skills/wayfinder/LICENSE +0 -21
  122. package/skills/wayfinder/SKILL.md +0 -128
  123. package/skills/wayfinder/agents/openai.yaml +0 -5
  124. package/skills/wizard/LICENSE +0 -21
  125. package/skills/wizard/SKILL.md +0 -44
  126. package/skills/wizard/agents/openai.yaml +0 -3
  127. package/skills/wizard/template.sh +0 -223
  128. package/skills/writing-for-agents/LICENSE +0 -21
  129. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  130. package/skills/writing-for-agents/SKILL.md +0 -81
  131. package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/house-cli.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { createReadStream } from 'node:fs';
3
- import { readFile } from 'node:fs/promises';
4
- import { request, setGlobalProxyFromEnv } from 'node:http';
3
+ import { request } from 'node:http';
5
4
  import { request as httpsRequest } from 'node:https';
6
- import { basename, extname, join, resolve } from 'node:path';
5
+ import { extname, join, resolve } from 'node:path';
7
6
  import { Readable } from 'node:stream';
8
7
  import { pipeline } from 'node:stream/promises';
9
8
  import { downloaded, inspectedFile, storeRefusal, uploaded } from './files.js';
@@ -11,7 +10,7 @@ import { HOUSE_REF } from './git.js';
11
10
  import { ownerSocket, readOwnAgent } from './home.js';
12
11
  import { callHouse, rpc, UNREACHABLE, Unsettled } from './mcp.js';
13
12
  import { refusalOf, retryable } from './refusals.js';
14
- import { relayed, selectRelay } from './relay.js';
13
+ import { proxyUnlessRelayed, relayed, selectRelay } from './relay.js';
15
14
  import { parse } from 'yaml';
16
15
  const USAGE = "usage: house <tool> ['<arguments as JSON>']";
17
16
  const PUSH_USAGE = 'usage: house push [commit] [--owner]';
@@ -90,7 +89,6 @@ async function direct(message) {
90
89
  if (ownAgent === null)
91
90
  throw new Error(UNCONNECTED);
92
91
  selectRelay(process.env.HOUSE_KIT_RELAY || ownAgent.relay);
93
- setGlobalProxyFromEnv();
94
92
  const answer = await callHouse(ownAgent.house, ownAgent.credential, message).catch(() => {
95
93
  throw new Unsettled(UNREACHABLE);
96
94
  });
@@ -116,43 +114,35 @@ function unreadable(path, error) {
116
114
  ? `${path} is a folder; upload one file at a time`
117
115
  : `${path} is not a readable file`);
118
116
  }
119
- async function localFile(path) {
120
- try {
121
- return await readFile(path);
122
- }
123
- catch (error) {
124
- throw unreadable(path, error);
125
- }
126
- }
127
117
  function opened(url, method, headers, body) {
128
118
  return new Promise((answered, failed) => {
129
119
  const local = socketPath !== undefined;
130
120
  const target = local ? null : relayed(url);
131
121
  if (!local)
132
- setGlobalProxyFromEnv();
122
+ proxyUnlessRelayed();
133
123
  const sent = target === null
134
124
  ? request({ socketPath, path: url, method, headers }, answered)
135
125
  : (target.protocol === 'https:' ? httpsRequest : request)(target, { method, headers }, answered);
136
126
  sent.on('error', (error) => failed(new Error(!local ? UNREACHABLE : error.code === 'EPERM' || error.code === 'EACCES' ? SANDBOXED : CLOSED)));
137
- if (body instanceof Readable)
138
- pipeline(body, sent).catch(() => undefined);
127
+ if (body === undefined)
128
+ sent.end();
139
129
  else
140
- sent.end(body);
130
+ pipeline(body, sent).catch(() => undefined);
141
131
  });
142
132
  }
143
- async function textOf(answer) {
144
- return Buffer.concat(await answer.toArray()).toString('utf8');
133
+ function ranged(file, start, end) {
134
+ return end === start ? undefined : createReadStream(file, { start, end: end - 1 });
145
135
  }
146
- async function sent(url, bytes, operation) {
147
- const headers = { 'content-type': 'application/octet-stream', 'x-house-byte-operation': operation };
148
- const answer = await opened(url, 'POST', headers, bytes);
149
- const text = await textOf(answer);
150
- if (answer.statusCode === 200)
151
- return text;
152
- throw new Error(refusalOf(text)?.text ?? storeRefusal('upload', answer.statusCode).message);
136
+ async function* read(file, start, end) {
137
+ try {
138
+ yield* ranged(file, start, end) ?? [];
139
+ }
140
+ catch (error) {
141
+ throw unreadable(file, error);
142
+ }
153
143
  }
154
- async function sentPart(url, file, start, end) {
155
- const answer = await opened(url, 'PUT', { 'content-length': String(end - start) }, end === start ? undefined : createReadStream(file, { start, end: end - 1 }));
144
+ async function sentPart(url, file, start, end, headers) {
145
+ const answer = await opened(url, 'PUT', { ...headers, 'content-length': String(end - start) }, ranged(file, start, end));
156
146
  await answer.toArray();
157
147
  if (answer.statusCode !== 200)
158
148
  throw storeRefusal('upload', answer.statusCode);
@@ -191,10 +181,10 @@ async function described(file) {
191
181
  const mediaType = MEDIA_TYPES.get(extname(file).slice(1).toLowerCase());
192
182
  return { bytes, sha256: digest.digest('hex'), ...(mediaType === undefined ? {} : { media_type: mediaType }) };
193
183
  }
194
- async function finished(upload, room) {
195
- const text = await called('upload', { upload, room_ref: room });
184
+ async function finished(upload, room, partSha256) {
185
+ const text = await called('upload', { upload, room_ref: room, part_sha256: partSha256 });
196
186
  const answer = parse(text);
197
- return answer !== null && ('upload' in answer || 'checking' in answer) ? answer : { text };
187
+ return answer !== null && 'upload' in answer ? answer : { text };
198
188
  }
199
189
  async function upload({ file, ...destination }) {
200
190
  if (typeof file !== 'string' || file === '')
@@ -205,33 +195,15 @@ async function upload({ file, ...destination }) {
205
195
  if (declared?.upload === undefined)
206
196
  return answer;
207
197
  const accepted = await uploaded(declared.upload, {
198
+ name: file,
208
199
  bytes: description.bytes,
209
- send: (url, start, end) => sentPart(url, file, start, end),
210
- finish: (id) => finished(id, destination.room_ref),
200
+ sha256: description.sha256,
201
+ read: (start, end) => read(file, start, end),
202
+ send: (url, start, end, headers) => sentPart(url, file, start, end, headers),
203
+ finish: (id, partSha256) => finished(id, destination.room_ref, partSha256),
211
204
  });
212
205
  return accepted.text;
213
206
  }
214
- async function appendRecord(args) {
215
- const held = new Map();
216
- const attachments = Array.isArray(args.attachments)
217
- ? await Promise.all(args.attachments.map(async (attachment) => {
218
- if (typeof attachment !== 'string')
219
- return attachment;
220
- const description = await described(attachment);
221
- held.set(description.sha256, attachment);
222
- return { name: basename(attachment), ...description };
223
- }))
224
- : args.attachments;
225
- const record = { ...args, attachments };
226
- for (;;) {
227
- const answer = await called('append_record', record);
228
- const { upload } = parse(answer);
229
- if (upload === undefined)
230
- return answer;
231
- for (const grant of upload)
232
- await sent(grant.url, await localFile(held.get(grant.sha256)), grant.operation);
233
- }
234
- }
235
207
  function unexpected(argument, usage) {
236
208
  return new Error(`unexpected argument ${argument}; ${usage}`);
237
209
  }
@@ -299,7 +271,7 @@ async function house(args) {
299
271
  return [...(tool.description === undefined ? [] : [tool.description]), JSON.stringify(tool.inputSchema)].join('\n');
300
272
  }
301
273
  const parsed = argumentsOf(argument);
302
- return verb === 'upload' ? upload(parsed) : verb === 'append_record' ? appendRecord(parsed) : called(verb, parsed);
274
+ return verb === 'upload' ? upload(parsed) : called(verb, parsed);
303
275
  }
304
276
  async function called(name, args) {
305
277
  const result = await mcp('tools/call', { name, arguments: args });
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
3
- if (major < 24 || (major === 24 && minor < 14)) {
4
- process.stderr.write(`house: the Kit needs Node 24.14.0 or newer; this is ${process.versions.node}\n`);
2
+ const [major = 0, minor = 0, patch = 0] = process.versions.node.split('.').map(Number);
3
+ if (major < 24 || (major === 24 && (minor < 22 || (minor === 22 && patch < 1)))) {
4
+ process.stderr.write(`house: the Kit needs Node 24.22.1 or newer; this is ${process.versions.node}\n`);
5
5
  process.exitCode = 1;
6
6
  }
7
7
  else {
@@ -1,12 +1,32 @@
1
+ import { agentInstructions } from '@agentshouse/skills';
1
2
  import { HOUSE_REF } from './git.js';
2
- import { readHowWeWork } from './skills.js';
3
- const SOURCE_LINE = "From House:";
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
- 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
- const READ_LINE = "Optional per-Room instructions: `/rooms/private/rooms/<handle>/AGENTS.md`.";
7
- const REQUEST_LINE = "For another Agent's help, `send_routine_request`; its mail comes back into this conversation. If an Agent of your User has no Routine to call, write one in the Private Room and `create_routine_access` with your User as its only caller.";
8
- const COPIES_LINE = `House Local copies; before work run \`house fetch\` and rebase onto \`${HOUSE_REF}\`; after it commit, then \`house push\`:`;
3
+ const OWNER_RULES = '/rooms/private/owner-rules.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 readOwnerRules(bridge) {
12
+ let document = '';
13
+ for (let command = `cat ${OWNER_RULES}`; 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 = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE, REQUEST_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
11
- return [lines.join('\n'), base, (await readHowWeWork(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
30
+ const lines = agentInstructions(HOUSE_REF, copies);
31
+ return [lines.join('\n'), base, (await readOwnerRules(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
12
32
  }
package/dist/kit-cli.js CHANGED
@@ -1,9 +1,9 @@
1
- import { setGlobalProxyFromEnv } from 'node:http';
2
1
  import { parseArgs } from 'node:util';
3
2
  import { answerGitCredential } from './app-git.js';
4
3
  import { chatgpt } from './chatgpt.js';
5
4
  import { login } from './login.js';
6
5
  import { logout } from './logout.js';
6
+ import { proxyUnlessRelayed } from './relay.js';
7
7
  import { resident } from './resident.js';
8
8
  const USAGE = 'usage: kit <login|logout|resident|chatgpt>\n';
9
9
  const COMMANDS = new Map([
@@ -36,7 +36,7 @@ const COMMANDS = new Map([
36
36
  ],
37
37
  ]);
38
38
  const [command, ...argv] = process.argv.slice(2);
39
- setGlobalProxyFromEnv();
39
+ proxyUnlessRelayed();
40
40
  async function run() {
41
41
  if (command === 'login')
42
42
  return login(argv);
package/dist/kit-main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
3
- if (major < 24 || (major === 24 && minor < 14)) {
4
- process.stderr.write(`kit: the Kit needs Node 24.14.0 or newer; this is ${process.versions.node}\n`);
2
+ const [major = 0, minor = 0, patch = 0] = process.versions.node.split('.').map(Number);
3
+ if (major < 24 || (major === 24 && (minor < 22 || (minor === 22 && patch < 1)))) {
4
+ process.stderr.write(`kit: the Kit needs Node 24.22.1 or newer; this is ${process.versions.node}\n`);
5
5
  process.exitCode = 1;
6
6
  }
7
7
  else {
package/dist/login.js CHANGED
@@ -10,6 +10,7 @@ import { refusalOf } from './refusals.js';
10
10
  import { selectRelay } from './relay.js';
11
11
  import { startService } from './service.js';
12
12
  import { placeSkillSet } from './skills.js';
13
+ import { endUnserved } from './wire.js';
13
14
  export const DEFAULT_HOUSE = 'https://agents.house';
14
15
  const OPENER = process.platform === 'darwin' ? 'open' : 'xdg-open';
15
16
  // Polling every two seconds brings the User's confirmation to the waiting Kit moments after they give it.
@@ -25,6 +26,7 @@ export async function accepted(house, credential) {
25
26
  return true;
26
27
  if (checked.status === 401)
27
28
  return false;
29
+ endUnserved(checked.text);
28
30
  throw new Error(`House answered ${checked.status} when Kit checked this computer's own agent's connection: ${checked.text}`);
29
31
  }
30
32
  const USAGE = `usage: kit login [--house <origin>] [--environment <id>] [--rooms <ref>[,<ref>...] | --no-private]
@@ -119,7 +121,7 @@ export async function login(argv) {
119
121
  open(started.start_url);
120
122
  exchanged = await retrying(() => post(house, '/kit/token', { login: started.login, code_verifier: verifier }), undefined, () => POLL_INTERVAL_MS);
121
123
  }
122
- await placeSkillSet();
124
+ await placeSkillSet(house);
123
125
  await writeCodexRule().catch((error) => process.stderr.write(`kit: ${error.message}\n`));
124
126
  const relayRecord = relay === undefined ? {} : { relay };
125
127
  await writeEnrolment({
package/dist/mcp.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { KIT_RELEASE } from './clis.js';
3
3
  import { relayed } from './relay.js';
4
+ import { KIT_WIRE, KIT_WIRE_HEADER } from './wire.js';
4
5
  const MCP_PROTOCOL_VERSION = '2026-07-28';
5
6
  export const UNREACHABLE = 'House is unreachable; call again in a minute';
6
7
  export class Unsettled extends Error {
@@ -33,6 +34,7 @@ export async function callHouse(origin, credential, message, operation = null, s
33
34
  'mcp-protocol-version': MCP_PROTOCOL_VERSION,
34
35
  'mcp-method': message.method,
35
36
  ...(tool === null ? {} : { 'mcp-name': tool }),
37
+ [KIT_WIRE_HEADER]: String(KIT_WIRE),
36
38
  },
37
39
  body: mcpBody(message, operation),
38
40
  signal,
package/dist/relay.js CHANGED
@@ -1,6 +1,11 @@
1
+ import { setGlobalProxyFromEnv } from 'node:http';
2
+ const PROXY = /^https?_proxy$/i;
3
+ const LOOPBACK = ['127.0.0.1', 'localhost', '::1'];
1
4
  let selected;
5
+ let restoreProxy;
2
6
  export function selectRelay(relay) {
3
7
  selected = relay === undefined || relay === '' ? null : relay;
8
+ proxyUnlessRelayed();
4
9
  }
5
10
  export function relayOrigin() {
6
11
  if (selected !== undefined)
@@ -8,6 +13,22 @@ export function relayOrigin() {
8
13
  const ambient = process.env.HOUSE_KIT_RELAY;
9
14
  return ambient === undefined || ambient === '' ? undefined : ambient;
10
15
  }
16
+ function unlessRelayed() {
17
+ if (relayOrigin() === undefined)
18
+ return { ...process.env };
19
+ return Object.fromEntries(Object.entries(process.env).filter(([name]) => !PROXY.test(name)));
20
+ }
21
+ export function proxyUnlessRelayed() {
22
+ restoreProxy?.();
23
+ restoreProxy = setGlobalProxyFromEnv(unlessRelayed());
24
+ }
25
+ export function childEnvironment() {
26
+ const environment = unlessRelayed();
27
+ if (!Object.keys(environment).some((name) => PROXY.test(name)))
28
+ return environment;
29
+ const bypassed = [environment.NO_PROXY ?? environment.no_proxy, ...LOOPBACK].filter((entry) => entry !== undefined && entry !== '').join(',');
30
+ return { ...environment, NO_PROXY: bypassed, no_proxy: bypassed };
31
+ }
11
32
  export function relayed(target) {
12
33
  const url = new URL(target);
13
34
  const relay = relayOrigin();
package/dist/resident.js CHANGED
@@ -10,10 +10,11 @@ import { house } from './api.js';
10
10
  import { received } from './bridge.js';
11
11
  import { CONVERSATION_METHODS, Conversations } from './conversations.js';
12
12
  import { LocalCopies } from './copies.js';
13
- import { restart } from './end.js';
14
- import { agentBase, enrolled, forgetRestart, kitHome, ownerLock, ownerSocket, restartAsked } from './home.js';
13
+ import { restart, unknownMethod } from './end.js';
14
+ import { agentBase, enrolled, forgetRestart, kitHome, ownerLock, ownerSocket, readEnrolment, restartAsked } from './home.js';
15
15
  import { Calls } from './calls.js';
16
16
  import { killDescendants } from './scope.js';
17
+ import { removeInputs, Scripts } from './scripts.js';
17
18
  import { quoted } from './shell.js';
18
19
  import { SignIns } from './sign-in.js';
19
20
  import { placeSkillSet } from './skills.js';
@@ -116,7 +117,7 @@ async function started() {
116
117
  killDescendants();
117
118
  process.exit(1);
118
119
  });
119
- await placeSkillSet();
120
+ await placeSkillSet((await readEnrolment()).house);
120
121
  await mkdir(agentBase(), { recursive: true });
121
122
  await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
122
123
  await installShim(codexLauncher(), 'codex-main');
@@ -142,6 +143,9 @@ export async function resident() {
142
143
  else if (restartAsked() && conversations.quiet()) {
143
144
  restart('restarting for an updated CLI');
144
145
  }
146
+ else if (update.held() && conversations.quiet()) {
147
+ update.apply(connection.pause());
148
+ }
145
149
  else if (!reported) {
146
150
  reported = true;
147
151
  logged(house.deliver('/kit/idle', {}, idle));
@@ -157,10 +161,13 @@ export async function resident() {
157
161
  changed,
158
162
  });
159
163
  const signIns = new SignIns(house, agents, calls);
160
- const update = new KitUpdate(calls);
164
+ const update = new KitUpdate(calls, changed);
165
+ removeInputs();
166
+ const scripts = new Scripts(calls, (notification) => connection?.send(notification) ?? false);
161
167
  for (const signal of ['SIGTERM', 'SIGINT']) {
162
168
  process.once(signal, () => {
163
169
  killDescendants();
170
+ removeInputs();
164
171
  process.kill(process.pid, signal);
165
172
  });
166
173
  }
@@ -173,15 +180,18 @@ export async function resident() {
173
180
  changed();
174
181
  logged(copies.connected());
175
182
  },
183
+ closed: () => scripts.disconnected(),
176
184
  call: (call) => {
177
185
  if (call.kind === 'sign_in')
178
186
  signIns.start(call);
179
187
  else if (call.kind === 'update')
180
- update.start(call);
188
+ update.hold(call);
189
+ else if (call.kind === 'run_wizard' || call.kind === 'run_function')
190
+ scripts.start(call);
181
191
  else if (CONVERSATION_METHODS.has(call.kind))
182
192
  conversations.call(call);
183
193
  else {
184
- const refused = { refused: `This Kit does not know ${call.kind}; update the Kit` };
194
+ const refused = { refused: unknownMethod(call.kind) };
185
195
  void calls.carry(call.input_id, async () => ({ result: refused }));
186
196
  }
187
197
  },
@@ -192,6 +202,7 @@ export async function resident() {
192
202
  logged(copies.changed(notification.rooms));
193
203
  if (notification.type === 'work_available' && notification.subject === 'agents')
194
204
  logged(agents.refresh());
205
+ scripts.notified(notification);
195
206
  },
196
207
  });
197
208
  }
@@ -0,0 +1,255 @@
1
+ import { spawn as spawnPiped } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { once } from 'node:events';
4
+ import { rmSync } from 'node:fs';
5
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
6
+ import { constants } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { loginPath, onLoginPath } from './clis.js';
9
+ import { kitHome } from './home.js';
10
+ import { childEnvironment } from './relay.js';
11
+ import { killTree } from './scope.js';
12
+ const TERMINAL = 'xterm-256color';
13
+ const FIRST_SIZE = { cols: 80, rows: 24 };
14
+ const SIGNALLED = 128;
15
+ const NEWLINE = 0x0a;
16
+ const NO_BASH = "bash is not on the login shell's PATH";
17
+ const LOST = { refused: 'the Kit lost its connection to House' };
18
+ function causeOf(error) {
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
21
+ function failed(plan, cause) {
22
+ process.stderr.write(`kit: ${plan.named} could not start: ${cause}\n`);
23
+ return { refused: `the script could not start: ${cause}` };
24
+ }
25
+ function planned(call) {
26
+ if (call.kind === 'run_wizard')
27
+ return { named: 'a Web Wizard', script: call.script, terminal: 'page', function: null };
28
+ return {
29
+ named: 'an Environment function',
30
+ script: call.script,
31
+ terminal: call.terminal ? 'at once' : null,
32
+ function: { input: call.input, timeCap: call.time_cap, outputCap: call.output_cap },
33
+ };
34
+ }
35
+ function inputs() {
36
+ return join(kitHome(), 'inputs');
37
+ }
38
+ async function inputFile(value) {
39
+ await mkdir(inputs(), { recursive: true, mode: 0o700 });
40
+ const path = join(inputs(), `${randomUUID()}.json`);
41
+ await writeFile(path, JSON.stringify(value), { mode: 0o600, flag: 'wx' });
42
+ return path;
43
+ }
44
+ export function removeInputs() {
45
+ rmSync(inputs(), { recursive: true, force: true });
46
+ }
47
+ function struck(line, typed) {
48
+ return typed.some((typedLine) => line.includes(typedLine));
49
+ }
50
+ class KeptOutput {
51
+ cap;
52
+ typedText = '';
53
+ text = '';
54
+ bytes = 0;
55
+ line = '';
56
+ lineBytes = 0;
57
+ dropping = false;
58
+ cut = false;
59
+ constructor(cap) {
60
+ this.cap = cap;
61
+ }
62
+ typed(data) {
63
+ this.typedText += data;
64
+ }
65
+ add(data) {
66
+ const pieces = data.split('\n');
67
+ const rest = pieces.pop();
68
+ for (const piece of pieces)
69
+ this.ended(piece);
70
+ this.extend(rest);
71
+ }
72
+ kept() {
73
+ const typed = this.typedLines(true);
74
+ this.text = [...this.text.split(/(?<=\n)/), this.line].filter((line) => line !== '' && !struck(line, typed)).join('');
75
+ this.bytes = Buffer.byteLength(this.text);
76
+ this.trim();
77
+ return { output: this.text, cut: this.cut };
78
+ }
79
+ typedLines(all) {
80
+ const lines = this.typedText.split(/\r\n|\r|\n/);
81
+ if (!all)
82
+ lines.pop();
83
+ return lines.filter((line) => line !== '');
84
+ }
85
+ ended(piece) {
86
+ const line = `${this.line}${piece}\n`;
87
+ const dropped = this.dropping;
88
+ this.line = '';
89
+ this.lineBytes = 0;
90
+ this.dropping = false;
91
+ if (dropped || struck(line, this.typedLines(false)))
92
+ return;
93
+ this.text += line;
94
+ this.bytes += Buffer.byteLength(line);
95
+ if (this.bytes > 2 * this.cap)
96
+ this.trim();
97
+ }
98
+ extend(piece) {
99
+ if (this.dropping)
100
+ return;
101
+ this.line += piece;
102
+ this.lineBytes += Buffer.byteLength(piece);
103
+ if (this.lineBytes <= this.cap)
104
+ return;
105
+ this.text = '';
106
+ this.bytes = 0;
107
+ this.line = '';
108
+ this.lineBytes = 0;
109
+ this.dropping = true;
110
+ this.cut = true;
111
+ }
112
+ trim() {
113
+ if (this.bytes <= this.cap)
114
+ return;
115
+ const bytes = Buffer.from(this.text);
116
+ const start = bytes.indexOf(NEWLINE, bytes.length - this.cap - 1) + 1;
117
+ this.text = bytes.subarray(start).toString('utf8');
118
+ this.bytes = bytes.length - start;
119
+ this.cut = true;
120
+ }
121
+ }
122
+ export class Scripts {
123
+ calls;
124
+ send;
125
+ runs = new Map();
126
+ constructor(calls, send) {
127
+ this.calls = calls;
128
+ this.send = send;
129
+ }
130
+ start(call) {
131
+ void this.calls.carry(call.input_id, () => this.run(call.input_id, planned(call)));
132
+ }
133
+ notified(notification) {
134
+ const run = this.runs.get(String(notification.wizard));
135
+ if (run === undefined)
136
+ return;
137
+ if (notification.type === 'wizard_input')
138
+ this.typed(run, String(notification.data));
139
+ if (notification.type === 'wizard_size')
140
+ this.resized(run, { cols: Number(notification.cols), rows: Number(notification.rows) });
141
+ if (notification.type === 'wizard_end' && run.plan.terminal === 'page')
142
+ this.end(run, {});
143
+ }
144
+ disconnected() {
145
+ for (const run of this.runs.values())
146
+ this.end(run, LOST);
147
+ }
148
+ async run(id, plan) {
149
+ const size = plan.terminal === 'page' ? null : FIRST_SIZE;
150
+ const output = plan.function === null ? null : new KeptOutput(plan.function.outputCap);
151
+ const run = { plan, input: null, output, process: null, pipes: [], terminal: null, size, typed: [], ended: null, sized: Promise.withResolvers() };
152
+ if (size !== null)
153
+ run.sized.resolve();
154
+ this.runs.set(id, run);
155
+ try {
156
+ return { result: await this.ran(id, run), release: plan.function !== null };
157
+ }
158
+ finally {
159
+ this.runs.delete(id);
160
+ if (run.input !== null)
161
+ await rm(run.input, { force: true });
162
+ }
163
+ }
164
+ async ran(id, run) {
165
+ const { plan, output } = run;
166
+ let exited;
167
+ try {
168
+ const { spawn } = await import('@lydell/node-pty');
169
+ const shell = await onLoginPath('bash');
170
+ if (shell === null)
171
+ return failed(plan, NO_BASH);
172
+ const path = await loginPath();
173
+ await run.sized.promise;
174
+ if (plan.function !== null)
175
+ run.input = await inputFile(plan.function.input);
176
+ if (run.ended !== null)
177
+ return run.ended;
178
+ const environment = { ...childEnvironment(), PATH: path, ...(run.input === null ? {} : { HOUSE_INPUT: run.input }) };
179
+ if (plan.terminal === null) {
180
+ ({ exited } = await this.piped(plan.script, shell, environment, run));
181
+ }
182
+ else {
183
+ const terminal = spawn(shell, ['-c', plan.script], { name: TERMINAL, ...run.size, cwd: process.cwd(), env: environment });
184
+ run.process = terminal;
185
+ run.terminal = terminal;
186
+ terminal.onData((data) => {
187
+ this.send({ type: 'wizard_output', wizard: id, data });
188
+ output?.add(data);
189
+ });
190
+ for (const data of run.typed.splice(0))
191
+ terminal.write(data);
192
+ exited = new Promise((resolve) => terminal.onExit(({ exitCode, signal }) => resolve(signal ? SIGNALLED + signal : exitCode)));
193
+ }
194
+ }
195
+ catch (error) {
196
+ return failed(plan, causeOf(error));
197
+ }
198
+ let timedOut = false;
199
+ // House sends a function run's time cap in seconds, and a run still going at its cap ends with its process group and the output pipes it holds.
200
+ const capped = plan.function === null ? undefined : setTimeout(() => {
201
+ timedOut = true;
202
+ this.stop(run);
203
+ }, plan.function.timeCap * 1000);
204
+ const code = await exited;
205
+ clearTimeout(capped);
206
+ if (run.ended !== null)
207
+ return run.ended;
208
+ if (output === null)
209
+ return { exit_code: code };
210
+ const kept = output.kept();
211
+ return timedOut ? { timed_out: true, ...kept } : { exit_code: code, ...kept };
212
+ }
213
+ async piped(script, shell, environment, run) {
214
+ const child = spawnPiped(shell, ['-c', script], { cwd: process.cwd(), env: environment, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
215
+ const exited = new Promise((resolve) => child.once('close', (code, signal) => resolve(signal === null ? code : SIGNALLED + constants.signals[signal])));
216
+ await once(child, 'spawn');
217
+ run.process = { pid: child.pid };
218
+ run.pipes = [child.stdout, child.stderr];
219
+ for (const pipe of run.pipes) {
220
+ pipe.setEncoding('utf8');
221
+ pipe.on('data', (data) => run.output.add(data));
222
+ }
223
+ if (run.ended !== null)
224
+ this.stop(run);
225
+ return { exited };
226
+ }
227
+ typed(run, data) {
228
+ run.output?.typed(data);
229
+ if (run.terminal === null)
230
+ run.typed.push(data);
231
+ else
232
+ run.terminal.write(data);
233
+ }
234
+ resized(run, size) {
235
+ run.size = size;
236
+ run.sized.resolve();
237
+ try {
238
+ run.terminal?.resize(size.cols, size.rows);
239
+ }
240
+ catch { }
241
+ }
242
+ end(run, result) {
243
+ if (run.ended !== null)
244
+ return;
245
+ run.ended = result;
246
+ run.sized.resolve();
247
+ this.stop(run);
248
+ }
249
+ stop(run) {
250
+ if (run.process !== null)
251
+ killTree(run.process);
252
+ for (const pipe of run.pipes)
253
+ pipe.destroy();
254
+ }
255
+ }
package/dist/sign-in.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
- import { CLIS, withoutProxy } from './clis.js';
3
+ import { CLIS } from './clis.js';
4
+ import { childEnvironment } from './relay.js';
4
5
  import { killTree } from './scope.js';
5
6
  import { holdSecretInput } from './secret-input.js';
6
7
  const ESCAPE = /\u001b\[[\d;]*m/g;
@@ -56,7 +57,7 @@ export class SignIns {
56
57
  const child = spawn(cli, login.args, {
57
58
  stdio: ['pipe', 'pipe', 'pipe'],
58
59
  detached: true,
59
- env: withoutProxy(),
60
+ env: childEnvironment(),
60
61
  });
61
62
  const running = () => child.exitCode === null && child.signalCode === null;
62
63
  const stop = () => killTree(child);