@agentshouse/kit 0.2.1-ce145e9aadbf6 → 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,57 +1,50 @@
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
+ async function executable(path) {
8
+ try {
9
+ await access(path, constants.X_OK);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
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);
9
22
  const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
10
23
  await mkdir(rules, { recursive: true });
11
- await writeFile(join(rules, 'house.rules'), 'prefix_rule(pattern = ["house"], decision = "allow")\n');
24
+ await writeFile(join(rules, 'house.rules'), `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`);
12
25
  }
13
26
  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;
27
+ function bridgeInClaudeSandbox(bridge) {
28
+ return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
37
29
  }
38
30
  export const CLIS = {
39
31
  'codex-acp': {
40
32
  bin: 'codex',
41
33
  minimum: '0.159.1',
42
34
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
43
- 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
+ },
44
41
  args: [],
45
42
  fullAccess: 'agent-full-access',
46
- sessionMeta: {},
43
+ sessionMeta: () => ({}),
47
44
  allowHouse: allowHouseInCodex,
48
45
  signedIn: { command: ['login', 'status'] },
49
46
  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,
47
+ air: ['asyncTasks', 'nativeSubagentSessions'],
55
48
  },
56
49
  'claude-agent-acp': {
57
50
  bin: 'claude',
@@ -65,17 +58,19 @@ export const CLIS = {
65
58
  },
66
59
  args: [],
67
60
  fullAccess: 'bypassPermissions',
68
- sessionMeta: { claudeCode: { options: { allowedTools: ['Bash(house:*)'] } } },
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
+ }),
69
70
  allowHouse: nothing,
70
71
  signedIn: { command: ['auth', 'status'] },
71
72
  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,
73
+ air: ['asyncTasks', 'sessionFailure'],
79
74
  },
80
75
  'grok-build': {
81
76
  bin: 'grok',
@@ -84,28 +79,20 @@ export const CLIS = {
84
79
  adapter: null,
85
80
  args: ['agent', '--always-approve', '--no-leader', 'stdio'],
86
81
  fullAccess: null,
87
- sessionMeta: {},
82
+ sessionMeta: () => ({}),
88
83
  allowHouse: nothing,
89
84
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
90
85
  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,
86
+ air: ['asyncTasks'],
102
87
  },
103
88
  };
104
89
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
105
90
  export const KIT_VERSION = KIT_PACKAGE.version;
106
91
  const PROXY = /^https?_proxy$/i;
107
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.
108
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.
109
96
  const READ_MS = 30_000;
110
97
  const CAUSE_CHARACTERS = 4000;
111
98
  function prefix(kind) {
@@ -133,8 +120,22 @@ export function run(command, args, timeoutMs, detached = false, signal) {
133
120
  timeout: timeoutMs,
134
121
  env: withoutProxy(),
135
122
  detached,
136
- signal,
123
+ signal: detached ? undefined : signal,
137
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
+ };
138
139
  let stdout = '';
139
140
  let output = '';
140
141
  child.stdout.setEncoding('utf8').on('data', (chunk) => {
@@ -144,25 +145,25 @@ export function run(command, args, timeoutMs, detached = false, signal) {
144
145
  child.stderr.setEncoding('utf8').on('data', (chunk) => {
145
146
  output += chunk;
146
147
  });
147
- child.on('error', (error) => resolve({ status: null, stdout, output: error.message }));
148
- 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 }));
149
150
  });
150
151
  }
151
152
  export function failureOf(ran, program) {
152
153
  return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
153
154
  }
154
- export async function installAdapter(kind) {
155
+ export async function installAdapter(kind, signal) {
155
156
  const adapter = CLIS[kind].adapter;
156
157
  if (adapter === null)
157
158
  return null;
158
159
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
159
160
  if ((await installedAdapter(kind)) === release)
160
161
  return null;
161
- 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);
162
163
  return ran.status === 0 ? null : failureOf(ran, 'npm');
163
164
  }
164
- export async function installCli(kind) {
165
- 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);
166
167
  return ran.status === 0 ? null : failureOf(ran, 'the install');
167
168
  }
168
169
  function loginShell() {
@@ -171,11 +172,11 @@ function loginShell() {
171
172
  const FOUND = 'house-kit-cli ';
172
173
  const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
173
174
  const USER_BIN = 'case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) PATH="$PATH:$HOME/.local/bin" ;; esac; export PATH';
174
- function inLoginShell(script, argument = '') {
175
- 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);
176
177
  }
177
- export async function locate(kind) {
178
- 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);
179
180
  return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
180
181
  }
181
182
  export async function loginPath() {
@@ -183,8 +184,8 @@ export async function loginPath() {
183
184
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
184
185
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
185
186
  }
186
- export async function versionOf(path) {
187
- 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;
188
189
  }
189
190
  function numbers(version) {
190
191
  return version.split(/[.-]/, 3).map(Number);