@agentshouse/kit 0.2.1-cb0bd1cadcfed → 0.2.1-cbabe8ad337ff

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/clis.js CHANGED
@@ -1,57 +1,60 @@
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 { constants, readFileSync } from 'node:fs';
3
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import { homedir, userInfo } from 'node:os';
5
- import { join } from 'node:path';
5
+ import { delimiter, isAbsolute, join } from 'node:path';
6
6
  import { kitHome } from './home.js';
7
- const RUNNING_JOB = new Set(['running', 'paused']);
8
- async function allowHouseInCodex() {
7
+ const RAW_SUBTYPES = [
8
+ 'background_tasks_changed',
9
+ 'task_started',
10
+ 'task_updated',
11
+ 'task_notification',
12
+ 'session_state_changed',
13
+ 'hook_started',
14
+ 'hook_response',
15
+ ];
16
+ async function executable(path) {
17
+ try {
18
+ await access(path, constants.X_OK);
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ async function allowHouseInCodex(path) {
26
+ const candidates = [...new Set(path.split(delimiter).filter((directory) => isAbsolute(directory)))].map((directory) => join(directory, 'house'));
27
+ const houses = [];
28
+ for (const candidate of candidates)
29
+ if (await executable(candidate))
30
+ houses.push(candidate);
9
31
  const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
10
32
  await mkdir(rules, { recursive: true });
11
- await writeFile(join(rules, 'house.rules'), 'prefix_rule(pattern = ["house"], decision = "allow")\n');
33
+ await writeFile(join(rules, 'house.rules'), `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`);
12
34
  }
13
35
  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;
36
+ function bridgeInClaudeSandbox(bridge) {
37
+ return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
37
38
  }
38
39
  export const CLIS = {
39
40
  'codex-acp': {
40
41
  bin: 'codex',
41
42
  minimum: '0.159.1',
42
43
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
43
- adapter: { package: '@agentclientprotocol/codex-acp', bin: 'codex-acp', executable: 'CODEX_PATH', env: {} },
44
+ adapter: {
45
+ package: '@agentclientprotocol/codex-acp',
46
+ bin: 'codex-acp',
47
+ executable: 'CODEX_PATH',
48
+ env: { CODEX_CONFIG: JSON.stringify({ features: { shell_zsh_fork: true } }) },
49
+ },
44
50
  args: [],
45
51
  fullAccess: 'agent-full-access',
46
- sessionMeta: {},
52
+ sessionMeta: () => ({}),
53
+ renames: true,
47
54
  allowHouse: allowHouseInCodex,
48
55
  signedIn: { command: ['login', 'status'] },
49
56
  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,
57
+ air: ['asyncTasks', 'nativeSubagentSessions'],
55
58
  },
56
59
  'claude-agent-acp': {
57
60
  bin: 'claude',
@@ -65,40 +68,36 @@ export const CLIS = {
65
68
  },
66
69
  args: [],
67
70
  fullAccess: 'bypassPermissions',
68
- sessionMeta: { claudeCode: { options: { allowedTools: ['Bash(house:*)'] } } },
71
+ sessionMeta: (bridge, title) => ({
72
+ claudeCode: {
73
+ options: {
74
+ allowedTools: ['Bash(house:*)'],
75
+ includeHookEvents: true,
76
+ title,
77
+ settings: { sandbox: { network: bridgeInClaudeSandbox(bridge) } },
78
+ },
79
+ emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
80
+ },
81
+ }),
82
+ renames: false,
69
83
  allowHouse: nothing,
70
84
  signedIn: { command: ['auth', 'status'] },
71
85
  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,
86
+ air: ['asyncTasks', 'sessionFailure'],
79
87
  },
80
88
  'grok-build': {
81
89
  bin: 'grok',
82
90
  minimum: '1.0.46',
83
91
  install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
84
92
  adapter: null,
85
- args: ['agent', '--always-approve', '--no-leader', 'stdio'],
93
+ args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
86
94
  fullAccess: null,
87
- sessionMeta: {},
95
+ sessionMeta: () => ({}),
96
+ renames: false,
88
97
  allowHouse: nothing,
89
98
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
90
99
  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,
100
+ air: ['asyncTasks'],
102
101
  },
103
102
  };
104
103
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
@@ -135,8 +134,22 @@ export function run(command, args, timeoutMs, detached = false, signal) {
135
134
  timeout: timeoutMs,
136
135
  env: withoutProxy(),
137
136
  detached,
138
- signal,
137
+ signal: detached ? undefined : signal,
139
138
  });
139
+ const stop = () => {
140
+ try {
141
+ process.kill(-child.pid, 'SIGKILL');
142
+ }
143
+ catch { }
144
+ };
145
+ if (detached && signal?.aborted)
146
+ stop();
147
+ if (detached)
148
+ signal?.addEventListener('abort', stop, { once: true });
149
+ const ended = (ran) => {
150
+ signal?.removeEventListener('abort', stop);
151
+ resolve(ran);
152
+ };
140
153
  let stdout = '';
141
154
  let output = '';
142
155
  child.stdout.setEncoding('utf8').on('data', (chunk) => {
@@ -146,25 +159,25 @@ export function run(command, args, timeoutMs, detached = false, signal) {
146
159
  child.stderr.setEncoding('utf8').on('data', (chunk) => {
147
160
  output += chunk;
148
161
  });
149
- child.on('error', (error) => resolve({ status: null, stdout, output: error.message }));
150
- child.on('close', (status) => resolve({ status, stdout, output }));
162
+ child.on('error', (error) => ended({ status: null, stdout, output: error.message }));
163
+ child.on('close', (status) => ended({ status, stdout, output }));
151
164
  });
152
165
  }
153
166
  export function failureOf(ran, program) {
154
167
  return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
155
168
  }
156
- export async function installAdapter(kind) {
169
+ export async function installAdapter(kind, signal) {
157
170
  const adapter = CLIS[kind].adapter;
158
171
  if (adapter === null)
159
172
  return null;
160
173
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
161
174
  if ((await installedAdapter(kind)) === release)
162
175
  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);
176
+ const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
164
177
  return ran.status === 0 ? null : failureOf(ran, 'npm');
165
178
  }
166
- export async function installCli(kind) {
167
- const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true);
179
+ export async function installCli(kind, signal) {
180
+ const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
168
181
  return ran.status === 0 ? null : failureOf(ran, 'the install');
169
182
  }
170
183
  function loginShell() {
@@ -173,11 +186,11 @@ function loginShell() {
173
186
  const FOUND = 'house-kit-cli ';
174
187
  const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
175
188
  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);
189
+ function inLoginShell(script, argument = '', signal) {
190
+ return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
178
191
  }
179
- export async function locate(kind) {
180
- const ran = await inLoginShell(LOOKUP, CLIS[kind].bin);
192
+ export async function locate(kind, signal) {
193
+ const ran = await inLoginShell(LOOKUP, CLIS[kind].bin, signal);
181
194
  return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
182
195
  }
183
196
  export async function loginPath() {
@@ -185,8 +198,8 @@ export async function loginPath() {
185
198
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
186
199
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
187
200
  }
188
- export async function versionOf(path) {
189
- return VERSION.exec((await run(path, ['--version'], READ_MS)).stdout)?.[0] ?? null;
201
+ export async function versionOf(path, signal) {
202
+ return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
190
203
  }
191
204
  function numbers(version) {
192
205
  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
+ });