@agentshouse/kit 0.2.1-cb6679d048e60 → 0.2.1-cbdf76b9a90a3

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 (104) hide show
  1. package/README.md +32 -31
  2. package/dist/acp.js +46 -103
  3. package/dist/agents.js +178 -49
  4. package/dist/api.js +36 -3
  5. package/dist/app-git.js +48 -0
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +39 -58
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +137 -65
  10. package/dist/codex-main.js +58 -0
  11. package/dist/conversations.js +525 -287
  12. package/dist/copies.js +215 -146
  13. package/dist/end.js +27 -0
  14. package/dist/enrol-main.js +7 -1
  15. package/dist/events.js +525 -0
  16. package/dist/files.js +24 -3
  17. package/dist/git.js +10 -8
  18. package/dist/home.js +96 -7
  19. package/dist/house-cli.js +291 -0
  20. package/dist/house-main.js +7 -159
  21. package/dist/inputs.js +63 -0
  22. package/dist/instructions.js +10 -32
  23. package/dist/kit-cli.js +68 -0
  24. package/dist/kit-main.js +8 -19
  25. package/dist/login.js +118 -103
  26. package/dist/logout.js +14 -13
  27. package/dist/mcp.js +2 -1
  28. package/dist/parts.js +82 -0
  29. package/dist/placement.js +39 -0
  30. package/dist/refusals.js +12 -0
  31. package/dist/relay.js +20 -0
  32. package/dist/resident.js +91 -35
  33. package/dist/scope.js +206 -0
  34. package/dist/secret-input.js +4 -1
  35. package/dist/service-main.js +9 -0
  36. package/dist/service.js +55 -0
  37. package/dist/shell.js +3 -0
  38. package/dist/sign-in.js +34 -27
  39. package/dist/skills.js +81 -12
  40. package/dist/stream.js +89 -12
  41. package/dist/transcription.js +57 -9
  42. package/dist/translate.js +2 -8
  43. package/dist/update.js +50 -29
  44. package/dist/wire.js +22 -0
  45. package/dist/work.js +346 -0
  46. package/how-we-work.md +13 -3
  47. package/package.json +3 -2
  48. package/skills/ask-matt/SKILL.md +17 -11
  49. package/skills/ask-matt/agents/openai.yaml +1 -0
  50. package/skills/code-review/SKILL.md +5 -5
  51. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  52. package/skills/codebase-design/SKILL.md +0 -2
  53. package/skills/diagnosing-bugs/SKILL.md +2 -4
  54. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  55. package/skills/domain-modeling/SKILL.md +12 -14
  56. package/skills/grill-me/SKILL.md +1 -2
  57. package/skills/grill-me/agents/openai.yaml +1 -0
  58. package/skills/grill-with-docs/SKILL.md +1 -2
  59. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  60. package/skills/grilling/SKILL.md +2 -2
  61. package/skills/handoff/SKILL.md +2 -3
  62. package/skills/handoff/agents/openai.yaml +1 -0
  63. package/skills/implement/SKILL.md +5 -4
  64. package/skills/implement/agents/openai.yaml +1 -0
  65. package/skills/implement-spec/SKILL.md +40 -0
  66. package/skills/implement-spec/agents/openai.yaml +5 -0
  67. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  68. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  69. package/skills/pr/CREDITS.md +3 -0
  70. package/skills/pr/LICENSE +21 -0
  71. package/skills/pr/SKILL.md +170 -0
  72. package/skills/pr/agents/openai.yaml +3 -0
  73. package/skills/prototype/SKILL.md +0 -2
  74. package/skills/research/SKILL.md +0 -2
  75. package/skills/retro/LICENSE +21 -0
  76. package/skills/retro/SKILL.md +44 -0
  77. package/skills/retro/agents/openai.yaml +5 -0
  78. package/skills/tdd/SKILL.md +2 -4
  79. package/skills/teach/SKILL.md +3 -4
  80. package/skills/teach/agents/openai.yaml +1 -0
  81. package/skills/to-questionnaire/SKILL.md +1 -2
  82. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  83. package/skills/to-spec/SKILL.md +1 -2
  84. package/skills/to-spec/agents/openai.yaml +1 -0
  85. package/skills/to-tickets/SKILL.md +3 -4
  86. package/skills/to-tickets/agents/openai.yaml +1 -0
  87. package/skills/triage/AGENT-BRIEF.md +207 -0
  88. package/skills/triage/LICENSE +21 -0
  89. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  90. package/skills/triage/SKILL.md +112 -0
  91. package/skills/triage/agents/openai.yaml +5 -0
  92. package/skills/wait-what/SKILL.md +2 -3
  93. package/skills/wait-what/agents/openai.yaml +1 -0
  94. package/skills/wayfinder/SKILL.md +5 -6
  95. package/skills/wayfinder/agents/openai.yaml +1 -0
  96. package/skills/wizard/LICENSE +21 -0
  97. package/skills/wizard/SKILL.md +44 -0
  98. package/skills/wizard/agents/openai.yaml +3 -0
  99. package/skills/wizard/template.sh +223 -0
  100. package/skills/writing-for-agents/SKILL.md +0 -2
  101. package/dist/blocks.js +0 -51
  102. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  103. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  104. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/bridge.js CHANGED
@@ -1,20 +1,19 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { once } from 'node:events';
3
3
  import { setTimeout as delay } from 'node:timers/promises';
4
- import { mkdir } from 'node:fs/promises';
5
4
  import { createServer, request as httpRequest } from 'node:http';
6
5
  import { request as httpsRequest } from 'node:https';
7
6
  import { delimiter, dirname, join } from 'node:path';
8
7
  import { parse } from 'yaml';
9
- import { HouseRefusal, retryDelay } from './api.js';
8
+ import { finalRefusal, HouseRefusal, retryDelay, retrying } from './api.js';
10
9
  import { loginPath } from './clis.js';
11
10
  import { changedPaths, } from './copies.js';
12
- import { kitHome, readEnrolment } from './home.js';
11
+ import { kitHome, readEnrolment, socketDirectory } from './home.js';
13
12
  import { callHouse, mcpBody, rpc, UNREACHABLE } from './mcp.js';
14
- import { operationId, uuidOf } from './operation.js';
15
- import { refusalOf } from './refusals.js';
13
+ import { operationId } from './operation.js';
14
+ import { refusalOf, retryable } from './refusals.js';
15
+ import { relayed } from './relay.js';
16
16
  const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
17
- const ATTACHMENTS = '/kit/attachments/upload';
18
17
  export async function received(request) {
19
18
  let text = '';
20
19
  request.setEncoding('utf8');
@@ -22,7 +21,8 @@ export async function received(request) {
22
21
  text += chunk;
23
22
  return text;
24
23
  }
25
- function relay(request, response, target, authorization) {
24
+ function relay(request, response, house, authorization) {
25
+ const target = relayed(house);
26
26
  const send = target.protocol === 'https:' ? httpsRequest : httpRequest;
27
27
  const forwarded = send(target, {
28
28
  method: request.method,
@@ -38,8 +38,6 @@ function refusalIn(text, status) {
38
38
  return refusalOf(text) ?? { code: '', text: text === '' ? `House refused it with ${status}` : text, conflicts: [] };
39
39
  }
40
40
  function answered(forwarded) {
41
- if (forwarded.status >= 500)
42
- throw new HouseRefusal('/', forwarded.status, forwarded.text);
43
41
  if (forwarded.status !== 200)
44
42
  return { refused: refusalIn(forwarded.text, forwarded.status) };
45
43
  const answer = JSON.parse(forwarded.text);
@@ -63,16 +61,11 @@ function pendingSeconds(forwarded) {
63
61
  function resendAfter(forwarded, attempt) {
64
62
  if (forwarded === null)
65
63
  return retryDelay(attempt);
66
- if (forwarded.status >= 500)
67
- return refusalOf(forwarded.text) === null ? retryDelay(attempt) : null;
64
+ if (forwarded.status !== 200)
65
+ return retryable(forwarded.text) === false ? null : retryDelay(attempt);
68
66
  const seconds = pendingSeconds(forwarded);
69
67
  return seconds === null ? null : seconds * 1000;
70
68
  }
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
69
  function editAnswer(forwarded) {
77
70
  const answer = answered(forwarded);
78
71
  if ('refused' in answer)
@@ -88,7 +81,7 @@ function editAnswer(forwarded) {
88
81
  export async function openBridge(house, copies, conversationId, signal) {
89
82
  const path = [join(kitHome(), 'shim'), dirname(process.execPath), await loginPath()].join(delimiter);
90
83
  const origin = (await readEnrolment()).house;
91
- const { credential } = await house.post(`/kit/conversations/${conversationId}/credential`, {}, signal);
84
+ const { credential } = await retrying(() => house.post(`/kit/conversations/${conversationId}/credential`, {}, signal), signal);
92
85
  const closed = new AbortController();
93
86
  const sent = (message, operation) => callHouse(origin, credential, message, operation, closed.signal);
94
87
  const send = async (message, operation) => {
@@ -103,6 +96,7 @@ export async function openBridge(house, copies, conversationId, signal) {
103
96
  const after = resendAfter(forwarded, attempt);
104
97
  if (after === null)
105
98
  return forwarded;
99
+ // A Tool call House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
106
100
  await delay(after, undefined, { signal: closed.signal });
107
101
  }
108
102
  };
@@ -117,46 +111,32 @@ export async function openBridge(house, copies, conversationId, signal) {
117
111
  if ('refused' in prepared)
118
112
  return prepared;
119
113
  const { transfer } = parse(prepared.text);
120
- const posted = await fetch(transfer.url, {
121
- method: 'POST',
122
- headers: { 'content-type': 'text/plain; charset=utf-8' },
123
- body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
124
- signal: closed.signal,
125
- });
126
- const text = await posted.text();
127
- if (posted.status >= 500)
128
- throw new HouseRefusal('/', posted.status, text);
129
- if (!posted.ok)
130
- return { refused: refusalIn(text, posted.status) };
131
- const held = JSON.parse(text).transfer;
132
- return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
133
- };
134
- let mutations = null;
135
- const writes = async (tool) => {
136
- mutations ??= house
137
- .post('/kit/tools/mutations', {}, closed.signal)
138
- .then(({ tools }) => new Set(tools.filter((listed) => listed.writes).map((listed) => listed.name)));
114
+ let text;
139
115
  try {
140
- return (await mutations).has(tool);
116
+ text = await retrying(async () => {
117
+ const posted = await fetch(relayed(transfer.url), {
118
+ method: 'POST',
119
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
120
+ body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
121
+ signal: closed.signal,
122
+ });
123
+ const answer = await posted.text();
124
+ if (!posted.ok)
125
+ throw new HouseRefusal('/', posted.status, answer);
126
+ return answer;
127
+ }, closed.signal);
141
128
  }
142
129
  catch (error) {
143
- mutations = null;
144
- throw error;
130
+ if (!finalRefusal(error))
131
+ throw error;
132
+ return { refused: refusalIn(error.text, error.status) };
145
133
  }
134
+ const held = JSON.parse(text).transfer;
135
+ return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
146
136
  };
147
- const forward = async (path, text) => {
148
- if (path === '/') {
149
- const message = JSON.parse(text);
150
- const tool = message.method === 'tools/call' ? String(message.params.name) : null;
151
- return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
152
- }
153
- const answer = await fetch(new URL(path, origin), {
154
- method: 'POST',
155
- headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
156
- body: versioned(conversationId, text),
157
- signal: closed.signal,
158
- });
159
- return { status: answer.status, text: await answer.text() };
137
+ const forward = (text) => {
138
+ const message = JSON.parse(text);
139
+ return send(message, message.method === 'tools/call' ? operationId() : null);
160
140
  };
161
141
  const caller = {
162
142
  key: `conversation:${conversationId}`,
@@ -178,15 +158,15 @@ export async function openBridge(house, copies, conversationId, signal) {
178
158
  const { cwd, commit } = JSON.parse(text);
179
159
  return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
180
160
  }
161
+ if (path === '/git/fetch')
162
+ return { status: 200, text: JSON.stringify(await copies.fetch()) };
181
163
  if (path === '/git/observed') {
182
164
  await copies.observed(JSON.parse(text));
183
165
  return { status: 200, text: '{}' };
184
166
  }
185
- return path === '/' || path === ATTACHMENTS ? forward(path, text) : { status: 404, text: '' };
167
+ return path === '/' ? forward(text) : { status: 404, text: '' };
186
168
  };
187
- const sockets = join(kitHome(), 'bridges');
188
- await mkdir(sockets, { recursive: true, mode: 0o700 });
189
- const socketPath = join(sockets, `${randomUUID()}.sock`);
169
+ const socketPath = join(socketDirectory(), `${randomUUID()}.sock`);
190
170
  const bridge = createServer(async (request, response) => {
191
171
  try {
192
172
  const path = request.url ?? '';
@@ -202,7 +182,8 @@ export async function openBridge(house, copies, conversationId, signal) {
202
182
  response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
203
183
  return;
204
184
  }
205
- response.writeHead(502).end(request.url === '/git/push' && error instanceof Error ? error.message : UNREACHABLE);
185
+ const verb = request.url === '/git/push' || request.url === '/git/fetch';
186
+ response.writeHead(502).end(verb && error instanceof Error ? error.message : UNREACHABLE);
206
187
  }
207
188
  });
208
189
  bridge.listen(socketPath);
@@ -229,7 +210,7 @@ export async function openBridge(house, copies, conversationId, signal) {
229
210
  GIT_CONFIG_VALUE_0: app,
230
211
  },
231
212
  tool: async (name, args) => {
232
- const forwarded = await forward('/', JSON.stringify(call(name, args)));
213
+ const forwarded = await forward(JSON.stringify(call(name, args)));
233
214
  if (forwarded.status !== 200)
234
215
  throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
235
216
  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(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,48 +1,94 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
3
- import { readFile } from 'node:fs/promises';
4
- import { userInfo } from 'node:os';
5
- import { join } from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { constants, readFileSync } from 'node:fs';
4
+ import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
5
+ import { homedir, userInfo } from 'node:os';
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
- function threadStatus({ method, params }) {
9
- if (method !== 'session/update' || params?.update?.sessionUpdate !== 'session_info_update')
10
- return undefined;
11
- return params.update._meta?.codex?.threadStatus?.type;
12
- }
13
- function asyncTask({ method, params }) {
14
- const update = params?.update;
15
- if (method !== 'session/update' || update?.asyncTaskId === undefined)
16
- return null;
17
- if (update.sessionUpdate === 'async_task_spawned')
18
- return { id: update.asyncTaskId, running: true };
19
- if (update.sessionUpdate === 'async_task_state_update' && !RUNNING_JOB.has(update.state ?? '')) {
20
- return { id: update.asyncTaskId, running: false };
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') : [];
21
43
  }
22
- return null;
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'));
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
+ }
57
+ await mkdir(rules, { recursive: true });
58
+ const temporary = `${file}.${randomUUID()}`;
59
+ await writeFile(temporary, houseRule(houses));
60
+ await rename(temporary, file);
61
+ }
62
+ async function nothing() { }
63
+ function answered(error) {
64
+ return typeof error === 'object' && error !== null ? error : {};
23
65
  }
24
- function airPhase({ _meta }) {
25
- const phase = _meta?.jetbrains?.air?.phase;
26
- if (phase === 'commentary')
27
- return 'note';
28
- if (phase === 'final_answer')
29
- return 'answer';
30
- return null;
66
+ function bridgeInClaudeSandbox(bridge) {
67
+ return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
31
68
  }
32
69
  export const CLIS = {
33
70
  'codex-acp': {
34
71
  bin: 'codex',
35
72
  minimum: '0.159.1',
36
73
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
37
- adapter: { package: '@agentclientprotocol/codex-acp', bin: 'codex-acp', executable: 'CODEX_PATH', env: {} },
74
+ adapter: {
75
+ package: '@agentclientprotocol/codex-acp',
76
+ bin: 'codex-acp',
77
+ executable: 'CODEX_PATH',
78
+ env: { CODEX_CONFIG: JSON.stringify({ features: { shell_zsh_fork: true } }) },
79
+ },
38
80
  args: [],
81
+ fullAccess: 'agent-full-access',
82
+ sessionMeta: () => ({}),
83
+ unknownSession: (error) => {
84
+ const { code, data } = answered(error);
85
+ return code === -32603 && typeof data?.details === 'string' && data.details.startsWith('no rollout found');
86
+ },
87
+ renames: true,
88
+ allowHouse: allowHouseInCodex,
39
89
  signedIn: { command: ['login', 'status'] },
40
90
  login: { args: ['login', '--device-auth'], code: 'show' },
41
- queues: false,
42
- turnStarted: (notice) => threadStatus(notice) === 'active',
43
- turnEnded: (notice) => threadStatus(notice) === 'idle',
44
- job: asyncTask,
45
- phase: airPhase,
91
+ air: ['asyncTasks', 'nativeSubagentSessions'],
46
92
  },
47
93
  'claude-agent-acp': {
48
94
  bin: 'claude',
@@ -52,45 +98,51 @@ export const CLIS = {
52
98
  package: '@agentclientprotocol/claude-agent-acp',
53
99
  bin: 'claude-agent-acp',
54
100
  executable: 'CLAUDE_CODE_EXECUTABLE',
55
- env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp' },
101
+ env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp', IS_SANDBOX: '1' },
56
102
  },
57
103
  args: [],
104
+ fullAccess: 'bypassPermissions',
105
+ sessionMeta: (bridge, title) => ({
106
+ claudeCode: {
107
+ options: {
108
+ allowedTools: ['Bash(house:*)'],
109
+ includeHookEvents: true,
110
+ title,
111
+ settings: { sandbox: { network: bridgeInClaudeSandbox(bridge) } },
112
+ },
113
+ emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
114
+ },
115
+ }),
116
+ unknownSession: (error) => answered(error).code === -32002,
117
+ renames: false,
118
+ allowHouse: nothing,
58
119
  signedIn: { command: ['auth', 'status'] },
59
120
  login: { args: ['auth', 'login', '--claudeai'], code: 'collect', rejected: 'Invalid code' },
60
- queues: false,
61
- turnStarted: () => false,
62
- turnEnded: ({ method, params }) => method === 'session/update' &&
63
- params?.update?.sessionUpdate === 'usage_update' &&
64
- params.update._meta?.['_claude/origin'] !== undefined,
65
- job: asyncTask,
66
- phase: () => null,
121
+ air: ['asyncTasks', 'sessionFailure'],
67
122
  },
68
123
  'grok-build': {
69
124
  bin: 'grok',
70
125
  minimum: '1.0.46',
71
126
  install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
72
127
  adapter: null,
73
- args: ['agent', '--no-leader', 'stdio'],
128
+ args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
129
+ fullAccess: null,
130
+ sessionMeta: () => ({}),
131
+ unknownSession: (error) => answered(error).data?.code === 'FS_NOT_FOUND',
132
+ renames: false,
133
+ allowHouse: nothing,
74
134
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
75
135
  login: { args: ['login', '--device-auth'], code: 'show' },
76
- queues: true,
77
- turnStarted: ({ method, params }) => method === '_x.ai/queue/changed' && params?.runningPromptId !== undefined,
78
- turnEnded: ({ method, params }) => method === '_x.ai/session_notification' && params?.update?.sessionUpdate === 'turn_completed',
79
- job: ({ method, params }) => {
80
- if (method === '_x.ai/task_backgrounded')
81
- return { id: params.update.task_id, running: true };
82
- if (method === '_x.ai/task_completed')
83
- return { id: params.update.task_snapshot.task_id, running: false };
84
- return null;
85
- },
86
- phase: () => null,
136
+ air: ['asyncTasks'],
87
137
  },
88
138
  };
89
139
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
90
140
  export const KIT_VERSION = KIT_PACKAGE.version;
91
141
  const PROXY = /^https?_proxy$/i;
92
142
  const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
143
+ // An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
93
144
  const INSTALL_MS = 15 * 60_000;
145
+ // A login shell or a version read is ended after half a minute, so a profile that waits for input cannot stall the report.
94
146
  const READ_MS = 30_000;
95
147
  const CAUSE_CHARACTERS = 4000;
96
148
  function prefix(kind) {
@@ -118,8 +170,22 @@ export function run(command, args, timeoutMs, detached = false, signal) {
118
170
  timeout: timeoutMs,
119
171
  env: withoutProxy(),
120
172
  detached,
121
- signal,
173
+ signal: detached ? undefined : signal,
122
174
  });
175
+ const stop = () => {
176
+ try {
177
+ process.kill(-child.pid, 'SIGKILL');
178
+ }
179
+ catch { }
180
+ };
181
+ if (detached && signal?.aborted)
182
+ stop();
183
+ if (detached)
184
+ signal?.addEventListener('abort', stop, { once: true });
185
+ const ended = (ran) => {
186
+ signal?.removeEventListener('abort', stop);
187
+ resolve(ran);
188
+ };
123
189
  let stdout = '';
124
190
  let output = '';
125
191
  child.stdout.setEncoding('utf8').on('data', (chunk) => {
@@ -129,25 +195,25 @@ export function run(command, args, timeoutMs, detached = false, signal) {
129
195
  child.stderr.setEncoding('utf8').on('data', (chunk) => {
130
196
  output += chunk;
131
197
  });
132
- child.on('error', (error) => resolve({ status: null, stdout, output: error.message }));
133
- child.on('close', (status) => resolve({ status, stdout, output }));
198
+ child.on('error', (error) => ended({ status: null, stdout, output: error.message }));
199
+ child.on('close', (status) => ended({ status, stdout, output }));
134
200
  });
135
201
  }
136
202
  export function failureOf(ran, program) {
137
203
  return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
138
204
  }
139
- export async function installAdapter(kind) {
205
+ export async function installAdapter(kind, signal) {
140
206
  const adapter = CLIS[kind].adapter;
141
207
  if (adapter === null)
142
208
  return null;
143
209
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
144
210
  if ((await installedAdapter(kind)) === release)
145
211
  return null;
146
- const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS);
212
+ 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);
147
213
  return ran.status === 0 ? null : failureOf(ran, 'npm');
148
214
  }
149
- export async function installCli(kind) {
150
- const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true);
215
+ export async function installCli(kind, signal) {
216
+ const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
151
217
  return ran.status === 0 ? null : failureOf(ran, 'the install');
152
218
  }
153
219
  function loginShell() {
@@ -156,11 +222,11 @@ function loginShell() {
156
222
  const FOUND = 'house-kit-cli ';
157
223
  const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
158
224
  const USER_BIN = 'case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) PATH="$PATH:$HOME/.local/bin" ;; esac; export PATH';
159
- function inLoginShell(script, argument = '') {
160
- return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true);
225
+ function inLoginShell(script, argument = '', signal) {
226
+ return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
161
227
  }
162
- export async function locate(kind) {
163
- const ran = await inLoginShell(LOOKUP, CLIS[kind].bin);
228
+ export async function locate(kind, signal) {
229
+ const ran = await inLoginShell(LOOKUP, CLIS[kind].bin, signal);
164
230
  return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
165
231
  }
166
232
  export async function loginPath() {
@@ -168,8 +234,14 @@ export async function loginPath() {
168
234
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
169
235
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
170
236
  }
171
- export async function versionOf(path) {
172
- return VERSION.exec((await run(path, ['--version'], READ_MS)).stdout)?.[0] ?? null;
237
+ export async function writeCodexRule() {
238
+ const shell = await loginPath().catch(() => '');
239
+ await allowHouseInCodex([dirname(process.execPath), process.env.PATH ?? '', shell].join(delimiter)).catch((error) => {
240
+ throw new Error(`Codex's rule for house could not be written: ${error.message}`);
241
+ });
242
+ }
243
+ export async function versionOf(path, signal) {
244
+ return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
173
245
  }
174
246
  function numbers(version) {
175
247
  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
+ });