@agentshouse/kit 0.2.1-c30728be20485 → 0.2.1-c35516351ef49

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 (99) hide show
  1. package/README.md +27 -26
  2. package/dist/acp.js +16 -93
  3. package/dist/agents.js +119 -34
  4. package/dist/api.js +9 -5
  5. package/dist/app-git.js +64 -0
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +11 -37
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +116 -70
  10. package/dist/codex-main.js +58 -0
  11. package/dist/conversations.js +301 -237
  12. package/dist/copies.js +145 -110
  13. package/dist/end.js +18 -4
  14. package/dist/enrol-main.js +7 -1
  15. package/dist/events.js +524 -0
  16. package/dist/files.js +21 -1
  17. package/dist/git.js +11 -8
  18. package/dist/home.js +81 -6
  19. package/dist/house-cli.js +291 -0
  20. package/dist/house-main.js +7 -159
  21. package/dist/instructions.js +9 -32
  22. package/dist/kit-cli.js +68 -0
  23. package/dist/kit-main.js +8 -19
  24. package/dist/login.js +118 -103
  25. package/dist/logout.js +16 -13
  26. package/dist/parts.js +82 -0
  27. package/dist/placement.js +48 -0
  28. package/dist/relay.js +12 -2
  29. package/dist/resident.js +38 -27
  30. package/dist/scope.js +203 -0
  31. package/dist/service-main.js +9 -0
  32. package/dist/service.js +33 -0
  33. package/dist/shell.js +3 -0
  34. package/dist/sign-in.js +34 -10
  35. package/dist/skills.js +81 -12
  36. package/dist/stream.js +66 -15
  37. package/dist/transcription.js +57 -11
  38. package/dist/update.js +28 -6
  39. package/dist/wire.js +6 -0
  40. package/dist/work.js +348 -0
  41. package/how-we-work.md +13 -3
  42. package/package.json +3 -2
  43. package/skills/ask-matt/SKILL.md +17 -11
  44. package/skills/ask-matt/agents/openai.yaml +1 -0
  45. package/skills/code-review/SKILL.md +5 -5
  46. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  47. package/skills/codebase-design/SKILL.md +0 -2
  48. package/skills/diagnosing-bugs/SKILL.md +2 -4
  49. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  50. package/skills/domain-modeling/SKILL.md +12 -14
  51. package/skills/grill-me/SKILL.md +1 -2
  52. package/skills/grill-me/agents/openai.yaml +1 -0
  53. package/skills/grill-with-docs/SKILL.md +1 -2
  54. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  55. package/skills/grilling/SKILL.md +2 -2
  56. package/skills/handoff/SKILL.md +2 -3
  57. package/skills/handoff/agents/openai.yaml +1 -0
  58. package/skills/implement/SKILL.md +5 -4
  59. package/skills/implement/agents/openai.yaml +1 -0
  60. package/skills/implement-spec/SKILL.md +40 -0
  61. package/skills/implement-spec/agents/openai.yaml +5 -0
  62. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  63. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  64. package/skills/pr/CREDITS.md +3 -0
  65. package/skills/pr/LICENSE +21 -0
  66. package/skills/pr/SKILL.md +170 -0
  67. package/skills/pr/agents/openai.yaml +3 -0
  68. package/skills/prototype/SKILL.md +0 -2
  69. package/skills/research/SKILL.md +0 -2
  70. package/skills/retro/LICENSE +21 -0
  71. package/skills/retro/SKILL.md +44 -0
  72. package/skills/retro/agents/openai.yaml +5 -0
  73. package/skills/tdd/SKILL.md +2 -4
  74. package/skills/teach/SKILL.md +3 -4
  75. package/skills/teach/agents/openai.yaml +1 -0
  76. package/skills/to-questionnaire/SKILL.md +1 -2
  77. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  78. package/skills/to-spec/SKILL.md +1 -2
  79. package/skills/to-spec/agents/openai.yaml +1 -0
  80. package/skills/to-tickets/SKILL.md +3 -4
  81. package/skills/to-tickets/agents/openai.yaml +1 -0
  82. package/skills/triage/AGENT-BRIEF.md +207 -0
  83. package/skills/triage/LICENSE +21 -0
  84. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  85. package/skills/triage/SKILL.md +112 -0
  86. package/skills/triage/agents/openai.yaml +5 -0
  87. package/skills/wait-what/SKILL.md +2 -3
  88. package/skills/wait-what/agents/openai.yaml +1 -0
  89. package/skills/wayfinder/SKILL.md +5 -6
  90. package/skills/wayfinder/agents/openai.yaml +1 -0
  91. package/skills/wizard/LICENSE +21 -0
  92. package/skills/wizard/SKILL.md +44 -0
  93. package/skills/wizard/agents/openai.yaml +3 -0
  94. package/skills/wizard/template.sh +223 -0
  95. package/skills/writing-for-agents/SKILL.md +0 -2
  96. package/dist/blocks.js +0 -51
  97. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  98. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  99. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "version": "0.2.1-c30728be20485",
3
+ "version": "0.2.1-c35516351ef49",
4
4
  "hosts": {
5
5
  "macos": {
6
6
  "asset": {
7
7
  "name": "connect-linux.sh",
8
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c30728be20485/connect-linux.sh"
8
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c35516351ef49/connect-linux.sh"
9
9
  }
10
10
  },
11
11
  "linux": {
12
12
  "asset": {
13
13
  "name": "connect-linux.sh",
14
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c30728be20485/connect-linux.sh"
14
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c35516351ef49/connect-linux.sh"
15
15
  }
16
16
  },
17
17
  "windows": {
18
18
  "asset": {
19
19
  "name": "connect-windows.ps1",
20
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c30728be20485/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c35516351ef49/connect-windows.ps1"
21
21
  }
22
22
  }
23
23
  }
package/dist/bridge.js CHANGED
@@ -11,11 +11,10 @@ import { loginPath } from './clis.js';
11
11
  import { changedPaths, } from './copies.js';
12
12
  import { kitHome, readEnrolment } from './home.js';
13
13
  import { callHouse, mcpBody, rpc, UNREACHABLE } from './mcp.js';
14
- import { operationId, uuidOf } from './operation.js';
14
+ import { operationId } from './operation.js';
15
15
  import { refusalOf, retryable } from './refusals.js';
16
16
  import { relayed } from './relay.js';
17
17
  const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
18
- const ATTACHMENTS = '/kit/attachments/upload';
19
18
  export async function received(request) {
20
19
  let text = '';
21
20
  request.setEncoding('utf8');
@@ -68,11 +67,6 @@ function resendAfter(forwarded, attempt) {
68
67
  const seconds = pendingSeconds(forwarded);
69
68
  return seconds === null ? null : seconds * 1000;
70
69
  }
71
- function versioned(conversationId, text) {
72
- const file = JSON.parse(text);
73
- const identity = JSON.stringify([conversationId, file.room_ref ?? null, file.name, file.sha256]);
74
- return JSON.stringify({ ...file, version: uuidOf(createHash('sha256').update(identity).digest(), 8) });
75
- }
76
70
  function editAnswer(forwarded) {
77
71
  const answer = answered(forwarded);
78
72
  if ('refused' in answer)
@@ -103,7 +97,7 @@ export async function openBridge(house, copies, conversationId, signal) {
103
97
  const after = resendAfter(forwarded, attempt);
104
98
  if (after === null)
105
99
  return forwarded;
106
- // A write House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
100
+ // A Tool call House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
107
101
  await delay(after, undefined, { signal: closed.signal });
108
102
  }
109
103
  };
@@ -141,32 +135,9 @@ export async function openBridge(house, copies, conversationId, signal) {
141
135
  const held = JSON.parse(text).transfer;
142
136
  return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
143
137
  };
144
- let mutations = null;
145
- const writes = async (tool) => {
146
- mutations ??= house
147
- .post('/kit/tools/mutations', {}, closed.signal)
148
- .then(({ tools }) => new Set(tools.filter((listed) => listed.writes).map((listed) => listed.name)));
149
- try {
150
- return (await mutations).has(tool);
151
- }
152
- catch (error) {
153
- mutations = null;
154
- throw error;
155
- }
156
- };
157
- const forward = async (path, text) => {
158
- if (path === '/') {
159
- const message = JSON.parse(text);
160
- const tool = message.method === 'tools/call' ? String(message.params.name) : null;
161
- return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
162
- }
163
- const answer = await fetch(relayed(new URL(path, origin)), {
164
- method: 'POST',
165
- headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
166
- body: versioned(conversationId, text),
167
- signal: closed.signal,
168
- });
169
- return { status: answer.status, text: await answer.text() };
138
+ const forward = (text) => {
139
+ const message = JSON.parse(text);
140
+ return send(message, message.method === 'tools/call' ? operationId() : null);
170
141
  };
171
142
  const caller = {
172
143
  key: `conversation:${conversationId}`,
@@ -188,11 +159,13 @@ export async function openBridge(house, copies, conversationId, signal) {
188
159
  const { cwd, commit } = JSON.parse(text);
189
160
  return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
190
161
  }
162
+ if (path === '/git/fetch')
163
+ return { status: 200, text: JSON.stringify(await copies.fetch()) };
191
164
  if (path === '/git/observed') {
192
165
  await copies.observed(JSON.parse(text));
193
166
  return { status: 200, text: '{}' };
194
167
  }
195
- return path === '/' || path === ATTACHMENTS ? forward(path, text) : { status: 404, text: '' };
168
+ return path === '/' ? forward(text) : { status: 404, text: '' };
196
169
  };
197
170
  const sockets = join(kitHome(), 'bridges');
198
171
  await mkdir(sockets, { recursive: true, mode: 0o700 });
@@ -212,7 +185,8 @@ export async function openBridge(house, copies, conversationId, signal) {
212
185
  response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
213
186
  return;
214
187
  }
215
- response.writeHead(502).end(request.url === '/git/push' && error instanceof Error ? error.message : UNREACHABLE);
188
+ const verb = request.url === '/git/push' || request.url === '/git/fetch';
189
+ response.writeHead(502).end(verb && error instanceof Error ? error.message : UNREACHABLE);
216
190
  }
217
191
  });
218
192
  bridge.listen(socketPath);
@@ -239,7 +213,7 @@ export async function openBridge(house, copies, conversationId, signal) {
239
213
  GIT_CONFIG_VALUE_0: app,
240
214
  },
241
215
  tool: async (name, args) => {
242
- const forwarded = await forward('/', JSON.stringify(call(name, args)));
216
+ const forwarded = await forward(JSON.stringify(call(name, args)));
243
217
  if (forwarded.status !== 200)
244
218
  throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
245
219
  const answer = JSON.parse(forwarded.text);
@@ -0,0 +1,32 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { placeAppGit } from './app-git.js';
3
+ import { kitHome, readOwnAgent, writeOwnAgent } from './home.js';
4
+ import { accepted, DEFAULT_HOUSE } from './login.js';
5
+ const KEY_SOURCE = 'New key for a script on Connections';
6
+ const USAGE = `usage: kit chatgpt --key <key> [--house <origin>]
7
+ --key <key> a key from ${KEY_SOURCE}
8
+ --house <origin> the House of the key, ${DEFAULT_HOUSE} unless one is stored
9
+ `;
10
+ export async function chatgpt(argv) {
11
+ const { values } = parseArgs({
12
+ args: argv,
13
+ options: { key: { type: 'string' }, house: { type: 'string' }, help: { type: 'boolean', default: false } },
14
+ });
15
+ if (values.help) {
16
+ process.stdout.write(USAGE);
17
+ return;
18
+ }
19
+ if (values.key === undefined)
20
+ throw new Error(`pass --key with a key from ${KEY_SOURCE}`);
21
+ const before = await readOwnAgent();
22
+ const house = values.house ?? before?.house ?? DEFAULT_HOUSE;
23
+ if (!(await accepted(house, values.key)))
24
+ throw new Error(`${house} does not accept this key; pass one from ${KEY_SOURCE}`);
25
+ const relay = process.env.HOUSE_KIT_RELAY || undefined;
26
+ await writeOwnAgent({ house, ...(relay === undefined ? {} : { relay }), credential: values.key });
27
+ await placeAppGit(before, await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
28
+ if (before !== null && before.credential !== values.key) {
29
+ process.stdout.write('This replaced the key this Kit home held; give each session its own HOUSE_KIT_HOME to keep its own key.\n');
30
+ }
31
+ process.stdout.write(`house now calls ${house} as you, with the key kept in ${kitHome()}.\n`);
32
+ }
package/dist/clis.js CHANGED
@@ -1,57 +1,87 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
3
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { constants, readFileSync } from 'node:fs';
4
+ import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
5
  import { homedir, userInfo } from 'node:os';
5
- import { join } from 'node:path';
6
+ import { delimiter, dirname, isAbsolute, join } from 'node:path';
6
7
  import { kitHome } from './home.js';
7
- const RUNNING_JOB = new Set(['running', 'paused']);
8
- async function allowHouseInCodex() {
8
+ const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
9
+ function houseRule(houses) {
10
+ return `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`;
11
+ }
12
+ const RAW_SUBTYPES = [
13
+ 'background_tasks_changed',
14
+ 'task_started',
15
+ 'task_updated',
16
+ 'task_notification',
17
+ 'session_state_changed',
18
+ 'hook_started',
19
+ 'hook_response',
20
+ ];
21
+ async function executable(path) {
22
+ try {
23
+ await access(path, constants.X_OK);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ async function listedHouses(file) {
31
+ let text;
32
+ try {
33
+ text = await readFile(file, 'utf8');
34
+ }
35
+ catch (error) {
36
+ if (error.code === 'ENOENT')
37
+ return [];
38
+ throw error;
39
+ }
40
+ try {
41
+ const listed = JSON.parse(HOUSE_EXECUTABLES.exec(text)?.[1] ?? '[]');
42
+ return Array.isArray(listed) ? listed.filter((house) => typeof house === 'string') : [];
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ }
48
+ export async function allowHouseInCodex(path) {
49
+ const candidates = [...new Set(path.split(delimiter).filter((directory) => isAbsolute(directory)))].map((directory) => join(directory, 'house'));
9
50
  const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
51
+ const file = join(rules, 'house.rules');
52
+ const houses = [];
53
+ for (const house of [...(await listedHouses(file)), ...candidates]) {
54
+ if (!houses.includes(house) && (await executable(house)))
55
+ houses.push(house);
56
+ }
10
57
  await mkdir(rules, { recursive: true });
11
- await writeFile(join(rules, 'house.rules'), 'prefix_rule(pattern = ["house"], decision = "allow")\n');
58
+ const temporary = `${file}.${randomUUID()}`;
59
+ await writeFile(temporary, houseRule(houses));
60
+ await rename(temporary, file);
12
61
  }
13
62
  async function nothing() { }
14
- function threadStatus({ method, params }) {
15
- if (method !== 'session/update' || params?.update?.sessionUpdate !== 'session_info_update')
16
- return undefined;
17
- return params.update._meta?.codex?.threadStatus?.type;
18
- }
19
- function asyncTask({ method, params }) {
20
- const update = params?.update;
21
- if (method !== 'session/update' || update?.asyncTaskId === undefined)
22
- return null;
23
- if (update.sessionUpdate === 'async_task_spawned')
24
- return { id: update.asyncTaskId, running: true };
25
- if (update.sessionUpdate === 'async_task_state_update' && !RUNNING_JOB.has(update.state ?? '')) {
26
- return { id: update.asyncTaskId, running: false };
27
- }
28
- return null;
29
- }
30
- function airPhase({ _meta }) {
31
- const phase = _meta?.jetbrains?.air?.phase;
32
- if (phase === 'commentary')
33
- return 'note';
34
- if (phase === 'final_answer')
35
- return 'answer';
36
- return null;
63
+ function bridgeInClaudeSandbox(bridge) {
64
+ return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
37
65
  }
38
66
  export const CLIS = {
39
67
  'codex-acp': {
40
68
  bin: 'codex',
41
69
  minimum: '0.159.1',
42
70
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
43
- adapter: { package: '@agentclientprotocol/codex-acp', bin: 'codex-acp', executable: 'CODEX_PATH', env: {} },
71
+ adapter: {
72
+ package: '@agentclientprotocol/codex-acp',
73
+ bin: 'codex-acp',
74
+ executable: 'CODEX_PATH',
75
+ env: { CODEX_CONFIG: JSON.stringify({ features: { shell_zsh_fork: true } }) },
76
+ },
44
77
  args: [],
45
78
  fullAccess: 'agent-full-access',
46
- sessionMeta: {},
79
+ sessionMeta: () => ({}),
80
+ renames: true,
47
81
  allowHouse: allowHouseInCodex,
48
82
  signedIn: { command: ['login', 'status'] },
49
83
  login: { args: ['login', '--device-auth'], code: 'show' },
50
- queues: false,
51
- turnStarted: (notice) => threadStatus(notice) === 'active',
52
- turnEnded: (notice) => threadStatus(notice) === 'idle',
53
- job: asyncTask,
54
- phase: airPhase,
84
+ air: ['asyncTasks', 'nativeSubagentSessions'],
55
85
  },
56
86
  'claude-agent-acp': {
57
87
  bin: 'claude',
@@ -65,40 +95,36 @@ export const CLIS = {
65
95
  },
66
96
  args: [],
67
97
  fullAccess: 'bypassPermissions',
68
- sessionMeta: { claudeCode: { options: { allowedTools: ['Bash(house:*)'] } } },
98
+ sessionMeta: (bridge, title) => ({
99
+ claudeCode: {
100
+ options: {
101
+ allowedTools: ['Bash(house:*)'],
102
+ includeHookEvents: true,
103
+ title,
104
+ settings: { sandbox: { network: bridgeInClaudeSandbox(bridge) } },
105
+ },
106
+ emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
107
+ },
108
+ }),
109
+ renames: false,
69
110
  allowHouse: nothing,
70
111
  signedIn: { command: ['auth', 'status'] },
71
112
  login: { args: ['auth', 'login', '--claudeai'], code: 'collect', rejected: 'Invalid code' },
72
- queues: false,
73
- turnStarted: () => false,
74
- turnEnded: ({ method, params }) => method === 'session/update' &&
75
- params?.update?.sessionUpdate === 'usage_update' &&
76
- params.update._meta?.['_claude/origin'] !== undefined,
77
- job: asyncTask,
78
- phase: () => null,
113
+ air: ['asyncTasks', 'sessionFailure'],
79
114
  },
80
115
  'grok-build': {
81
116
  bin: 'grok',
82
117
  minimum: '1.0.46',
83
118
  install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
84
119
  adapter: null,
85
- args: ['agent', '--always-approve', '--no-leader', 'stdio'],
120
+ args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
86
121
  fullAccess: null,
87
- sessionMeta: {},
122
+ sessionMeta: () => ({}),
123
+ renames: false,
88
124
  allowHouse: nothing,
89
125
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
90
126
  login: { args: ['login', '--device-auth'], code: 'show' },
91
- queues: true,
92
- turnStarted: ({ method, params }) => method === '_x.ai/queue/changed' && params?.runningPromptId !== undefined,
93
- turnEnded: ({ method, params }) => method === '_x.ai/session_notification' && params?.update?.sessionUpdate === 'turn_completed',
94
- job: ({ method, params }) => {
95
- if (method === '_x.ai/task_backgrounded')
96
- return { id: params.update.task_id, running: true };
97
- if (method === '_x.ai/task_completed')
98
- return { id: params.update.task_snapshot.task_id, running: false };
99
- return null;
100
- },
101
- phase: () => null,
127
+ air: ['asyncTasks'],
102
128
  },
103
129
  };
104
130
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
@@ -135,8 +161,22 @@ export function run(command, args, timeoutMs, detached = false, signal) {
135
161
  timeout: timeoutMs,
136
162
  env: withoutProxy(),
137
163
  detached,
138
- signal,
164
+ signal: detached ? undefined : signal,
139
165
  });
166
+ const stop = () => {
167
+ try {
168
+ process.kill(-child.pid, 'SIGKILL');
169
+ }
170
+ catch { }
171
+ };
172
+ if (detached && signal?.aborted)
173
+ stop();
174
+ if (detached)
175
+ signal?.addEventListener('abort', stop, { once: true });
176
+ const ended = (ran) => {
177
+ signal?.removeEventListener('abort', stop);
178
+ resolve(ran);
179
+ };
140
180
  let stdout = '';
141
181
  let output = '';
142
182
  child.stdout.setEncoding('utf8').on('data', (chunk) => {
@@ -146,25 +186,25 @@ export function run(command, args, timeoutMs, detached = false, signal) {
146
186
  child.stderr.setEncoding('utf8').on('data', (chunk) => {
147
187
  output += chunk;
148
188
  });
149
- child.on('error', (error) => resolve({ status: null, stdout, output: error.message }));
150
- child.on('close', (status) => resolve({ status, stdout, output }));
189
+ child.on('error', (error) => ended({ status: null, stdout, output: error.message }));
190
+ child.on('close', (status) => ended({ status, stdout, output }));
151
191
  });
152
192
  }
153
193
  export function failureOf(ran, program) {
154
194
  return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
155
195
  }
156
- export async function installAdapter(kind) {
196
+ export async function installAdapter(kind, signal) {
157
197
  const adapter = CLIS[kind].adapter;
158
198
  if (adapter === null)
159
199
  return null;
160
200
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
161
201
  if ((await installedAdapter(kind)) === release)
162
202
  return null;
163
- const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS);
203
+ const ran = await run('npm', ['install', '--prefix', prefix(kind), '--cache', join(kitHome(), 'npm-cache'), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
164
204
  return ran.status === 0 ? null : failureOf(ran, 'npm');
165
205
  }
166
- export async function installCli(kind) {
167
- const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true);
206
+ export async function installCli(kind, signal) {
207
+ const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
168
208
  return ran.status === 0 ? null : failureOf(ran, 'the install');
169
209
  }
170
210
  function loginShell() {
@@ -173,11 +213,11 @@ function loginShell() {
173
213
  const FOUND = 'house-kit-cli ';
174
214
  const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
175
215
  const USER_BIN = 'case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) PATH="$PATH:$HOME/.local/bin" ;; esac; export PATH';
176
- function inLoginShell(script, argument = '') {
177
- return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true);
216
+ function inLoginShell(script, argument = '', signal) {
217
+ return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
178
218
  }
179
- export async function locate(kind) {
180
- const ran = await inLoginShell(LOOKUP, CLIS[kind].bin);
219
+ export async function locate(kind, signal) {
220
+ const ran = await inLoginShell(LOOKUP, CLIS[kind].bin, signal);
181
221
  return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
182
222
  }
183
223
  export async function loginPath() {
@@ -185,8 +225,14 @@ export async function loginPath() {
185
225
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
186
226
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
187
227
  }
188
- export async function versionOf(path) {
189
- return VERSION.exec((await run(path, ['--version'], READ_MS)).stdout)?.[0] ?? null;
228
+ export async function writeCodexRule() {
229
+ const shell = await loginPath().catch(() => '');
230
+ await allowHouseInCodex([dirname(process.execPath), process.env.PATH ?? '', shell].join(delimiter)).catch((error) => {
231
+ throw new Error(`Codex's rule for house could not be written: ${error.message}`);
232
+ });
233
+ }
234
+ export async function versionOf(path, signal) {
235
+ return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
190
236
  }
191
237
  function numbers(version) {
192
238
  return version.split(/[.-]/, 3).map(Number);
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { connect } from 'node:net';
4
+ const KIT_ANSWER = '"house-kit-';
5
+ const SYNC = 'house-kit/sync';
6
+ function lines(each) {
7
+ let buffer = '';
8
+ return (chunk) => {
9
+ buffer += chunk;
10
+ for (let index = buffer.indexOf('\n'); index >= 0; index = buffer.indexOf('\n')) {
11
+ each(buffer.slice(0, index + 1));
12
+ buffer = buffer.slice(index + 1);
13
+ }
14
+ };
15
+ }
16
+ function answersKit(line) {
17
+ if (!line.includes(KIT_ANSWER))
18
+ return false;
19
+ try {
20
+ const message = JSON.parse(line);
21
+ return message.method === undefined && typeof message.id === 'string' && message.id.startsWith(KIT_ANSWER.slice(1));
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ const args = process.argv.slice(2);
28
+ const serving = args[0] === 'app-server';
29
+ if (process.env.HOUSE_KIT_SANDBOX === '1' && serving)
30
+ args.push('--disable', 'memories');
31
+ const codex = spawn(process.env.HOUSE_KIT_CODEX, args, { stdio: ['pipe', 'pipe', 'inherit'] });
32
+ const stream = serving ? connect(process.env.HOUSE_KIT_CODEX_STREAM) : null;
33
+ stream?.on('error', () => undefined);
34
+ process.stdin.setEncoding('utf8').on('data', lines((line) => codex.stdin.write(line)));
35
+ process.stdin.on('end', () => codex.stdin.end());
36
+ stream?.setEncoding('utf8').on('data', lines((line) => {
37
+ const message = JSON.parse(line);
38
+ if (message.method === SYNC)
39
+ stream.write(`${JSON.stringify({ jsonrpc: '2.0', id: message.id, result: {} })}\n`);
40
+ else
41
+ codex.stdin.write(line);
42
+ }));
43
+ codex.stdout.setEncoding('utf8').on('data', lines((line) => {
44
+ stream?.write(line);
45
+ if (answersKit(line) || process.stdout.write(line))
46
+ return;
47
+ codex.stdout.pause();
48
+ process.stdout.once('drain', () => codex.stdout.resume());
49
+ }));
50
+ codex.stdin.on('error', () => undefined);
51
+ for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP'])
52
+ process.on(signal, () => codex.kill(signal));
53
+ codex.on('exit', (code, signal) => {
54
+ if (signal === null)
55
+ process.exit(code ?? 1);
56
+ process.removeAllListeners(signal);
57
+ process.kill(process.pid, signal);
58
+ });