@agentshouse/kit 0.2.1-c03721a31074a → 0.2.1-c0535b8294007

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/acp.js CHANGED
@@ -1,31 +1,70 @@
1
1
  import { ndJsonStream, PROTOCOL_VERSION, } from '@agentclientprotocol/sdk';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { Readable, Writable } from 'node:stream';
4
- import { CLIS, KIT_RELEASE, adapterCommand, withoutProxy } from './clis.js';
4
+ import { CLIS, KIT_RELEASE, adapterCommand } from './clis.js';
5
+ import { childEnvironment } from './relay.js';
5
6
  import { killTree } from './scope.js';
7
+ import { Silence } from './silence.js';
6
8
  const RETIRED = 'the CLI was retired';
9
+ // Claude Code ends a shell command or a model request after ten minutes, so a CLI that owes the Kit an answer and sends nothing for half an hour has hung.
10
+ export const CLI_SILENCE_MS = 30 * 60_000;
11
+ const SILENT = `the CLI sent nothing for ${CLI_SILENCE_MS / 60_000} minutes`;
7
12
  export async function startAdapter(kind, cli, cwd, app, events = () => undefined, added = {}, signal, scope = null) {
8
13
  const adapter = CLIS[kind].adapter;
9
14
  const started = [adapter === null ? cli : adapterCommand(kind), CLIS[kind].args];
10
15
  const [command, args] = scope?.command(...started) ?? started;
11
16
  const child = spawn(command, args, {
12
17
  cwd,
13
- env: { ...withoutProxy(), ...(adapter === null ? {} : { ...adapter.env, [adapter.executable]: cli }), ...added },
18
+ env: { ...childEnvironment(), ...(adapter === null ? {} : { ...adapter.env, [adapter.executable]: cli }), ...added },
14
19
  stdio: ['pipe', 'pipe', 'pipe'],
15
20
  detached: true,
16
21
  });
17
22
  let owned = true;
18
23
  let retired = false;
24
+ let silent = false;
25
+ let over = false;
26
+ let open = false;
19
27
  let stderr = '';
28
+ const owed = new Set();
29
+ const asked = new Set();
30
+ const silence = new Silence(CLI_SILENCE_MS, () => {
31
+ silent = true;
32
+ connection.close(new Error(SILENT));
33
+ if (child.pid !== undefined)
34
+ killTree(child);
35
+ });
36
+ const watch = () => (!over && (owed.size > 0 || open) && asked.size === 0 ? silence.heard() : silence.stop());
20
37
  child.stderr.on('data', (chunk) => {
21
38
  stderr = (stderr + chunk.toString('utf8')).slice(-4000);
22
39
  });
23
40
  const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
41
+ const output = stream.writable.getWriter();
24
42
  const connection = app.connect({
25
- writable: stream.writable,
43
+ writable: new WritableStream({
44
+ write(message) {
45
+ const sent = message;
46
+ if (sent.id !== undefined) {
47
+ if (sent.method === undefined)
48
+ asked.delete(sent.id);
49
+ else
50
+ owed.add(sent.id);
51
+ watch();
52
+ }
53
+ return output.write(message);
54
+ },
55
+ close: () => output.close(),
56
+ abort: (reason) => output.abort(reason),
57
+ }),
26
58
  readable: stream.readable.pipeThrough(new TransformStream({
27
59
  transform(message, controller) {
28
60
  const read = message;
61
+ if (read.id !== undefined) {
62
+ if (read.method === undefined)
63
+ owed.delete(read.id);
64
+ else
65
+ asked.add(read.id);
66
+ }
67
+ watch();
29
68
  if (owned)
30
69
  events(read);
31
70
  if (read.id !== undefined || (read.method !== 'session/update' && !read.method.startsWith('_'))) {
@@ -37,6 +76,8 @@ export async function startAdapter(kind, cli, cwd, app, events = () => undefined
37
76
  const { promise: lost, resolve: lose } = Promise.withResolvers();
38
77
  const exited = new Promise((resolve) => {
39
78
  const end = (cause) => {
79
+ over = true;
80
+ silence.stop();
40
81
  if (owned)
41
82
  lose(cause);
42
83
  owned = false;
@@ -46,11 +87,13 @@ export async function startAdapter(kind, cli, cwd, app, events = () => undefined
46
87
  child.on('error', (error) => end(`the CLI did not start: ${error.message}`));
47
88
  child.on('exit', (code, signal) => {
48
89
  const last = stderr.trim().split('\n').at(-1);
49
- end(`the CLI exited with ${code ?? signal}${last ? `: ${last}` : ''}`);
90
+ end(silent ? SILENT : `the CLI exited with ${code ?? signal}${last ? `: ${last}` : ''}`);
50
91
  });
51
92
  });
52
93
  const retire = () => {
53
94
  owned = false;
95
+ over = true;
96
+ silence.stop();
54
97
  if (retired)
55
98
  return;
56
99
  retired = true;
@@ -74,7 +117,11 @@ export async function startAdapter(kind, cli, cwd, app, events = () => undefined
74
117
  clientInfo: { name: '@agentshouse/kit', version: KIT_RELEASE },
75
118
  });
76
119
  scope?.entered(child.pid);
77
- return { connection, initialized, exited, lost, retire };
120
+ const turn = (opened) => {
121
+ open = opened;
122
+ watch();
123
+ };
124
+ return { connection, initialized, exited, lost, silent: () => silent, turn, retire };
78
125
  }
79
126
  catch (error) {
80
127
  retire();
@@ -1,10 +1,10 @@
1
- import { setGlobalProxyFromEnv } from 'node:http';
2
1
  import { HouseRefusal, post } from './api.js';
3
2
  import { readEnrolment } from './home.js';
3
+ import { proxyUnlessRelayed } from './relay.js';
4
4
  function unanswered(error) {
5
5
  return error instanceof HouseRefusal ? error.status >= 500 : error instanceof TypeError && error.message === 'fetch failed';
6
6
  }
7
- setGlobalProxyFromEnv();
7
+ proxyUnlessRelayed();
8
8
  try {
9
9
  const { house, credential } = (await readEnrolment());
10
10
  try {
@@ -0,0 +1,46 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createWriteStream } from 'node:fs';
3
+ import { rm } from 'node:fs/promises';
4
+ import { Transform } from 'node:stream';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import { readEnrolment } from './home.js';
7
+ import { relayed } from './relay.js';
8
+ const MANIFEST = 'bootstrap.json';
9
+ async function served(name, signal) {
10
+ const enrolment = await readEnrolment();
11
+ if (enrolment === null)
12
+ throw new Error('this Kit is connected to no House');
13
+ const answer = await fetch(relayed(new URL(`/kit-installer/${encodeURIComponent(name)}`, enrolment.house)), { signal });
14
+ if (answer.status === 200 && answer.body !== null)
15
+ return answer;
16
+ await answer.body?.cancel();
17
+ throw new Error(`House answered ${answer.status} for ${name}`);
18
+ }
19
+ export async function servedDigest(name, signal) {
20
+ const manifest = (await (await served(MANIFEST, signal)).json());
21
+ const sha256 = manifest.files?.[name]?.sha256;
22
+ return typeof sha256 === 'string' ? sha256 : null;
23
+ }
24
+ export async function downloadServed(name, sha256, destination, signal, most = Infinity) {
25
+ const answer = await served(name, signal);
26
+ const digest = createHash('sha256');
27
+ let bytes = 0;
28
+ const hashed = new Transform({
29
+ transform(chunk, _encoding, done) {
30
+ bytes += chunk.length;
31
+ if (bytes > most)
32
+ return done(new Error(`${name} is larger than its pinned ${most} bytes`));
33
+ digest.update(chunk);
34
+ done(null, chunk);
35
+ },
36
+ });
37
+ try {
38
+ await pipeline(answer.body, hashed, createWriteStream(destination), { signal });
39
+ if (digest.digest('hex') !== sha256)
40
+ throw new Error(`${name} does not match its sha256`);
41
+ }
42
+ catch (error) {
43
+ await rm(destination, { force: true });
44
+ throw error;
45
+ }
46
+ }
package/dist/clis-main.js CHANGED
@@ -1,8 +1,8 @@
1
- import { setGlobalProxyFromEnv } from 'node:http';
2
1
  import { post } from './api.js';
3
2
  import { CLIS, below, locate, versionOf } from './clis.js';
4
3
  import { readEnrolment } from './home.js';
5
- setGlobalProxyFromEnv();
4
+ import { proxyUnlessRelayed } from './relay.js';
5
+ proxyUnlessRelayed();
6
6
  try {
7
7
  const { house, credential } = (await readEnrolment());
8
8
  const { agents } = await post(house, '/kit/agents/desired', {}, credential);
package/dist/clis.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { constants, readFileSync } from 'node:fs';
4
- import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
+ import { access, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
5
5
  import { homedir, userInfo } from 'node:os';
6
6
  import { delimiter, dirname, isAbsolute, join } from 'node:path';
7
+ import { downloadServed, servedDigest } from './building.js';
7
8
  import { kitHome } from './home.js';
9
+ import { childEnvironment } from './relay.js';
8
10
  const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
9
11
  function houseRule(houses) {
10
12
  return `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`;
@@ -69,7 +71,7 @@ function bridgeInClaudeSandbox(bridge) {
69
71
  export const CLIS = {
70
72
  'codex-acp': {
71
73
  bin: 'codex',
72
- minimum: '0.159.1',
74
+ minimum: '0.160.1',
73
75
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
74
76
  adapter: {
75
77
  package: '@agentclientprotocol/codex-acp',
@@ -92,7 +94,7 @@ export const CLIS = {
92
94
  },
93
95
  'claude-agent-acp': {
94
96
  bin: 'claude',
95
- minimum: '2.1.286',
97
+ minimum: '2.1.303',
96
98
  install: 'curl -fsSL https://claude.ai/install.sh | bash',
97
99
  adapter: {
98
100
  package: '@agentclientprotocol/claude-agent-acp',
@@ -138,7 +140,6 @@ export const CLIS = {
138
140
  };
139
141
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
140
142
  export const KIT_RELEASE = KIT_PACKAGE.version;
141
- const PROXY = /^https?_proxy$/i;
142
143
  const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
143
144
  // An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
144
145
  const INSTALL_MS = 15 * 60_000;
@@ -160,15 +161,12 @@ async function installedAdapter(kind) {
160
161
  return null;
161
162
  }
162
163
  }
163
- export function withoutProxy() {
164
- return Object.fromEntries(Object.entries(process.env).filter(([name]) => !PROXY.test(name)));
165
- }
166
164
  export function run(command, args, timeoutMs, detached = false, signal) {
167
165
  return new Promise((resolve) => {
168
166
  const child = spawn(command, args, {
169
167
  stdio: ['ignore', 'pipe', 'pipe'],
170
168
  timeout: timeoutMs,
171
- env: withoutProxy(),
169
+ env: childEnvironment(),
172
170
  detached,
173
171
  signal: detached ? undefined : signal,
174
172
  });
@@ -202,15 +200,37 @@ export function run(command, args, timeoutMs, detached = false, signal) {
202
200
  export function failureOf(ran, program) {
203
201
  return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
204
202
  }
203
+ export function adapterArchive(kind) {
204
+ return `${kind}-${KIT_PACKAGE.peerDependencies[CLIS[kind].adapter.package]}.tar.gz`;
205
+ }
205
206
  export async function installAdapter(kind, signal) {
206
207
  const adapter = CLIS[kind].adapter;
207
208
  if (adapter === null)
208
209
  return null;
209
- const release = KIT_PACKAGE.peerDependencies[adapter.package];
210
- if ((await installedAdapter(kind)) === release)
210
+ if ((await installedAdapter(kind)) === KIT_PACKAGE.peerDependencies[adapter.package])
211
211
  return null;
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);
213
- return ran.status === 0 ? null : failureOf(ran, 'npm');
212
+ const name = adapterArchive(kind);
213
+ const archive = `${prefix(kind)}.tar.gz`;
214
+ const deadline = AbortSignal.timeout(INSTALL_MS);
215
+ const bounded = signal === undefined ? deadline : AbortSignal.any([signal, deadline]);
216
+ try {
217
+ const sha256 = await servedDigest(name, bounded);
218
+ if (sha256 === null)
219
+ return `House serves no ${name}`;
220
+ await rm(prefix(kind), { recursive: true, force: true });
221
+ await mkdir(prefix(kind), { recursive: true });
222
+ await downloadServed(name, sha256, archive, bounded);
223
+ const ran = await run('tar', ['-xzf', archive, '-C', prefix(kind)], INSTALL_MS, true, signal);
224
+ return ran.status === 0 ? null : failureOf(ran, 'tar');
225
+ }
226
+ catch (error) {
227
+ if (deadline.aborted && signal?.aborted !== true)
228
+ return `${name} did not arrive within ${INSTALL_MS / 1000} seconds`;
229
+ return error instanceof Error ? error.message : String(error);
230
+ }
231
+ finally {
232
+ await rm(archive, { force: true });
233
+ }
214
234
  }
215
235
  export async function installCli(kind, signal) {
216
236
  const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
@@ -3,40 +3,13 @@ import { HouseRefusal, retryDelay } from './api.js';
3
3
  import { end, REPLACED, signInAgain } from './end.js';
4
4
  import { readEnrolment } from './home.js';
5
5
  import { relayed } from './relay.js';
6
+ import { Silence } from './silence.js';
6
7
  import { endUnserved, KIT_WIRE } from './wire.js';
7
8
  const PATH = '/kit/stream';
8
9
  const SUBPROTOCOL = `house.kit.stream.${KIT_WIRE}`;
9
10
  const REFUSAL_HEADER = 'x-house-refusal';
10
11
  // House beats each Kit connection every thirty seconds, so two silent beats mean this Kit process cannot hear House.
11
12
  export const CONNECTION_SILENCE_MS = 60_000;
12
- const SYSTEM_CONNECTION_CLOCK = {
13
- later: (delay, callback) => setTimeout(callback, delay),
14
- clear: (timer) => clearTimeout(timer),
15
- };
16
- export class ConnectionSilence {
17
- timer = null;
18
- silent;
19
- clock;
20
- interval;
21
- constructor(silent, clock = SYSTEM_CONNECTION_CLOCK, interval = CONNECTION_SILENCE_MS) {
22
- this.silent = silent;
23
- this.clock = clock;
24
- this.interval = interval;
25
- }
26
- heard() {
27
- this.stop();
28
- this.timer = this.clock.later(this.interval, () => {
29
- this.timer = null;
30
- this.silent();
31
- });
32
- }
33
- stop() {
34
- if (this.timer === null)
35
- return;
36
- this.clock.clear(this.timer);
37
- this.timer = null;
38
- }
39
- }
40
13
  function connectionUrl(house) {
41
14
  const url = new URL(PATH, house);
42
15
  url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -58,19 +31,26 @@ subscribe('undici:request:headers', (message) => {
58
31
  });
59
32
  export function holdConnection(handlers) {
60
33
  let connection = null;
34
+ let socket = null;
35
+ let paused = false;
61
36
  let failures = 0;
62
37
  const connect = async () => {
63
38
  const { house, credential } = (await readEnrolment());
39
+ if (paused || socket !== null)
40
+ return;
64
41
  refused = null;
65
42
  const opening = new WebSocket(relayed(connectionUrl(house)), {
66
43
  protocols: [SUBPROTOCOL],
67
44
  headers: { authorization: `Bearer ${credential}` },
68
45
  });
46
+ socket = opening;
69
47
  let opened = false;
70
48
  let silence = null;
71
49
  const closed = (event) => {
72
50
  silence?.stop();
73
51
  silence = null;
52
+ if (socket === opening)
53
+ socket = null;
74
54
  if (connection === opening)
75
55
  connection = null;
76
56
  handlers.closed();
@@ -85,6 +65,8 @@ export function holdConnection(handlers) {
85
65
  endUnserved(refused.text);
86
66
  if (!opened && refused !== null)
87
67
  process.stderr.write(`kit: ${refused.message}\n`);
68
+ if (paused)
69
+ return;
88
70
  // A closed connection reopens in one to three seconds, spread so Kit processes one restart closed return apart; an opening House refused or left unanswered backs off on the jittered curve, retryable or not, since a resident outlives a House deploy.
89
71
  setTimeout(() => void connect(), opened ? 1000 + Math.random() * 2000 : retryDelay(failures++));
90
72
  };
@@ -92,7 +74,7 @@ export function holdConnection(handlers) {
92
74
  opened = true;
93
75
  failures = 0;
94
76
  connection = opening;
95
- silence = new ConnectionSilence(() => {
77
+ silence = new Silence(CONNECTION_SILENCE_MS, () => {
96
78
  if (connection !== opening)
97
79
  return;
98
80
  connection = null;
@@ -122,5 +104,13 @@ export function holdConnection(handlers) {
122
104
  connection.send(JSON.stringify(notification));
123
105
  return true;
124
106
  },
107
+ pause() {
108
+ paused = true;
109
+ socket?.close();
110
+ return () => {
111
+ paused = false;
112
+ void connect();
113
+ };
114
+ },
125
115
  };
126
116
  }
@@ -73,7 +73,9 @@ function answeredWith(error) {
73
73
  return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
74
74
  }
75
75
  function exitedWith(running, cause) {
76
- return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
76
+ if (running.killed)
77
+ return { failure: 'killed', cause: KILLED };
78
+ return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
77
79
  }
78
80
  function answerMessage(request, response) {
79
81
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
@@ -585,6 +587,7 @@ export class Conversations {
585
587
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
586
588
  void work.settled().then(() => scope.record());
587
589
  conversation.running = running;
590
+ adapter.turn(conversation.open > 0);
588
591
  conversation.session = running.sessionId;
589
592
  conversation.options = options;
590
593
  this.facts(conversation);
@@ -702,6 +705,7 @@ export class Conversations {
702
705
  begin(conversation) {
703
706
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
704
707
  conversation.open++;
708
+ conversation.running?.adapter.turn(true);
705
709
  void this.deliver(conversation, turn, 'started', {}).catch(logged);
706
710
  conversation.turn = turn;
707
711
  if (conversation.context !== null)
@@ -724,6 +728,7 @@ export class Conversations {
724
728
  turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
725
729
  turn.ended = true;
726
730
  conversation.open--;
731
+ conversation.running?.adapter.turn(conversation.open > 0);
727
732
  if (conversation.turn === turn)
728
733
  conversation.turn = null;
729
734
  const body = Promise.allSettled(turn.uploads).then(() => ({
package/dist/git.js CHANGED
@@ -2,8 +2,8 @@ import { spawn } from 'node:child_process';
2
2
  import { isUtf8 } from 'node:buffer';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { join } from 'node:path';
5
- import { withoutProxy } from './clis.js';
6
5
  import { workspaceRoot } from './home.js';
6
+ import { childEnvironment } from './relay.js';
7
7
  export function localCopies() {
8
8
  return join(workspaceRoot(), 'local-copies');
9
9
  }
@@ -11,7 +11,7 @@ export const HOUSE_REF = 'refs/house/received';
11
11
  const COMMITTER = 'Kit <house-kit@localhost>';
12
12
  export function run(cwd, args, input, env = {}) {
13
13
  return new Promise((resolve, reject) => {
14
- const child = spawn('git', args, { cwd, env: { ...withoutProxy(), ...env } });
14
+ const child = spawn('git', args, { cwd, env: { ...childEnvironment(), ...env } });
15
15
  const stdout = [];
16
16
  let stderr = '';
17
17
  child.stdout.on('data', (chunk) => stdout.push(chunk));
package/dist/house-cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { createReadStream } from 'node:fs';
3
- import { request, setGlobalProxyFromEnv } from 'node:http';
3
+ import { request } from 'node:http';
4
4
  import { request as httpsRequest } from 'node:https';
5
5
  import { extname, join, resolve } from 'node:path';
6
6
  import { Readable } from 'node:stream';
@@ -10,7 +10,7 @@ import { HOUSE_REF } from './git.js';
10
10
  import { ownerSocket, readOwnAgent } from './home.js';
11
11
  import { callHouse, rpc, UNREACHABLE, Unsettled } from './mcp.js';
12
12
  import { refusalOf, retryable } from './refusals.js';
13
- import { relayed, selectRelay } from './relay.js';
13
+ import { proxyUnlessRelayed, relayed, selectRelay } from './relay.js';
14
14
  import { parse } from 'yaml';
15
15
  const USAGE = "usage: house <tool> ['<arguments as JSON>']";
16
16
  const PUSH_USAGE = 'usage: house push [commit] [--owner]';
@@ -89,7 +89,6 @@ async function direct(message) {
89
89
  if (ownAgent === null)
90
90
  throw new Error(UNCONNECTED);
91
91
  selectRelay(process.env.HOUSE_KIT_RELAY || ownAgent.relay);
92
- setGlobalProxyFromEnv();
93
92
  const answer = await callHouse(ownAgent.house, ownAgent.credential, message).catch(() => {
94
93
  throw new Unsettled(UNREACHABLE);
95
94
  });
@@ -120,7 +119,7 @@ function opened(url, method, headers, body) {
120
119
  const local = socketPath !== undefined;
121
120
  const target = local ? null : relayed(url);
122
121
  if (!local)
123
- setGlobalProxyFromEnv();
122
+ proxyUnlessRelayed();
124
123
  const sent = target === null
125
124
  ? request({ socketPath, path: url, method, headers }, answered)
126
125
  : (target.protocol === 'https:' ? httpsRequest : request)(target, { method, headers }, answered);
@@ -1,6 +1,6 @@
1
1
  import { agentInstructions } from '@agentshouse/skills';
2
2
  import { HOUSE_REF } from './git.js';
3
- const HOW_WE_WORK = '/rooms/private/how-we-work.md';
3
+ const OWNER_RULES = '/rooms/private/owner-rules.md';
4
4
  const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
5
5
  const FAILED = /^exit: [1-9]\d*$/;
6
6
  const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
@@ -8,9 +8,9 @@ function cutOf(lines) {
8
8
  const cut = CUT.exec(lines[0] ?? '');
9
9
  return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
10
10
  }
11
- async function readHowWeWork(bridge) {
11
+ async function readOwnerRules(bridge) {
12
12
  let document = '';
13
- for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
13
+ for (let command = `cat ${OWNER_RULES}`; command !== undefined;) {
14
14
  const read = await bridge.tool('shell', { command });
15
15
  const text = read.content.map((content) => content.text).join('\n');
16
16
  const lines = text.split('\n');
@@ -28,5 +28,5 @@ async function readHowWeWork(bridge) {
28
28
  }
29
29
  export async function instructions(bridge, base, copies) {
30
30
  const lines = agentInstructions(HOUSE_REF, copies);
31
- return [lines.join('\n'), base, (await readHowWeWork(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
31
+ return [lines.join('\n'), base, (await readOwnerRules(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
32
32
  }
package/dist/kit-cli.js CHANGED
@@ -1,9 +1,9 @@
1
- import { setGlobalProxyFromEnv } from 'node:http';
2
1
  import { parseArgs } from 'node:util';
3
2
  import { answerGitCredential } from './app-git.js';
4
3
  import { chatgpt } from './chatgpt.js';
5
4
  import { login } from './login.js';
6
5
  import { logout } from './logout.js';
6
+ import { proxyUnlessRelayed } from './relay.js';
7
7
  import { resident } from './resident.js';
8
8
  const USAGE = 'usage: kit <login|logout|resident|chatgpt>\n';
9
9
  const COMMANDS = new Map([
@@ -36,7 +36,7 @@ const COMMANDS = new Map([
36
36
  ],
37
37
  ]);
38
38
  const [command, ...argv] = process.argv.slice(2);
39
- setGlobalProxyFromEnv();
39
+ proxyUnlessRelayed();
40
40
  async function run() {
41
41
  if (command === 'login')
42
42
  return login(argv);
package/dist/login.js CHANGED
@@ -119,7 +119,7 @@ export async function login(argv) {
119
119
  open(started.start_url);
120
120
  exchanged = await retrying(() => post(house, '/kit/token', { login: started.login, code_verifier: verifier }), undefined, () => POLL_INTERVAL_MS);
121
121
  }
122
- await placeSkillSet();
122
+ await placeSkillSet(house);
123
123
  await writeCodexRule().catch((error) => process.stderr.write(`kit: ${error.message}\n`));
124
124
  const relayRecord = relay === undefined ? {} : { relay };
125
125
  await writeEnrolment({
package/dist/relay.js CHANGED
@@ -1,6 +1,11 @@
1
+ import { setGlobalProxyFromEnv } from 'node:http';
2
+ const PROXY = /^https?_proxy$/i;
3
+ const LOOPBACK = ['127.0.0.1', 'localhost', '::1'];
1
4
  let selected;
5
+ let restoreProxy;
2
6
  export function selectRelay(relay) {
3
7
  selected = relay === undefined || relay === '' ? null : relay;
8
+ proxyUnlessRelayed();
4
9
  }
5
10
  export function relayOrigin() {
6
11
  if (selected !== undefined)
@@ -8,6 +13,22 @@ export function relayOrigin() {
8
13
  const ambient = process.env.HOUSE_KIT_RELAY;
9
14
  return ambient === undefined || ambient === '' ? undefined : ambient;
10
15
  }
16
+ function unlessRelayed() {
17
+ if (relayOrigin() === undefined)
18
+ return { ...process.env };
19
+ return Object.fromEntries(Object.entries(process.env).filter(([name]) => !PROXY.test(name)));
20
+ }
21
+ export function proxyUnlessRelayed() {
22
+ restoreProxy?.();
23
+ restoreProxy = setGlobalProxyFromEnv(unlessRelayed());
24
+ }
25
+ export function childEnvironment() {
26
+ const environment = unlessRelayed();
27
+ if (!Object.keys(environment).some((name) => PROXY.test(name)))
28
+ return environment;
29
+ const bypassed = [environment.NO_PROXY ?? environment.no_proxy, ...LOOPBACK].filter((entry) => entry !== undefined && entry !== '').join(',');
30
+ return { ...environment, NO_PROXY: bypassed, no_proxy: bypassed };
31
+ }
11
32
  export function relayed(target) {
12
33
  const url = new URL(target);
13
34
  const relay = relayOrigin();
package/dist/resident.js CHANGED
@@ -11,7 +11,7 @@ import { received } from './bridge.js';
11
11
  import { CONVERSATION_METHODS, Conversations } from './conversations.js';
12
12
  import { LocalCopies } from './copies.js';
13
13
  import { restart } from './end.js';
14
- import { agentBase, enrolled, forgetRestart, kitHome, ownerLock, ownerSocket, restartAsked } from './home.js';
14
+ import { agentBase, enrolled, forgetRestart, kitHome, ownerLock, ownerSocket, readEnrolment, restartAsked } from './home.js';
15
15
  import { Calls } from './calls.js';
16
16
  import { killDescendants } from './scope.js';
17
17
  import { quoted } from './shell.js';
@@ -117,7 +117,7 @@ async function started() {
117
117
  killDescendants();
118
118
  process.exit(1);
119
119
  });
120
- await placeSkillSet();
120
+ await placeSkillSet((await readEnrolment()).house);
121
121
  await mkdir(agentBase(), { recursive: true });
122
122
  await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
123
123
  await installShim(codexLauncher(), 'codex-main');
@@ -143,6 +143,9 @@ export async function resident() {
143
143
  else if (restartAsked() && conversations.quiet()) {
144
144
  restart('restarting for an updated CLI');
145
145
  }
146
+ else if (update.held() && conversations.quiet()) {
147
+ update.apply(connection.pause());
148
+ }
146
149
  else if (!reported) {
147
150
  reported = true;
148
151
  logged(house.deliver('/kit/idle', {}, idle));
@@ -158,7 +161,7 @@ export async function resident() {
158
161
  changed,
159
162
  });
160
163
  const signIns = new SignIns(house, agents, calls);
161
- const update = new KitUpdate(calls);
164
+ const update = new KitUpdate(calls, changed);
162
165
  const wizards = new WebWizards(calls, (notification) => connection?.send(notification) ?? false);
163
166
  for (const signal of ['SIGTERM', 'SIGINT']) {
164
167
  process.once(signal, () => {
@@ -180,7 +183,7 @@ export async function resident() {
180
183
  if (call.kind === 'sign_in')
181
184
  signIns.start(call);
182
185
  else if (call.kind === 'update')
183
- update.start(call);
186
+ update.hold(call);
184
187
  else if (call.kind === 'run_wizard')
185
188
  wizards.start(call);
186
189
  else if (CONVERSATION_METHODS.has(call.kind))
package/dist/sign-in.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
- import { CLIS, withoutProxy } from './clis.js';
3
+ import { CLIS } from './clis.js';
4
+ import { childEnvironment } from './relay.js';
4
5
  import { killTree } from './scope.js';
5
6
  import { holdSecretInput } from './secret-input.js';
6
7
  const ESCAPE = /\u001b\[[\d;]*m/g;
@@ -56,7 +57,7 @@ export class SignIns {
56
57
  const child = spawn(cli, login.args, {
57
58
  stdio: ['pipe', 'pipe', 'pipe'],
58
59
  detached: true,
59
- env: withoutProxy(),
60
+ env: childEnvironment(),
60
61
  });
61
62
  const running = () => child.exitCode === null && child.signalCode === null;
62
63
  const stop = () => killTree(child);
@@ -0,0 +1,28 @@
1
+ const SYSTEM_CLOCK = {
2
+ later: (delay, callback) => setTimeout(callback, delay),
3
+ clear: (timer) => clearTimeout(timer),
4
+ };
5
+ export class Silence {
6
+ timer = null;
7
+ interval;
8
+ silent;
9
+ clock;
10
+ constructor(interval, silent, clock = SYSTEM_CLOCK) {
11
+ this.interval = interval;
12
+ this.silent = silent;
13
+ this.clock = clock;
14
+ }
15
+ heard() {
16
+ this.stop();
17
+ this.timer = this.clock.later(this.interval, () => {
18
+ this.timer = null;
19
+ this.silent();
20
+ });
21
+ }
22
+ stop() {
23
+ if (this.timer === null)
24
+ return;
25
+ this.clock.clear(this.timer);
26
+ this.timer = null;
27
+ }
28
+ }
package/dist/skills.js CHANGED
@@ -1,10 +1,14 @@
1
- import { cp, lstat, mkdir, realpath, rm, symlink } from 'node:fs/promises';
1
+ import { cp, lstat, mkdir, realpath, rm, symlink, writeFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, relative } from 'node:path';
4
- import { listSkills } from '@agentshouse/skills';
4
+ import { GUIDE, listSkills } from '@agentshouse/skills';
5
+ import { parse } from 'yaml';
5
6
  import { KIT_RELEASE } from './clis.js';
6
7
  import { kitHome, readConfiguration, readHome, writeHome } from './home.js';
7
8
  const RECORD = 'skill-set.json';
9
+ const ENTRY = 'SKILL.md';
10
+ const FRONTMATTER = /^---\n([\s\S]*?)\n---\n/u;
11
+ const ORIGIN = '<origin>';
8
12
  function skillsDirectory() {
9
13
  return join(homedir(), '.agents', 'skills');
10
14
  }
@@ -22,9 +26,31 @@ async function present(path) {
22
26
  throw error;
23
27
  }
24
28
  }
25
- async function install() {
29
+ function skillName(text) {
30
+ const front = FRONTMATTER.exec(text);
31
+ const fields = (front === null ? null : parse(front[1]));
32
+ return typeof fields?.name === 'string' && typeof fields.description === 'string' ? fields.name : undefined;
33
+ }
34
+ function skillSet(origin) {
35
+ const upstream = listSkills().map(({ name, directory }) => ({
36
+ name,
37
+ place: (destination) => cp(directory, destination, { recursive: true }),
38
+ }));
39
+ const pages = GUIDE.flatMap(({ text }) => {
40
+ const name = skillName(text);
41
+ if (name === undefined)
42
+ return [];
43
+ const place = async (destination) => {
44
+ await mkdir(destination);
45
+ await writeFile(join(destination, ENTRY), text.replaceAll(ORIGIN, origin));
46
+ };
47
+ return [{ name, place }];
48
+ });
49
+ return [...upstream, ...pages].sort((left, right) => (left.name < right.name ? -1 : 1));
50
+ }
51
+ async function install(origin) {
26
52
  const placed = [];
27
- for (const skill of listSkills()) {
53
+ for (const skill of skillSet(origin)) {
28
54
  if (!(await present(join(skillsDirectory(), skill.name))) && !(await present(join(claudeDirectory(), skill.name)))) {
29
55
  placed.push(skill);
30
56
  }
@@ -35,32 +61,32 @@ async function install() {
35
61
  await mkdir(claudeDirectory(), { recursive: true });
36
62
  const skills = await realpath(skillsDirectory());
37
63
  const claude = await realpath(claudeDirectory());
38
- for (const { name, directory } of placed) {
39
- await cp(directory, join(skillsDirectory(), name), { recursive: true });
64
+ for (const { name, place } of placed) {
65
+ await place(join(skillsDirectory(), name));
40
66
  if (claude !== skills)
41
67
  await symlink(relative(claude, join(skills, name)), join(claudeDirectory(), name));
42
68
  }
43
- await writeHome(RECORD, { version: KIT_RELEASE, names });
69
+ await writeHome(RECORD, { version: KIT_RELEASE, origin, names });
44
70
  }
45
- export async function placeSkillSet() {
71
+ export async function placeSkillSet(origin) {
46
72
  try {
47
- await replaceSkillSet();
73
+ await replaceSkillSet(origin);
48
74
  }
49
75
  catch (error) {
50
76
  process.stderr.write(`kit: the skills could not be placed: ${error.message}\n`);
51
77
  }
52
78
  }
53
- async function replaceSkillSet() {
79
+ async function replaceSkillSet(origin) {
54
80
  const accepted = (await readConfiguration()).skills;
55
81
  const installed = await readHome(RECORD);
56
- if (accepted && installed?.version === KIT_RELEASE)
82
+ if (accepted && installed?.version === KIT_RELEASE && installed.origin === origin)
57
83
  return;
58
84
  for (const name of installed?.names ?? []) {
59
85
  await rm(join(claudeDirectory(), name), { recursive: true, force: true });
60
86
  await rm(join(skillsDirectory(), name), { recursive: true, force: true });
61
87
  }
62
88
  if (accepted)
63
- await install();
89
+ await install(origin);
64
90
  else
65
91
  await rm(join(kitHome(), RECORD), { force: true });
66
92
  }
@@ -1,32 +1,31 @@
1
- import { createHash } from 'node:crypto';
2
- import { createReadStream } from 'node:fs';
3
1
  import { chmod, mkdir, mkdtemp, readdir, readFile, rename, rm, stat } from 'node:fs/promises';
4
2
  import { availableParallelism, tmpdir } from 'node:os';
5
3
  import { basename, join } from 'node:path';
6
- import { pipeline } from 'node:stream/promises';
7
4
  import { unlessAborted } from './abort.js';
5
+ import { downloadServed } from './building.js';
8
6
  import { failureOf, run } from './clis.js';
9
7
  import { downloaded, fetched } from './files.js';
10
8
  import { kitHome } from './home.js';
11
9
  const RELEASE = 'https://github.com/agentshouse/kit/releases/download/engine-2';
12
10
  export const MODEL = {
13
- url: 'https://huggingface.co/ggml-org/parakeet-GGUF/resolve/35156454d1a39de06863303dd209fd2bed6ee079/ggml-parakeet-tdt-0.6b-v3-q4_k.bin',
11
+ name: 'ggml-parakeet-tdt-0.6b-v3-q4_k.bin',
12
+ from: 'https://huggingface.co/ggml-org/parakeet-GGUF/resolve/35156454d1a39de06863303dd209fd2bed6ee079/ggml-parakeet-tdt-0.6b-v3-q4_k.bin',
14
13
  sha256: '8b205b8b39c6535e153de6fb11c51db46125d45c4f16ba496fe41a0fe71b885e',
15
14
  bytes: 415_611_879,
16
15
  executable: false,
17
16
  };
18
17
  export const ENGINES = {
19
18
  'linux-x64': {
20
- decoder: { url: `${RELEASE}/ffmpeg-linux-x86_64`, sha256: '1f8235d206428305afb89c6e8ab99d100425a0ba61e22bb4c7aff017db8c8de5', bytes: 5_175_832, executable: true },
21
- engine: { url: `${RELEASE}/parakeet-cli-linux-x86_64`, sha256: 'bb2585695992fe1927a33960002cdf51b8a90a96ad65593c3d0a9f1fbc0865c2', bytes: 3_801_616, executable: true },
19
+ decoder: { name: 'ffmpeg-linux-x86_64', from: `${RELEASE}/ffmpeg-linux-x86_64`, sha256: '1f8235d206428305afb89c6e8ab99d100425a0ba61e22bb4c7aff017db8c8de5', bytes: 5_175_832, executable: true },
20
+ engine: { name: 'parakeet-cli-linux-x86_64', from: `${RELEASE}/parakeet-cli-linux-x86_64`, sha256: 'bb2585695992fe1927a33960002cdf51b8a90a96ad65593c3d0a9f1fbc0865c2', bytes: 3_801_616, executable: true },
22
21
  },
23
22
  'linux-arm64': {
24
- decoder: { url: `${RELEASE}/ffmpeg-linux-aarch64`, sha256: 'a7e08a0fae0179d1078312f6bb13d2a9aa4d4c71a1d82a022953f25dee452f5e', bytes: 4_590_208, executable: true },
25
- engine: { url: `${RELEASE}/parakeet-cli-linux-aarch64`, sha256: 'cbc0bedc8daae97f1ef9dcee4200c06226218e300e0fd62ead1306d22742b7ed', bytes: 3_477_624, executable: true },
23
+ decoder: { name: 'ffmpeg-linux-aarch64', from: `${RELEASE}/ffmpeg-linux-aarch64`, sha256: 'a7e08a0fae0179d1078312f6bb13d2a9aa4d4c71a1d82a022953f25dee452f5e', bytes: 4_590_208, executable: true },
24
+ engine: { name: 'parakeet-cli-linux-aarch64', from: `${RELEASE}/parakeet-cli-linux-aarch64`, sha256: 'cbc0bedc8daae97f1ef9dcee4200c06226218e300e0fd62ead1306d22742b7ed', bytes: 3_477_624, executable: true },
26
25
  },
27
26
  'darwin-arm64': {
28
- decoder: { url: `${RELEASE}/ffmpeg-darwin-arm64`, sha256: '7abe154aaed3d6b49f5167ca7221bb66bed51388925d99a7e4261a41fe52fba6', bytes: 3_696_768, executable: true },
29
- engine: { url: `${RELEASE}/parakeet-cli-darwin-arm64`, sha256: '9a2aafc0922ad7c0b6db6eff1641e96572ba32ac40bf995a1df242ed2febf297', bytes: 1_939_728, executable: true },
27
+ decoder: { name: 'ffmpeg-darwin-arm64', from: `${RELEASE}/ffmpeg-darwin-arm64`, sha256: '7abe154aaed3d6b49f5167ca7221bb66bed51388925d99a7e4261a41fe52fba6', bytes: 3_696_768, executable: true },
28
+ engine: { name: 'parakeet-cli-darwin-arm64', from: `${RELEASE}/parakeet-cli-darwin-arm64`, sha256: '9a2aafc0922ad7c0b6db6eff1641e96572ba32ac40bf995a1df242ed2febf297', bytes: 1_939_728, executable: true },
30
29
  },
31
30
  };
32
31
  const CHUNK_SECONDS = 20;
@@ -47,7 +46,7 @@ function installed(artifact) {
47
46
  return join(directory(), artifact.sha256);
48
47
  }
49
48
  async function executed(command, args, signal) {
50
- // A download or a transcription runs as long as its size needs; a download's deadline, the setting turning off or the turn's interrupt or end aborts it instead.
49
+ // A decode or a transcription runs as long as its audio needs; the setting turning off or the turn's interrupt or end aborts it instead.
51
50
  const ran = await run(command, args, 0, false, signal);
52
51
  if (ran.status !== 0)
53
52
  throw new Error(failureOf(ran, basename(command)));
@@ -61,11 +60,6 @@ async function threads() {
61
60
  const allowed = quota === 'max' ? Infinity : Math.ceil(Number(quota) / Number(period));
62
61
  return Math.max(1, Math.floor(Math.min(availableParallelism(), allowed) / 2));
63
62
  }
64
- async function sha256(path) {
65
- const hash = createHash('sha256');
66
- await pipeline(createReadStream(path), hash);
67
- return hash.digest('hex');
68
- }
69
63
  function causeOf(error) {
70
64
  return error instanceof Error ? error.message : String(error);
71
65
  }
@@ -86,6 +80,25 @@ function ready() {
86
80
  function pinnedHere() {
87
81
  return ENGINES[`${process.platform}-${process.arch}`];
88
82
  }
83
+ const DOWNLOADS = 3;
84
+ async function downloadedAgain(artifact, signal) {
85
+ // An artifact arrives at a hundred kilobytes a second or its download ends, so a stalled Building cannot hold the install open.
86
+ const deadlineMs = Math.ceil(artifact.bytes / 100);
87
+ const deadline = AbortSignal.timeout(deadlineMs);
88
+ for (let attempt = 1;; attempt++) {
89
+ try {
90
+ return await downloadServed(artifact.name, artifact.sha256, `${installed(artifact)}.part`, AbortSignal.any([signal, deadline]), artifact.bytes);
91
+ }
92
+ catch (error) {
93
+ if (signal.aborted)
94
+ throw error;
95
+ if (deadline.aborted)
96
+ throw new Error(`${artifact.name} did not arrive within ${Math.ceil(deadlineMs / 1000)} seconds`);
97
+ if (attempt === DOWNLOADS)
98
+ throw error;
99
+ }
100
+ }
101
+ }
89
102
  async function install(artifacts, signal) {
90
103
  await mkdir(directory(), { recursive: true });
91
104
  const pinned = new Set(artifacts.map((artifact) => artifact.sha256));
@@ -98,25 +111,8 @@ async function install(artifacts, signal) {
98
111
  if (!(await present(installed(artifact))))
99
112
  missing.push(artifact);
100
113
  try {
101
- for (const artifact of missing) {
102
- const part = `${installed(artifact)}.part`;
103
- // An artifact arrives at a hundred kilobytes a second or its download ends, so a stalled host cannot hold the install open.
104
- const deadlineMs = Math.ceil(artifact.bytes / 100);
105
- const deadline = AbortSignal.timeout(deadlineMs);
106
- // Three retries carry a large download past a dropped connection before the download fails.
107
- try {
108
- await executed('bash', ['-c', 'trap "" XFSZ && ulimit -f "$1" && exec curl -fsSL --retry 3 -o "$2" "$3"', 'bash', String(Math.ceil(artifact.bytes / 1024)), part, artifact.url], AbortSignal.any([signal, deadline]));
109
- }
110
- catch (error) {
111
- if (((await stat(part).catch(() => null))?.size ?? 0) >= artifact.bytes)
112
- throw new Error(`${artifact.url} is larger than its pinned ${artifact.bytes} bytes`);
113
- if (deadline.aborted && !signal.aborted)
114
- throw new Error(`${artifact.url} did not arrive within ${Math.ceil(deadlineMs / 1000)} seconds`);
115
- throw error;
116
- }
117
- if ((await sha256(part)) !== artifact.sha256)
118
- throw new Error(`${artifact.url} does not match its sha256`);
119
- }
114
+ for (const artifact of missing)
115
+ await downloadedAgain(artifact, signal);
120
116
  for (const artifact of missing) {
121
117
  if (artifact.executable)
122
118
  await chmod(`${installed(artifact)}.part`, 0o755);
package/dist/update.js CHANGED
@@ -6,9 +6,13 @@ import { join } from 'node:path';
6
6
  import { run } from './clis.js';
7
7
  import { kitHome, workspaceRoot } from './home.js';
8
8
  import { service, throughSudo } from './placement.js';
9
+ import { relayed } from './relay.js';
9
10
  const BOOTSTRAP_BYTES = 1_000_000;
10
11
  // A bootstrap is at most a million bytes, so two minutes carry it over ten kilobytes a second and no stalled host holds the update queue.
11
12
  const BOOTSTRAP_MS = 120_000;
13
+ // A bootstrap's downloads and CLI updates take minutes, so one still running after an hour has hung and is ended before it keeps the Kit offline.
14
+ const RERUN_MS = 60 * 60_000;
15
+ const TIMED_OUT = '124';
12
16
  function directory() {
13
17
  return join(kitHome(), 'update');
14
18
  }
@@ -34,7 +38,7 @@ function placement() {
34
38
  return service() === 'user' ? ['--workspace', workspaceRoot()] : ['--linux'];
35
39
  }
36
40
  function record() {
37
- return `outcome=$1; shift; bash "$0" "$@"; printf %s "$?" > "$outcome"; ${[...elevated(), 'systemctl', ...manager(), 'start', 'house-kit.service'].join(' ')}`;
41
+ return `outcome=$1; shift; timeout ${RERUN_MS / 1000} bash "$0" "$@"; printf %s "$?" > "$outcome"; ${[...elevated(), 'systemctl', ...manager(), 'start', 'house-kit.service'].join(' ')}`;
38
42
  }
39
43
  async function pendingCall() {
40
44
  try {
@@ -48,6 +52,8 @@ async function recorded() {
48
52
  const status = await readFile(outcome(), 'utf8').catch(() => null);
49
53
  if (status === null)
50
54
  return null;
55
+ if (status.trim() === TIMED_OUT)
56
+ return { refused: `the Kit bootstrap did not finish within ${RERUN_MS / 1000} seconds` };
51
57
  return status.trim() === '0' ? {} : { refused: `the Kit bootstrap exited with ${status.trim()}` };
52
58
  }
53
59
  async function forget(callId) {
@@ -108,9 +114,12 @@ function rerun(script, callId, flags) {
108
114
  }
109
115
  export class KitUpdate {
110
116
  calls;
117
+ changed;
118
+ waiting = new Map();
111
119
  queue = Promise.resolve();
112
- constructor(calls) {
120
+ constructor(calls, changed) {
113
121
  this.calls = calls;
122
+ this.changed = changed;
114
123
  }
115
124
  async resumed() {
116
125
  const callId = await pendingCall();
@@ -118,8 +127,17 @@ export class KitUpdate {
118
127
  return;
119
128
  await this.enqueue(callId, () => finished(callId));
120
129
  }
121
- start(call) {
122
- void this.enqueue(call.input_id, () => this.carry(call));
130
+ hold(call) {
131
+ this.waiting.set(call.input_id, call);
132
+ this.changed();
133
+ }
134
+ held() {
135
+ return this.waiting.size > 0;
136
+ }
137
+ apply(resume) {
138
+ const held = [...this.waiting.values()];
139
+ this.waiting.clear();
140
+ void Promise.all(held.map((call) => this.enqueue(call.input_id, () => this.carry(call)))).then(resume);
123
141
  }
124
142
  enqueue(callId, work) {
125
143
  const before = this.queue;
@@ -136,7 +154,7 @@ export class KitUpdate {
136
154
  const deadline = AbortSignal.timeout(BOOTSTRAP_MS);
137
155
  let script;
138
156
  try {
139
- const answer = await fetch(call.bootstrap, { signal: deadline });
157
+ const answer = await fetch(relayed(call.bootstrap), { signal: deadline });
140
158
  if (!answer.ok)
141
159
  return { refused: `the Kit bootstrap could not be downloaded: ${answer.status}` };
142
160
  script = await bounded(answer.body, BOOTSTRAP_BYTES);
@@ -1,4 +1,5 @@
1
- import { loginPath, onLoginPath, withoutProxy } from './clis.js';
1
+ import { loginPath, onLoginPath } from './clis.js';
2
+ import { childEnvironment } from './relay.js';
2
3
  import { killTree } from './scope.js';
3
4
  const TERMINAL = 'xterm-256color';
4
5
  const SIGNALLED = 128;
@@ -62,7 +63,7 @@ export class WebWizards {
62
63
  name: TERMINAL,
63
64
  ...run.size,
64
65
  cwd: process.cwd(),
65
- env: { ...withoutProxy(), PATH: path },
66
+ env: { ...childEnvironment(), PATH: path },
66
67
  });
67
68
  }
68
69
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.2.1-c03721a31074a",
3
+ "version": "0.2.1-c0535b8294007",
4
4
  "description": "Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -12,28 +12,27 @@
12
12
  "kit": "dist/kit-main.js",
13
13
  "house": "dist/house-main.js"
14
14
  },
15
- "exports": {
16
- "./bootstrap": "./dist/bootstrap.json"
17
- },
18
15
  "files": [
19
16
  "dist"
20
17
  ],
21
18
  "scripts": {
22
- "build": "tsc -p tsconfig.build.json && node scripts/bootstrap-manifest.mjs",
19
+ "bootstrap-manifest": "node scripts/bootstrap-manifest.mjs",
20
+ "build": "tsc -p tsconfig.build.json",
23
21
  "engine": "mkdir -p tmp/engine && docker run --rm -v \"$PWD:/kit\" -w /kit alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 sh scripts/engine.sh tmp/engine",
24
22
  "follow-skills": "node scripts/follow-skills.mjs",
23
+ "release-files": "node scripts/release-files.mjs",
25
24
  "test": "tsc -p tsconfig.json && vitest run"
26
25
  },
27
26
  "dependencies": {
28
- "@agentclientprotocol/sdk": "1.7.0",
29
- "@agentshouse/mdmodel": "0.1.0-cabd61587647f",
30
- "@agentshouse/skills": "0.1.0-c34ad2b295b4a",
27
+ "@agentclientprotocol/sdk": "2.0.1",
28
+ "@agentshouse/mdmodel": "0.1.0-c4f4d37bc9d6a",
29
+ "@agentshouse/skills": "0.1.0-c08575ab21b57",
31
30
  "@lydell/node-pty": "1.2.0-beta.15",
32
31
  "yaml": "2.9.1"
33
32
  },
34
33
  "peerDependencies": {
35
- "@agentclientprotocol/claude-agent-acp": "0.85.1",
36
- "@agentclientprotocol/codex-acp": "2.1.1"
34
+ "@agentclientprotocol/claude-agent-acp": "0.94.0",
35
+ "@agentclientprotocol/codex-acp": "2.2.2"
37
36
  },
38
37
  "peerDependenciesMeta": {
39
38
  "@agentclientprotocol/claude-agent-acp": {
@@ -44,10 +43,10 @@
44
43
  }
45
44
  },
46
45
  "devDependencies": {
47
- "@types/node": "24.13.6",
46
+ "@types/node": "24.19.2",
48
47
  "@types/ws": "8.18.2",
49
48
  "typescript": "7.0.2",
50
- "vitest": "5.0.1",
49
+ "vitest": "5.0.5",
51
50
  "ws": "8.22.0"
52
51
  },
53
52
  "packageManager": "npm@11.19.1",
@@ -1,24 +0,0 @@
1
- {
2
- "schemaVersion": 2,
3
- "version": "0.2.1-c03721a31074a",
4
- "hosts": {
5
- "macos": {
6
- "asset": {
7
- "name": "connect-linux.sh",
8
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c03721a31074a/connect-linux.sh"
9
- }
10
- },
11
- "linux": {
12
- "asset": {
13
- "name": "connect-linux.sh",
14
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c03721a31074a/connect-linux.sh"
15
- }
16
- },
17
- "windows": {
18
- "asset": {
19
- "name": "connect-windows.ps1",
20
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c03721a31074a/connect-windows.ps1"
21
- }
22
- }
23
- }
24
- }