@agentshouse/kit 0.2.1-c3304109525e3 → 0.2.1-c33a81a492b90
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/README.md +13 -13
- package/dist/afk.js +17 -0
- package/dist/agents.js +42 -10
- package/dist/building.js +7 -3
- package/dist/chunks.js +49 -0
- package/dist/clis.js +27 -49
- package/dist/connection.js +2 -13
- package/dist/conversations.js +18 -4
- package/dist/copies.js +271 -120
- package/dist/end.js +1 -1
- package/dist/events.js +2 -2
- package/dist/files.js +2 -2
- package/dist/git.js +44 -10
- package/dist/home.js +0 -7
- package/dist/placement.js +10 -18
- package/dist/resident.js +10 -10
- package/dist/scope.js +12 -4
- package/dist/update.js +105 -159
- package/dist/wire.js +23 -5
- package/package.json +2 -2
- package/dist/clis-main.js +0 -23
package/README.md
CHANGED
|
@@ -9,25 +9,25 @@ Run `sh -c 'kit=$(curl -fsSL "$0") || { echo "Kit could not be downloaded; run t
|
|
|
9
9
|
or `& { try { $kit = irm agents.house/kit.ps1 } catch { throw 'Kit could not be downloaded; run this command again.' }; & ([scriptblock]::Create($kit)) @args }` on Windows, which run nothing of a failed download, and again to update; options follow `agents.house/kit`.
|
|
10
10
|
On macOS and Linux it installs Kit natively under your own account, without an admin password, as a service that runs
|
|
11
11
|
while you are logged in. `kit` and `house` are then on your login shell's `PATH`, and your workspace is `~/AgentsHouse` unless you
|
|
12
|
-
pass `--workspace <path>`.
|
|
12
|
+
pass `--workspace <path>`. Every run places the Kit release your Building serves, with the Node.js and agent adapters it runs on, in a directory of its own beside the one that runs, then starts it; the earlier one stays until the next release. Kit also updates itself: when your Building takes another release, Kit places it the same way and, once no turn is open, ends so its service, or the container's restart policy, starts it. When House refuses the Kit that runs as too old or too new, Kit places your Building's release at once, ending any running turn, and ends the same way; while it cannot, it stays offline, prints why with the command that installs it again, and tries again regularly.
|
|
13
13
|
|
|
14
|
-
Add `--container` to run Kit in one Docker container instead, with your
|
|
15
|
-
workspace
|
|
16
|
-
|
|
14
|
+
Add `--container` to run Kit in one Docker container instead, with your workspace mounted at `/agents/house`; your Agents then reach only that
|
|
15
|
+
workspace. The container's image holds no Kit release and no Node.js: every run places them in Kit home's `kit` folder, which a container created again keeps. On Windows,
|
|
16
|
+
Kit home is the Docker volume `house-kit-home` and the container is the one choice. Append a `kit` or `house` command to the container's options to run it in that container.
|
|
17
17
|
Running the command for the other choice on a connected computer asks you to
|
|
18
18
|
confirm, then replaces its Environment, whose Agents end, with a new one. Running it with a different `HOUSE_KIT_HOME`
|
|
19
19
|
from the running Kit process asks you to confirm before it replaces that process, and leaves the earlier Kit home as it was.
|
|
20
20
|
|
|
21
|
-
On your own Linux server, run the Linux command with `--linux` to
|
|
22
|
-
install Kit natively, with its workspace at `/agents/house`; `kit` and
|
|
23
|
-
`house` are then on the server's `PATH`.
|
|
21
|
+
On your own Linux server, run the Linux command with `--linux` to install Kit natively, with its workspace at `/agents/house` and a missing `ca-certificates`, `curl` or `git` installed through apt; `kit` and `house` are then on the server's `PATH`.
|
|
24
22
|
|
|
25
|
-
Kit runs the
|
|
26
|
-
|
|
27
|
-
each CLI
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
Kit is built for macOS 27 on Apple silicon, Ubuntu 26.04 LTS on amd64 and Windows 11 on amd64. On another release, distribution or architecture the command prints one warning line, such as `Warning: macOS 26.7.1 is outdated; update to macOS 27.`, and installs anyway. It refuses only an architecture other than amd64 and arm64, and a native install on a Linux without glibc, where `--container` still runs Kit. On a Linux without systemd it installs and connects Kit but starts nothing: run the `kit resident` command it prints under your own service manager, restarting it whenever it ends with a failure. `--container` installs Docker Engine on Ubuntu and Debian; another Linux needs a working Docker Engine first.
|
|
24
|
+
|
|
25
|
+
Kit runs each agent CLI through the link your login shell finds and installs a missing one through that CLI's own
|
|
26
|
+
installer. When it starts and regularly after, Kit runs each CLI's own update command, such as `claude update`, beside
|
|
27
|
+
your conversations: a running conversation keeps its version and the next one runs the new release. A CLI older than
|
|
28
|
+
the oldest release Kit is built for still runs, though its conversations may fail: Kit tells House its version, that
|
|
29
|
+
oldest release and its update command. Kit also gives every Agent on the Environment the agent skills of
|
|
30
|
+
`@agentshouse/skills`, at the version each Kit release installs. Add `--no-skills` to the command to leave them out; running it again without the flag brings them back.
|
|
31
31
|
|
|
32
32
|
## Commands
|
|
33
33
|
|
package/dist/afk.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CLIS } from './clis.js';
|
|
2
|
+
const NOBODY_THERE = 'Nobody is there to answer. Decide for yourself and go on.';
|
|
3
|
+
export const PLAN_APPROVED = { outcome: 'approved' };
|
|
4
|
+
function told(fields) {
|
|
5
|
+
return Object.fromEntries(fields.map((field) => [field, NOBODY_THERE]));
|
|
6
|
+
}
|
|
7
|
+
export function permissionAllowed({ options }) {
|
|
8
|
+
const option = options.find(({ kind }) => kind === 'allow_once') ?? options.find(({ kind }) => kind === 'allow_always');
|
|
9
|
+
return { outcome: { outcome: 'selected', optionId: option.optionId } };
|
|
10
|
+
}
|
|
11
|
+
export function formAnswered(kind, request) {
|
|
12
|
+
const fields = CLIS[kind].questionFields(request);
|
|
13
|
+
return fields.length === 0 ? { action: 'decline' } : { action: 'accept', content: told(fields) };
|
|
14
|
+
}
|
|
15
|
+
export function grokQuestionAnswered({ questions }) {
|
|
16
|
+
return { outcome: 'accepted', answers: told(questions.map(({ question }) => question)) };
|
|
17
|
+
}
|
package/dist/agents.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { client } from '@agentclientprotocol/sdk';
|
|
2
|
-
import { readFile
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
4
|
import { signedOut, startAdapter } from './acp.js';
|
|
5
|
-
import { CLIS, KIT_RELEASE, below,
|
|
5
|
+
import { CLIS, KIT_RELEASE, below, installCli, locate, run, updateCli, versionOf } from './clis.js';
|
|
6
6
|
import { agentBase, kitHome } from './home.js';
|
|
7
|
-
import { updatesItself } from './placement.js';
|
|
8
7
|
import { holdTranscription, transcriptionReport } from './transcription.js';
|
|
9
8
|
import { KIT_WIRE } from './wire.js';
|
|
10
9
|
// Two quiet seconds let a fresh conversation start before background model discovery competes for its CPU.
|
|
11
10
|
const MODEL_PROBE_QUIET_MS = 2_000;
|
|
11
|
+
// A CLI's maker can publish several releases a day, so running each chosen CLI's update command hourly brings a new one to the next conversation within the hour for one small check.
|
|
12
|
+
export const CLI_UPDATE_MS = 60 * 60_000;
|
|
12
13
|
// A sign-in's model read that has not answered in ten seconds leaves its sign-in to the status check, so a hung CLI never holds the sign-in's result.
|
|
13
14
|
const SIGNED_IN_READ_MS = 10_000;
|
|
14
15
|
const SIGNED_OUT = { signed_in: false, models: [], modes: [] };
|
|
@@ -165,13 +166,15 @@ export class Agents {
|
|
|
165
166
|
sending = Promise.resolve();
|
|
166
167
|
sent = null;
|
|
167
168
|
refreshing = 0;
|
|
169
|
+
loaded = Promise.resolve();
|
|
170
|
+
updating = null;
|
|
168
171
|
changed;
|
|
169
172
|
constructor(house, changed = () => undefined) {
|
|
170
173
|
this.house = house;
|
|
171
174
|
this.changed = changed;
|
|
172
175
|
}
|
|
173
176
|
idle() {
|
|
174
|
-
return this.refreshing === 0;
|
|
177
|
+
return this.refreshing === 0 && this.updating === null;
|
|
175
178
|
}
|
|
176
179
|
read() {
|
|
177
180
|
return this.reading.then(() => undefined, () => undefined);
|
|
@@ -187,7 +190,7 @@ export class Agents {
|
|
|
187
190
|
return this.desired;
|
|
188
191
|
});
|
|
189
192
|
this.reading = reading;
|
|
190
|
-
reading
|
|
193
|
+
this.loaded = reading
|
|
191
194
|
.then(({ agents, transcribe }) => Promise.all([
|
|
192
195
|
holdTranscription(transcribe)
|
|
193
196
|
.then((changed) => (changed ? this.report() : undefined))
|
|
@@ -203,6 +206,10 @@ export class Agents {
|
|
|
203
206
|
});
|
|
204
207
|
return reading.then(() => undefined);
|
|
205
208
|
}
|
|
209
|
+
keepCurrent() {
|
|
210
|
+
this.updateAll(this.loaded);
|
|
211
|
+
setInterval(() => this.updateAll(Promise.resolve()), CLI_UPDATE_MS);
|
|
212
|
+
}
|
|
206
213
|
cli(kind) {
|
|
207
214
|
this.starts += 1;
|
|
208
215
|
return this.queue(kind, async () => {
|
|
@@ -281,9 +288,6 @@ export class Agents {
|
|
|
281
288
|
return signal === undefined ? queued : Promise.race([queued, ended(signal)]);
|
|
282
289
|
}
|
|
283
290
|
async ensure(kind, signal, owner = {}) {
|
|
284
|
-
const failed = await installAdapter(kind, signal);
|
|
285
|
-
if (failed !== null)
|
|
286
|
-
throw new Error(failed);
|
|
287
291
|
const found = await locate(kind, signal);
|
|
288
292
|
if (found !== null)
|
|
289
293
|
return found;
|
|
@@ -302,7 +306,7 @@ export class Agents {
|
|
|
302
306
|
async resolve(kind, signal, owner) {
|
|
303
307
|
let cli;
|
|
304
308
|
try {
|
|
305
|
-
cli = await
|
|
309
|
+
cli = await this.ensure(kind, signal, owner);
|
|
306
310
|
}
|
|
307
311
|
catch (error) {
|
|
308
312
|
const failure = causeOf(error);
|
|
@@ -321,6 +325,35 @@ export class Agents {
|
|
|
321
325
|
if (this.reads.get(kind) === current && !signal?.aborted)
|
|
322
326
|
this.hold(kind, reading);
|
|
323
327
|
}
|
|
328
|
+
updateAll(after) {
|
|
329
|
+
if (this.updating !== null)
|
|
330
|
+
return;
|
|
331
|
+
this.updating = after
|
|
332
|
+
.then(async () => {
|
|
333
|
+
for (const kind of this.desired.agents) {
|
|
334
|
+
if (this.installs.has(kind) || !this.readings.get(kind)?.release)
|
|
335
|
+
continue;
|
|
336
|
+
const failure = await updateCli(kind);
|
|
337
|
+
if (failure !== null)
|
|
338
|
+
process.stderr.write(`kit: ${kind} did not update: ${failure}\n`);
|
|
339
|
+
await this.readVersion(kind);
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
.catch(logged)
|
|
343
|
+
.finally(() => {
|
|
344
|
+
this.updating = null;
|
|
345
|
+
this.changed();
|
|
346
|
+
});
|
|
347
|
+
this.changed();
|
|
348
|
+
}
|
|
349
|
+
async readVersion(kind) {
|
|
350
|
+
const cli = await locate(kind);
|
|
351
|
+
const release = cli === null ? null : await versionOf(cli);
|
|
352
|
+
const current = this.readings.get(kind);
|
|
353
|
+
if (release === null || !current?.release || current.release === release)
|
|
354
|
+
return;
|
|
355
|
+
this.update(kind, { ...current, release });
|
|
356
|
+
}
|
|
324
357
|
async quietOffer(kind, cli, known) {
|
|
325
358
|
for (let seen = -1; seen !== this.starts;) {
|
|
326
359
|
seen = this.starts;
|
|
@@ -347,7 +380,6 @@ export class Agents {
|
|
|
347
380
|
...(form === undefined ? {} : { form }),
|
|
348
381
|
kit_release: KIT_RELEASE,
|
|
349
382
|
wire: KIT_WIRE,
|
|
350
|
-
updates_itself: await updatesItself(),
|
|
351
383
|
agent_base: agentBase(),
|
|
352
384
|
agents,
|
|
353
385
|
transcription: transcriptionReport(),
|
package/dist/building.js
CHANGED
|
@@ -16,10 +16,14 @@ async function served(name, signal) {
|
|
|
16
16
|
await answer.body?.cancel();
|
|
17
17
|
throw new Error(`House answered ${answer.status} for ${name}`);
|
|
18
18
|
}
|
|
19
|
-
export async function
|
|
19
|
+
export async function servedRelease(signal) {
|
|
20
20
|
const manifest = (await (await served(MANIFEST, signal)).json());
|
|
21
|
-
const
|
|
22
|
-
|
|
21
|
+
const installer = manifest.hosts?.linux;
|
|
22
|
+
const sha256 = typeof installer === 'string' ? manifest.files?.[installer]?.sha256 : undefined;
|
|
23
|
+
if (typeof manifest.version !== 'string' || typeof installer !== 'string' || typeof sha256 !== 'string') {
|
|
24
|
+
throw new Error(`House serves no Kit release in ${MANIFEST}`);
|
|
25
|
+
}
|
|
26
|
+
return { version: manifest.version, installer, sha256 };
|
|
23
27
|
}
|
|
24
28
|
export async function downloadServed(name, sha256, destination, signal, most = Infinity) {
|
|
25
29
|
const answer = await served(name, signal);
|
package/dist/chunks.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { downloaded, fetched } from './files.js';
|
|
4
|
+
const DOWNLOADS_AT_ONCE = 6;
|
|
5
|
+
// A chunk is about one megabyte, so a download still unfinished after two minutes holds every Room behind it on a dead connection.
|
|
6
|
+
const CHUNK_DOWNLOAD_MS = 120_000;
|
|
7
|
+
class AddressExpired extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super("House's store refused every chunk it asked for");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function once(chunk) {
|
|
13
|
+
let asked = false;
|
|
14
|
+
return async () => {
|
|
15
|
+
if (asked)
|
|
16
|
+
throw new AddressExpired();
|
|
17
|
+
asked = true;
|
|
18
|
+
return { download: chunk.download, sha256: chunk.hash };
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function stagedChunks(directory, chunks) {
|
|
22
|
+
let next = 0;
|
|
23
|
+
let failure;
|
|
24
|
+
const downloading = async () => {
|
|
25
|
+
while (next < chunks.length && failure === undefined) {
|
|
26
|
+
const chunk = chunks[next++];
|
|
27
|
+
const reading = (url) => fetched(url, AbortSignal.timeout(CHUNK_DOWNLOAD_MS));
|
|
28
|
+
await downloaded(once(chunk), reading, join(directory, chunk.hash), 'A Room chunk').catch((error) => {
|
|
29
|
+
failure ??= error;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
await Promise.all(Array.from({ length: DOWNLOADS_AT_ONCE }, downloading));
|
|
34
|
+
return failure;
|
|
35
|
+
}
|
|
36
|
+
export async function* chunkTexts(directory, chunks, models) {
|
|
37
|
+
yield* models;
|
|
38
|
+
for (const { hash } of chunks) {
|
|
39
|
+
const file = join(directory, hash);
|
|
40
|
+
for (const line of (await readFile(file, 'utf8')).split('\n')) {
|
|
41
|
+
if (line === '')
|
|
42
|
+
continue;
|
|
43
|
+
const entry = JSON.parse(line);
|
|
44
|
+
if (entry.content !== undefined)
|
|
45
|
+
yield entry;
|
|
46
|
+
}
|
|
47
|
+
await rm(file);
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/clis.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
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,
|
|
4
|
+
import { access, mkdir, readFile, rename, 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 {
|
|
8
|
-
import { kitHome } from './home.js';
|
|
7
|
+
import { releaseDirectory } from './placement.js';
|
|
9
8
|
import { childEnvironment } from './relay.js';
|
|
10
9
|
const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
|
|
11
10
|
function houseRule(houses) {
|
|
@@ -63,6 +62,14 @@ export async function allowHouseInCodex(path) {
|
|
|
63
62
|
await rename(temporary, file);
|
|
64
63
|
}
|
|
65
64
|
async function nothing() { }
|
|
65
|
+
function claudeQuestionFields(request) {
|
|
66
|
+
const properties = request.requestedSchema?.properties ?? {};
|
|
67
|
+
return Object.keys(properties).filter((name) => properties[name]._meta?.jetbrains?.air?.customAnswer !== undefined);
|
|
68
|
+
}
|
|
69
|
+
function codexQuestionFields(request) {
|
|
70
|
+
const { _meta, requestedSchema } = request;
|
|
71
|
+
return _meta?.codex === undefined ? [] : (requestedSchema?.required ?? []);
|
|
72
|
+
}
|
|
66
73
|
function answered(error) {
|
|
67
74
|
return typeof error === 'object' && error !== null ? error : {};
|
|
68
75
|
}
|
|
@@ -90,6 +97,7 @@ export const CLIS = {
|
|
|
90
97
|
},
|
|
91
98
|
renames: true,
|
|
92
99
|
allowHouse: allowHouseInCodex,
|
|
100
|
+
questionFields: codexQuestionFields,
|
|
93
101
|
signedIn: { command: ['login', 'status'] },
|
|
94
102
|
login: { args: ['login', '--device-auth'], code: 'show' },
|
|
95
103
|
air: ['asyncTasks', 'nativeSubagentSessions'],
|
|
@@ -121,6 +129,7 @@ export const CLIS = {
|
|
|
121
129
|
unknownSession: (error) => answered(error).code === -32002,
|
|
122
130
|
renames: false,
|
|
123
131
|
allowHouse: nothing,
|
|
132
|
+
questionFields: claudeQuestionFields,
|
|
124
133
|
signedIn: { command: ['auth', 'status'] },
|
|
125
134
|
login: { args: ['auth', 'login', '--claudeai'], code: 'collect', rejected: 'Invalid code' },
|
|
126
135
|
air: ['asyncTasks', 'sessionFailure'],
|
|
@@ -137,6 +146,7 @@ export const CLIS = {
|
|
|
137
146
|
unknownSession: (error) => answered(error).data?.code === 'FS_NOT_FOUND',
|
|
138
147
|
renames: false,
|
|
139
148
|
allowHouse: nothing,
|
|
149
|
+
questionFields: () => [],
|
|
140
150
|
signedIn: { initializeMeta: 'defaultAuthMethodId' },
|
|
141
151
|
login: { args: ['login', '--device-auth'], code: 'show' },
|
|
142
152
|
air: ['asyncTasks'],
|
|
@@ -145,25 +155,13 @@ export const CLIS = {
|
|
|
145
155
|
const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
146
156
|
export const KIT_RELEASE = KIT_PACKAGE.version;
|
|
147
157
|
const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
|
|
148
|
-
// An install still running after fifteen minutes is ended
|
|
158
|
+
// An install or update still running after fifteen minutes is ended as failed, so a hung download can neither hold its CLI's report nor keep the Kit busy.
|
|
149
159
|
const INSTALL_MS = 15 * 60_000;
|
|
150
160
|
// A login shell or a version read is ended after half a minute, so a profile that waits for input cannot stall the report.
|
|
151
161
|
const READ_MS = 30_000;
|
|
152
162
|
const CAUSE_CHARACTERS = 4000;
|
|
153
|
-
function prefix(kind) {
|
|
154
|
-
return join(kitHome(), 'agents', kind);
|
|
155
|
-
}
|
|
156
163
|
export function adapterCommand(kind) {
|
|
157
|
-
return join(
|
|
158
|
-
}
|
|
159
|
-
async function installedAdapter(kind) {
|
|
160
|
-
try {
|
|
161
|
-
const manifest = join(prefix(kind), 'node_modules', CLIS[kind].adapter.package, 'package.json');
|
|
162
|
-
return JSON.parse(await readFile(manifest, 'utf8')).version;
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
164
|
+
return join(releaseDirectory(KIT_RELEASE), 'adapters', kind, 'node_modules', '.bin', CLIS[kind].adapter.bin);
|
|
167
165
|
}
|
|
168
166
|
export function run(command, args, timeoutMs, detached = false, signal) {
|
|
169
167
|
return new Promise((resolve) => {
|
|
@@ -207,38 +205,15 @@ export function failureOf(ran, program) {
|
|
|
207
205
|
export function adapterArchive(kind) {
|
|
208
206
|
return `${kind}-${KIT_PACKAGE.peerDependencies[CLIS[kind].adapter.package]}.tar.gz`;
|
|
209
207
|
}
|
|
210
|
-
|
|
211
|
-
const adapter = CLIS[kind].adapter;
|
|
212
|
-
if (adapter === null)
|
|
213
|
-
return null;
|
|
214
|
-
if ((await installedAdapter(kind)) === KIT_PACKAGE.peerDependencies[adapter.package])
|
|
215
|
-
return null;
|
|
216
|
-
const name = adapterArchive(kind);
|
|
217
|
-
const archive = `${prefix(kind)}.tar.gz`;
|
|
208
|
+
async function bounded(program, work, signal) {
|
|
218
209
|
const deadline = AbortSignal.timeout(INSTALL_MS);
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return `House serves no ${name}`;
|
|
224
|
-
await rm(prefix(kind), { recursive: true, force: true });
|
|
225
|
-
await mkdir(prefix(kind), { recursive: true });
|
|
226
|
-
await downloadServed(name, sha256, archive, bounded);
|
|
227
|
-
const ran = await run('tar', ['-xzf', archive, '-C', prefix(kind)], INSTALL_MS, true, signal);
|
|
228
|
-
return ran.status === 0 ? null : failureOf(ran, 'tar');
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
if (deadline.aborted && signal?.aborted !== true)
|
|
232
|
-
return `${name} did not arrive within ${INSTALL_MS / 1000} seconds`;
|
|
233
|
-
return error instanceof Error ? error.message : String(error);
|
|
234
|
-
}
|
|
235
|
-
finally {
|
|
236
|
-
await rm(archive, { force: true });
|
|
237
|
-
}
|
|
210
|
+
const ran = await work(signal === undefined ? deadline : AbortSignal.any([signal, deadline]));
|
|
211
|
+
if (deadline.aborted && signal?.aborted !== true)
|
|
212
|
+
return `${program} did not end within ${INSTALL_MS / 1000} seconds`;
|
|
213
|
+
return ran.status === 0 ? null : failureOf(ran, program);
|
|
238
214
|
}
|
|
239
|
-
export
|
|
240
|
-
|
|
241
|
-
return ran.status === 0 ? null : failureOf(ran, 'the install');
|
|
215
|
+
export function installCli(kind, signal) {
|
|
216
|
+
return bounded('the install', (within) => run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, within), signal);
|
|
242
217
|
}
|
|
243
218
|
function loginShell() {
|
|
244
219
|
return process.env.SHELL || userInfo().shell;
|
|
@@ -246,8 +221,8 @@ function loginShell() {
|
|
|
246
221
|
const FOUND = 'house-kit-cli ';
|
|
247
222
|
const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
|
|
248
223
|
const USER_BIN = 'case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) PATH="$PATH:$HOME/.local/bin" ;; esac; export PATH';
|
|
249
|
-
function inLoginShell(script, argument = '', signal) {
|
|
250
|
-
return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`],
|
|
224
|
+
function inLoginShell(script, argument = '', signal, timeoutMs = READ_MS) {
|
|
225
|
+
return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], timeoutMs, true, signal);
|
|
251
226
|
}
|
|
252
227
|
export async function onLoginPath(program, signal) {
|
|
253
228
|
const ran = await inLoginShell(LOOKUP, program, signal);
|
|
@@ -256,6 +231,9 @@ export async function onLoginPath(program, signal) {
|
|
|
256
231
|
export function locate(kind, signal) {
|
|
257
232
|
return onLoginPath(CLIS[kind].bin, signal);
|
|
258
233
|
}
|
|
234
|
+
export function updateCli(kind) {
|
|
235
|
+
return bounded('the update', (within) => inLoginShell(CLIS[kind].update, '', within, INSTALL_MS));
|
|
236
|
+
}
|
|
259
237
|
export async function loginPath() {
|
|
260
238
|
const marker = 'house-kit-path ';
|
|
261
239
|
const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
|
package/dist/connection.js
CHANGED
|
@@ -4,7 +4,7 @@ import { end, REPLACED, signInAgain } from './end.js';
|
|
|
4
4
|
import { readEnrolment } from './home.js';
|
|
5
5
|
import { relayed } from './relay.js';
|
|
6
6
|
import { Silence } from './silence.js';
|
|
7
|
-
import { endUnserved, KIT_WIRE } from './wire.js';
|
|
7
|
+
import { endUnserved, KIT_WIRE, unserved } from './wire.js';
|
|
8
8
|
const PATH = '/kit/stream';
|
|
9
9
|
const SUBPROTOCOL = `house.kit.stream.${KIT_WIRE}`;
|
|
10
10
|
const REFUSAL_HEADER = 'x-house-refusal';
|
|
@@ -32,11 +32,10 @@ subscribe('undici:request:headers', (message) => {
|
|
|
32
32
|
export function holdConnection(handlers) {
|
|
33
33
|
let connection = null;
|
|
34
34
|
let socket = null;
|
|
35
|
-
let paused = false;
|
|
36
35
|
let failures = 0;
|
|
37
36
|
const connect = async () => {
|
|
38
37
|
const { house, credential } = (await readEnrolment());
|
|
39
|
-
if (
|
|
38
|
+
if (socket !== null || unserved())
|
|
40
39
|
return;
|
|
41
40
|
refused = null;
|
|
42
41
|
const opening = new WebSocket(relayed(connectionUrl(house)), {
|
|
@@ -65,8 +64,6 @@ export function holdConnection(handlers) {
|
|
|
65
64
|
endUnserved(refused.text);
|
|
66
65
|
if (!opened && refused !== null)
|
|
67
66
|
process.stderr.write(`kit: ${refused.message}\n`);
|
|
68
|
-
if (paused)
|
|
69
|
-
return;
|
|
70
67
|
// 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.
|
|
71
68
|
setTimeout(() => void connect(), opened ? 1000 + Math.random() * 2000 : retryDelay(failures++));
|
|
72
69
|
};
|
|
@@ -104,13 +101,5 @@ export function holdConnection(handlers) {
|
|
|
104
101
|
connection.send(JSON.stringify(notification));
|
|
105
102
|
return true;
|
|
106
103
|
},
|
|
107
|
-
pause() {
|
|
108
|
-
paused = true;
|
|
109
|
-
socket?.close();
|
|
110
|
-
return () => {
|
|
111
|
-
paused = false;
|
|
112
|
-
void connect();
|
|
113
|
-
};
|
|
114
|
-
},
|
|
115
104
|
};
|
|
116
105
|
}
|
package/dist/conversations.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { client, } from '@agentclientprotocol/sdk';
|
|
1
|
+
import { client, RequestError, } from '@agentclientprotocol/sdk';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { unlessAborted } from './abort.js';
|
|
6
6
|
import { signedOut, startAdapter } from './acp.js';
|
|
7
|
+
import { formAnswered, grokQuestionAnswered, permissionAllowed, PLAN_APPROVED } from './afk.js';
|
|
7
8
|
import { finalRefusal, HouseRefusal } from './api.js';
|
|
8
9
|
import { openBridge } from './bridge.js';
|
|
9
10
|
import { CLIS } from './clis.js';
|
|
@@ -62,6 +63,11 @@ const EXIT_PLAN_MODE = {
|
|
|
62
63
|
return value;
|
|
63
64
|
},
|
|
64
65
|
};
|
|
66
|
+
const GROK_QUESTION = {
|
|
67
|
+
parse(value) {
|
|
68
|
+
return value;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
65
71
|
export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
|
|
66
72
|
function causeOf(error) {
|
|
67
73
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -128,6 +134,7 @@ class Conversation {
|
|
|
128
134
|
commands = null;
|
|
129
135
|
options = null;
|
|
130
136
|
exitUntold = false;
|
|
137
|
+
afk = false;
|
|
131
138
|
context = null;
|
|
132
139
|
leftovers = new Set();
|
|
133
140
|
waiting = [];
|
|
@@ -177,6 +184,8 @@ export class Conversations {
|
|
|
177
184
|
call(call) {
|
|
178
185
|
void this.kit.calls.carry(call.input_id, () => {
|
|
179
186
|
const conversation = this.conversation(call.conversation_id);
|
|
187
|
+
if (call.kind === 'open' || call.kind === 'message')
|
|
188
|
+
conversation.afk = call.afk === true;
|
|
180
189
|
if (call.kind === 'kill') {
|
|
181
190
|
conversation.kills++;
|
|
182
191
|
conversation.killed.abort(new Error(KILLED));
|
|
@@ -622,9 +631,14 @@ export class Conversations {
|
|
|
622
631
|
const reader = readerFor(route.kind);
|
|
623
632
|
reader.session = sessionId;
|
|
624
633
|
const app = client({ name: '@agentshouse/kit' })
|
|
625
|
-
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
626
|
-
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
627
|
-
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal))
|
|
634
|
+
.onRequest('session/request_permission', ({ params, signal }) => conversation.afk ? permissionAllowed(params) : this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
635
|
+
.onRequest('elicitation/create', ({ params, signal }) => conversation.afk ? formAnswered(route.kind, params) : this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
636
|
+
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => conversation.afk ? PLAN_APPROVED : this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal))
|
|
637
|
+
.onRequest('_x.ai/ask_user_question', GROK_QUESTION, ({ params }) => {
|
|
638
|
+
if (!conversation.afk)
|
|
639
|
+
throw RequestError.methodNotFound('_x.ai/ask_user_question');
|
|
640
|
+
return grokQuestionAnswered(params);
|
|
641
|
+
});
|
|
628
642
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
629
643
|
const bridging = this.bridged(conversation);
|
|
630
644
|
const bridge = await step(unbridged, bridging);
|