@agentshouse/kit 0.2.1-cd9cbd8b6428c → 0.2.1-cfe80fd914176

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,48 +1,50 @@
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 { constants, readFileSync } from 'node:fs';
3
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { homedir, userInfo } from 'node:os';
5
+ import { delimiter, isAbsolute, join } from 'node:path';
6
6
  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 };
7
+ async function executable(path) {
8
+ try {
9
+ await access(path, constants.X_OK);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
21
14
  }
22
- return null;
23
15
  }
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;
16
+ async function allowHouseInCodex(path) {
17
+ const candidates = [...new Set(path.split(delimiter).filter((directory) => isAbsolute(directory)))].map((directory) => join(directory, 'house'));
18
+ const houses = [];
19
+ for (const candidate of candidates)
20
+ if (await executable(candidate))
21
+ houses.push(candidate);
22
+ const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
23
+ await mkdir(rules, { recursive: true });
24
+ await writeFile(join(rules, 'house.rules'), `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`);
25
+ }
26
+ async function nothing() { }
27
+ function bridgeInClaudeSandbox(bridge) {
28
+ return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
31
29
  }
32
30
  export const CLIS = {
33
31
  'codex-acp': {
34
32
  bin: 'codex',
35
33
  minimum: '0.159.1',
36
34
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
37
- adapter: { package: '@agentclientprotocol/codex-acp', bin: 'codex-acp', executable: 'CODEX_PATH', env: {} },
35
+ adapter: {
36
+ package: '@agentclientprotocol/codex-acp',
37
+ bin: 'codex-acp',
38
+ executable: 'CODEX_PATH',
39
+ env: { CODEX_CONFIG: JSON.stringify({ features: { shell_zsh_fork: true } }) },
40
+ },
38
41
  args: [],
42
+ fullAccess: 'agent-full-access',
43
+ sessionMeta: () => ({}),
44
+ allowHouse: allowHouseInCodex,
39
45
  signedIn: { command: ['login', 'status'] },
40
46
  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,
47
+ air: ['asyncTasks', 'nativeSubagentSessions'],
46
48
  },
47
49
  'claude-agent-acp': {
48
50
  bin: 'claude',
@@ -52,45 +54,45 @@ export const CLIS = {
52
54
  package: '@agentclientprotocol/claude-agent-acp',
53
55
  bin: 'claude-agent-acp',
54
56
  executable: 'CLAUDE_CODE_EXECUTABLE',
55
- env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp' },
57
+ env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp', IS_SANDBOX: '1' },
56
58
  },
57
59
  args: [],
60
+ fullAccess: 'bypassPermissions',
61
+ sessionMeta: (bridge) => ({
62
+ claudeCode: {
63
+ options: {
64
+ allowedTools: ['Bash(house:*)'],
65
+ settings: { sandbox: { network: bridgeInClaudeSandbox(bridge) } },
66
+ },
67
+ emitRawSDKMessages: ['background_tasks_changed', 'session_state_changed', 'task_started', 'task_notification'].map((subtype) => ({ type: 'system', subtype })),
68
+ },
69
+ }),
70
+ allowHouse: nothing,
58
71
  signedIn: { command: ['auth', 'status'] },
59
72
  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,
73
+ air: ['asyncTasks', 'sessionFailure'],
67
74
  },
68
75
  'grok-build': {
69
76
  bin: 'grok',
70
77
  minimum: '1.0.46',
71
78
  install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
72
79
  adapter: null,
73
- args: ['agent', '--no-leader', 'stdio'],
80
+ args: ['agent', '--always-approve', '--no-leader', 'stdio'],
81
+ fullAccess: null,
82
+ sessionMeta: () => ({}),
83
+ allowHouse: nothing,
74
84
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
75
85
  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,
86
+ air: ['asyncTasks'],
87
87
  },
88
88
  };
89
89
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
90
90
  export const KIT_VERSION = KIT_PACKAGE.version;
91
91
  const PROXY = /^https?_proxy$/i;
92
92
  const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
93
+ // An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
93
94
  const INSTALL_MS = 15 * 60_000;
95
+ // 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
96
  const READ_MS = 30_000;
95
97
  const CAUSE_CHARACTERS = 4000;
96
98
  function prefix(kind) {
@@ -118,8 +120,22 @@ export function run(command, args, timeoutMs, detached = false, signal) {
118
120
  timeout: timeoutMs,
119
121
  env: withoutProxy(),
120
122
  detached,
121
- signal,
123
+ signal: detached ? undefined : signal,
122
124
  });
125
+ const stop = () => {
126
+ try {
127
+ process.kill(-child.pid, 'SIGKILL');
128
+ }
129
+ catch { }
130
+ };
131
+ if (detached && signal?.aborted)
132
+ stop();
133
+ if (detached)
134
+ signal?.addEventListener('abort', stop, { once: true });
135
+ const ended = (ran) => {
136
+ signal?.removeEventListener('abort', stop);
137
+ resolve(ran);
138
+ };
123
139
  let stdout = '';
124
140
  let output = '';
125
141
  child.stdout.setEncoding('utf8').on('data', (chunk) => {
@@ -129,25 +145,25 @@ export function run(command, args, timeoutMs, detached = false, signal) {
129
145
  child.stderr.setEncoding('utf8').on('data', (chunk) => {
130
146
  output += chunk;
131
147
  });
132
- child.on('error', (error) => resolve({ status: null, stdout, output: error.message }));
133
- child.on('close', (status) => resolve({ status, stdout, output }));
148
+ child.on('error', (error) => ended({ status: null, stdout, output: error.message }));
149
+ child.on('close', (status) => ended({ status, stdout, output }));
134
150
  });
135
151
  }
136
152
  export function failureOf(ran, program) {
137
153
  return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
138
154
  }
139
- export async function installAdapter(kind) {
155
+ export async function installAdapter(kind, signal) {
140
156
  const adapter = CLIS[kind].adapter;
141
157
  if (adapter === null)
142
158
  return null;
143
159
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
144
160
  if ((await installedAdapter(kind)) === release)
145
161
  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);
162
+ const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
147
163
  return ran.status === 0 ? null : failureOf(ran, 'npm');
148
164
  }
149
- export async function installCli(kind) {
150
- const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true);
165
+ export async function installCli(kind, signal) {
166
+ const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
151
167
  return ran.status === 0 ? null : failureOf(ran, 'the install');
152
168
  }
153
169
  function loginShell() {
@@ -156,11 +172,11 @@ function loginShell() {
156
172
  const FOUND = 'house-kit-cli ';
157
173
  const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
158
174
  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);
175
+ function inLoginShell(script, argument = '', signal) {
176
+ return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
161
177
  }
162
- export async function locate(kind) {
163
- const ran = await inLoginShell(LOOKUP, CLIS[kind].bin);
178
+ export async function locate(kind, signal) {
179
+ const ran = await inLoginShell(LOOKUP, CLIS[kind].bin, signal);
164
180
  return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
165
181
  }
166
182
  export async function loginPath() {
@@ -168,8 +184,8 @@ export async function loginPath() {
168
184
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
169
185
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
170
186
  }
171
- export async function versionOf(path) {
172
- return VERSION.exec((await run(path, ['--version'], READ_MS)).stdout)?.[0] ?? null;
187
+ export async function versionOf(path, signal) {
188
+ return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
173
189
  }
174
190
  function numbers(version) {
175
191
  return version.split(/[.-]/, 3).map(Number);