@agentshouse/kit 0.2.1-c3e4a20bb3183 → 0.2.1-c4ea88ba9424e
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 +23 -24
- package/dist/acp.js +4 -1
- package/dist/agents.js +79 -38
- package/dist/api.js +32 -3
- package/dist/app-git.js +64 -0
- package/dist/bootstrap.json +10 -17
- package/dist/bridge.js +29 -19
- package/dist/clis.js +48 -17
- package/dist/conversations.js +12 -8
- package/dist/copies.js +34 -41
- package/dist/end.js +8 -0
- package/dist/files.js +2 -1
- package/dist/home.js +15 -2
- package/dist/house-main.js +11 -12
- package/dist/instructions.js +2 -2
- package/dist/kit-main.js +7 -1
- package/dist/login.js +6 -2
- package/dist/logout.js +21 -0
- package/dist/mcp.js +2 -1
- package/dist/refusals.js +12 -0
- package/dist/relay.js +10 -0
- package/dist/resident.js +26 -9
- package/dist/secret-input.js +4 -1
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +33 -8
- package/dist/skills.js +1 -1
- package/dist/stream.js +31 -5
- package/dist/transcription.js +54 -6
- package/dist/update.js +2 -0
- package/package.json +1 -1
package/dist/clis.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import { userInfo } from 'node:os';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir, userInfo } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { kitHome } from './home.js';
|
|
7
7
|
const RUNNING_JOB = new Set(['running', 'paused']);
|
|
8
|
+
async function allowHouseInCodex() {
|
|
9
|
+
const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
|
|
10
|
+
await mkdir(rules, { recursive: true });
|
|
11
|
+
await writeFile(join(rules, 'house.rules'), 'prefix_rule(pattern = ["house"], decision = "allow")\n');
|
|
12
|
+
}
|
|
13
|
+
async function nothing() { }
|
|
8
14
|
function threadStatus({ method, params }) {
|
|
9
15
|
if (method !== 'session/update' || params?.update?.sessionUpdate !== 'session_info_update')
|
|
10
16
|
return undefined;
|
|
@@ -36,6 +42,9 @@ export const CLIS = {
|
|
|
36
42
|
install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
|
|
37
43
|
adapter: { package: '@agentclientprotocol/codex-acp', bin: 'codex-acp', executable: 'CODEX_PATH', env: {} },
|
|
38
44
|
args: [],
|
|
45
|
+
fullAccess: 'agent-full-access',
|
|
46
|
+
sessionMeta: {},
|
|
47
|
+
allowHouse: allowHouseInCodex,
|
|
39
48
|
signedIn: { command: ['login', 'status'] },
|
|
40
49
|
login: { args: ['login', '--device-auth'], code: 'show' },
|
|
41
50
|
queues: false,
|
|
@@ -52,9 +61,12 @@ export const CLIS = {
|
|
|
52
61
|
package: '@agentclientprotocol/claude-agent-acp',
|
|
53
62
|
bin: 'claude-agent-acp',
|
|
54
63
|
executable: 'CLAUDE_CODE_EXECUTABLE',
|
|
55
|
-
env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp' },
|
|
64
|
+
env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp', IS_SANDBOX: '1' },
|
|
56
65
|
},
|
|
57
66
|
args: [],
|
|
67
|
+
fullAccess: 'bypassPermissions',
|
|
68
|
+
sessionMeta: { claudeCode: { options: { allowedTools: ['Bash(house:*)'] } } },
|
|
69
|
+
allowHouse: nothing,
|
|
58
70
|
signedIn: { command: ['auth', 'status'] },
|
|
59
71
|
login: { args: ['auth', 'login', '--claudeai'], code: 'collect', rejected: 'Invalid code' },
|
|
60
72
|
queues: false,
|
|
@@ -70,7 +82,10 @@ export const CLIS = {
|
|
|
70
82
|
minimum: '1.0.46',
|
|
71
83
|
install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
|
|
72
84
|
adapter: null,
|
|
73
|
-
args: ['agent', '--no-leader', 'stdio'],
|
|
85
|
+
args: ['agent', '--always-approve', '--no-leader', 'stdio'],
|
|
86
|
+
fullAccess: null,
|
|
87
|
+
sessionMeta: {},
|
|
88
|
+
allowHouse: nothing,
|
|
74
89
|
signedIn: { initializeMeta: 'defaultAuthMethodId' },
|
|
75
90
|
login: { args: ['login', '--device-auth'], code: 'show' },
|
|
76
91
|
queues: true,
|
|
@@ -90,7 +105,9 @@ const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.me
|
|
|
90
105
|
export const KIT_VERSION = KIT_PACKAGE.version;
|
|
91
106
|
const PROXY = /^https?_proxy$/i;
|
|
92
107
|
const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
|
|
108
|
+
// An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
|
|
93
109
|
const INSTALL_MS = 15 * 60_000;
|
|
110
|
+
// 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
111
|
const READ_MS = 30_000;
|
|
95
112
|
const CAUSE_CHARACTERS = 4000;
|
|
96
113
|
function prefix(kind) {
|
|
@@ -118,8 +135,22 @@ export function run(command, args, timeoutMs, detached = false, signal) {
|
|
|
118
135
|
timeout: timeoutMs,
|
|
119
136
|
env: withoutProxy(),
|
|
120
137
|
detached,
|
|
121
|
-
signal,
|
|
138
|
+
signal: detached ? undefined : signal,
|
|
122
139
|
});
|
|
140
|
+
const stop = () => {
|
|
141
|
+
try {
|
|
142
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
143
|
+
}
|
|
144
|
+
catch { }
|
|
145
|
+
};
|
|
146
|
+
if (detached && signal?.aborted)
|
|
147
|
+
stop();
|
|
148
|
+
if (detached)
|
|
149
|
+
signal?.addEventListener('abort', stop, { once: true });
|
|
150
|
+
const ended = (ran) => {
|
|
151
|
+
signal?.removeEventListener('abort', stop);
|
|
152
|
+
resolve(ran);
|
|
153
|
+
};
|
|
123
154
|
let stdout = '';
|
|
124
155
|
let output = '';
|
|
125
156
|
child.stdout.setEncoding('utf8').on('data', (chunk) => {
|
|
@@ -129,25 +160,25 @@ export function run(command, args, timeoutMs, detached = false, signal) {
|
|
|
129
160
|
child.stderr.setEncoding('utf8').on('data', (chunk) => {
|
|
130
161
|
output += chunk;
|
|
131
162
|
});
|
|
132
|
-
child.on('error', (error) =>
|
|
133
|
-
child.on('close', (status) =>
|
|
163
|
+
child.on('error', (error) => ended({ status: null, stdout, output: error.message }));
|
|
164
|
+
child.on('close', (status) => ended({ status, stdout, output }));
|
|
134
165
|
});
|
|
135
166
|
}
|
|
136
167
|
export function failureOf(ran, program) {
|
|
137
168
|
return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
|
|
138
169
|
}
|
|
139
|
-
export async function installAdapter(kind) {
|
|
170
|
+
export async function installAdapter(kind, signal) {
|
|
140
171
|
const adapter = CLIS[kind].adapter;
|
|
141
172
|
if (adapter === null)
|
|
142
173
|
return null;
|
|
143
174
|
const release = KIT_PACKAGE.peerDependencies[adapter.package];
|
|
144
175
|
if ((await installedAdapter(kind)) === release)
|
|
145
176
|
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);
|
|
177
|
+
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
178
|
return ran.status === 0 ? null : failureOf(ran, 'npm');
|
|
148
179
|
}
|
|
149
|
-
export async function installCli(kind) {
|
|
150
|
-
const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true);
|
|
180
|
+
export async function installCli(kind, signal) {
|
|
181
|
+
const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
|
|
151
182
|
return ran.status === 0 ? null : failureOf(ran, 'the install');
|
|
152
183
|
}
|
|
153
184
|
function loginShell() {
|
|
@@ -156,11 +187,11 @@ function loginShell() {
|
|
|
156
187
|
const FOUND = 'house-kit-cli ';
|
|
157
188
|
const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
|
|
158
189
|
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);
|
|
190
|
+
function inLoginShell(script, argument = '', signal) {
|
|
191
|
+
return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
|
|
161
192
|
}
|
|
162
|
-
export async function locate(kind) {
|
|
163
|
-
const ran = await inLoginShell(LOOKUP, CLIS[kind].bin);
|
|
193
|
+
export async function locate(kind, signal) {
|
|
194
|
+
const ran = await inLoginShell(LOOKUP, CLIS[kind].bin, signal);
|
|
164
195
|
return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
|
|
165
196
|
}
|
|
166
197
|
export async function loginPath() {
|
|
@@ -168,8 +199,8 @@ export async function loginPath() {
|
|
|
168
199
|
const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
|
|
169
200
|
return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
|
|
170
201
|
}
|
|
171
|
-
export async function versionOf(path) {
|
|
172
|
-
return VERSION.exec((await run(path, ['--version'], READ_MS)).stdout)?.[0] ?? null;
|
|
202
|
+
export async function versionOf(path, signal) {
|
|
203
|
+
return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
|
|
173
204
|
}
|
|
174
205
|
function numbers(version) {
|
|
175
206
|
return version.split(/[.-]/, 3).map(Number);
|
package/dist/conversations.js
CHANGED
|
@@ -209,7 +209,7 @@ export class Conversations {
|
|
|
209
209
|
continue;
|
|
210
210
|
try {
|
|
211
211
|
const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
|
|
212
|
-
if (text !== '')
|
|
212
|
+
if (text !== null && text !== '')
|
|
213
213
|
lines.push(`Transcript: ${text}`);
|
|
214
214
|
}
|
|
215
215
|
catch (error) {
|
|
@@ -285,13 +285,15 @@ export class Conversations {
|
|
|
285
285
|
conversation.opening = bridge;
|
|
286
286
|
try {
|
|
287
287
|
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
288
|
+
await CLIS[route.kind].allowHouse();
|
|
288
289
|
const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
|
|
289
290
|
void adapter.exited.then(() => bridge.close());
|
|
290
291
|
const agent = adapter.connection.agent;
|
|
291
292
|
const cwd = route.working_directory;
|
|
293
|
+
const _meta = CLIS[route.kind].sessionMeta;
|
|
292
294
|
const opened = sessionId === null
|
|
293
|
-
? await agent.request('session/new', { cwd, mcpServers: [] })
|
|
294
|
-
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [] })), sessionId };
|
|
295
|
+
? await agent.request('session/new', { cwd, mcpServers: [], _meta })
|
|
296
|
+
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
|
|
295
297
|
const running = {
|
|
296
298
|
adapter,
|
|
297
299
|
bridge,
|
|
@@ -321,6 +323,7 @@ export class Conversations {
|
|
|
321
323
|
async launchSettings(running, route, offered) {
|
|
322
324
|
let options = offered;
|
|
323
325
|
const settings = [
|
|
326
|
+
['mode', route.mode ?? CLIS[route.kind].fullAccess],
|
|
324
327
|
['model', route.model],
|
|
325
328
|
['thought_level', route.effort],
|
|
326
329
|
];
|
|
@@ -389,10 +392,8 @@ export class Conversations {
|
|
|
389
392
|
turn.admitted = true;
|
|
390
393
|
for (const note of turn.notes.splice(0))
|
|
391
394
|
this.kit.send(note);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
this.draft(conversation, turn);
|
|
395
|
-
}
|
|
395
|
+
turn.unsentFrom = 0;
|
|
396
|
+
this.draft(conversation, turn);
|
|
396
397
|
if (turn.entries !== null)
|
|
397
398
|
this.plan(conversation, turn);
|
|
398
399
|
}
|
|
@@ -442,8 +443,11 @@ export class Conversations {
|
|
|
442
443
|
this.attach(conversation.turn ?? this.begin(conversation), sent);
|
|
443
444
|
}
|
|
444
445
|
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
|
|
445
|
-
if (!running.adapter.connection.signal.aborted)
|
|
446
|
+
if (!running.adapter.connection.signal.aborted) {
|
|
446
447
|
this.answered(conversation, sent, causeOf(error));
|
|
448
|
+
if (conversation.open === 0 && conversation.running === running)
|
|
449
|
+
stop(running.bridge);
|
|
450
|
+
}
|
|
447
451
|
else if (sent.turn !== null) {
|
|
448
452
|
this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
|
|
449
453
|
}
|
package/dist/copies.js
CHANGED
|
@@ -2,15 +2,15 @@ import { parse } from '@agentshouse/mdmodel';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import { HouseRefusal } from './api.js';
|
|
5
|
+
import { finalRefusal, HouseRefusal, retrying } from './api.js';
|
|
6
6
|
import { blobId, blobText, git, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
|
|
7
7
|
import { kitHome } from './home.js';
|
|
8
8
|
import { operationId } from './operation.js';
|
|
9
9
|
import { refusalOf } from './refusals.js';
|
|
10
10
|
import { changed } from './translate.js';
|
|
11
|
+
import { relayed } from './relay.js';
|
|
11
12
|
const BRANCH = 'main';
|
|
12
13
|
const PROVENANCE = '_provenance';
|
|
13
|
-
const REPLICA_RETRY_MS = 30_000;
|
|
14
14
|
const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
|
|
15
15
|
function causeOf(error) {
|
|
16
16
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -40,11 +40,11 @@ function sidecar(path) {
|
|
|
40
40
|
segments.splice(-1, 0, PROVENANCE);
|
|
41
41
|
return segments.join('/');
|
|
42
42
|
}
|
|
43
|
-
function rootOf(
|
|
44
|
-
return
|
|
43
|
+
function rootOf(handle) {
|
|
44
|
+
return `/rooms/${handle}`;
|
|
45
45
|
}
|
|
46
46
|
function relative(path) {
|
|
47
|
-
return path.replace(/^\/
|
|
47
|
+
return path.replace(/^\/rooms\/[^/]+\//, '');
|
|
48
48
|
}
|
|
49
49
|
function declaredWritable(path, copy) {
|
|
50
50
|
const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
|
|
@@ -160,11 +160,11 @@ export class LocalCopies {
|
|
|
160
160
|
key: 'owner',
|
|
161
161
|
submit: async (operation, changes) => {
|
|
162
162
|
try {
|
|
163
|
-
const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
|
|
163
|
+
const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
|
|
164
164
|
return { accepted: outcome.contents };
|
|
165
165
|
}
|
|
166
166
|
catch (error) {
|
|
167
|
-
if (!(error
|
|
167
|
+
if (!finalRefusal(error))
|
|
168
168
|
throw error;
|
|
169
169
|
return {
|
|
170
170
|
refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
|
|
@@ -176,7 +176,7 @@ export class LocalCopies {
|
|
|
176
176
|
async load() {
|
|
177
177
|
const home = join(kitHome(), 'local-copies');
|
|
178
178
|
const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
|
|
179
|
-
for (const path of
|
|
179
|
+
for (const path of names.map((name) => join(home, 'rooms', name))) {
|
|
180
180
|
const text = await readFile(path, 'utf8').catch(() => null);
|
|
181
181
|
if (text === null)
|
|
182
182
|
continue;
|
|
@@ -188,7 +188,7 @@ export class LocalCopies {
|
|
|
188
188
|
await this.selecting;
|
|
189
189
|
return [...this.copies.values()]
|
|
190
190
|
.filter((copy) => copy.active)
|
|
191
|
-
.map((copy) => `${rootOf(copy)}: ${copy.repository}`)
|
|
191
|
+
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
192
192
|
.sort();
|
|
193
193
|
}
|
|
194
194
|
select() {
|
|
@@ -198,12 +198,7 @@ export class LocalCopies {
|
|
|
198
198
|
}
|
|
199
199
|
async reselect() {
|
|
200
200
|
const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
|
|
201
|
-
const selected = [
|
|
202
|
-
...(chosen?.rooms ?? [])
|
|
203
|
-
.filter((room) => room.room_handle !== null)
|
|
204
|
-
.map((room) => ({ key: `rooms/${room.room_ref}`, private: false, handle: room.room_handle })),
|
|
205
|
-
...(chosen?.private === true ? [{ key: 'private', private: true, handle: null }] : []),
|
|
206
|
-
];
|
|
201
|
+
const selected = (chosen?.rooms ?? []).flatMap((room) => room.room_handle === null ? [] : [{ key: `rooms/${room.room_ref}`, handle: room.room_handle }]);
|
|
207
202
|
const keys = new Set(selected.map((scope) => scope.key));
|
|
208
203
|
const work = [];
|
|
209
204
|
for (const key of this.copies.keys()) {
|
|
@@ -255,11 +250,11 @@ export class LocalCopies {
|
|
|
255
250
|
async push(cwd, selector, caller) {
|
|
256
251
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
257
252
|
if (located.status !== 0)
|
|
258
|
-
return { refused: true, text: `${cwd} is not a House Local copy; run house
|
|
253
|
+
return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
|
|
259
254
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
260
255
|
const key = await this.keyAt(dirname(common));
|
|
261
256
|
if (key === undefined)
|
|
262
|
-
return { refused: true, text: `${worktree} is not a House Local copy; run house
|
|
257
|
+
return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
|
|
263
258
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
264
259
|
}
|
|
265
260
|
async keyAt(repository) {
|
|
@@ -287,7 +282,7 @@ export class LocalCopies {
|
|
|
287
282
|
}).catch(logged);
|
|
288
283
|
}
|
|
289
284
|
async save(copy) {
|
|
290
|
-
const path = join(kitHome(), 'local-copies',
|
|
285
|
+
const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
|
|
291
286
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
292
287
|
const temporary = `${path}.${process.pid}`;
|
|
293
288
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -295,12 +290,12 @@ export class LocalCopies {
|
|
|
295
290
|
this.copies.set(copy.key, copy);
|
|
296
291
|
return copy;
|
|
297
292
|
}
|
|
298
|
-
async read(
|
|
299
|
-
const prefixes = [rootOf(
|
|
293
|
+
async read(handle) {
|
|
294
|
+
const prefixes = [rootOf(handle)];
|
|
300
295
|
const discovery = await this.house.post('/kit/door/discover', { prefixes });
|
|
301
296
|
let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
|
|
302
297
|
if ('transfer' in bundle)
|
|
303
|
-
bundle = (await (await fetch(bundle.transfer.url)).json());
|
|
298
|
+
bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
|
|
304
299
|
const [view] = bundle.scopes;
|
|
305
300
|
if (view === undefined)
|
|
306
301
|
return null;
|
|
@@ -343,13 +338,13 @@ export class LocalCopies {
|
|
|
343
338
|
return;
|
|
344
339
|
}
|
|
345
340
|
const discovery = await this.house.post('/kit/door/discover', {
|
|
346
|
-
prefixes: [rootOf(copy)],
|
|
341
|
+
prefixes: [rootOf(copy.room_handle)],
|
|
347
342
|
});
|
|
348
343
|
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
349
344
|
if (authority === undefined)
|
|
350
345
|
return;
|
|
351
346
|
if (authority.managed_replication !== 'admitted') {
|
|
352
|
-
logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
|
|
347
|
+
logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
|
|
353
348
|
return;
|
|
354
349
|
}
|
|
355
350
|
if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
|
|
@@ -360,12 +355,12 @@ export class LocalCopies {
|
|
|
360
355
|
await this.save(await this.caughtUp(copy));
|
|
361
356
|
}
|
|
362
357
|
async materialize(scope) {
|
|
363
|
-
const read = await this.read(
|
|
358
|
+
const read = await this.read(scope.handle);
|
|
364
359
|
if (read === null) {
|
|
365
|
-
logged(`House serves nothing of ${rootOf(
|
|
360
|
+
logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
|
|
366
361
|
return;
|
|
367
362
|
}
|
|
368
|
-
const repository =
|
|
363
|
+
const repository = join(localCopies(), 'rooms', read.anchor);
|
|
369
364
|
if (await exists(repository)) {
|
|
370
365
|
logged(`${repository} already exists and House Kit leaves it as it is`);
|
|
371
366
|
return;
|
|
@@ -377,7 +372,7 @@ export class LocalCopies {
|
|
|
377
372
|
try {
|
|
378
373
|
await git(staging, ['init', '-q', '-b', BRANCH]);
|
|
379
374
|
house = await importCommits(staging, [
|
|
380
|
-
{ message: `House ${rootOf(
|
|
375
|
+
{ message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
|
|
381
376
|
]);
|
|
382
377
|
await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
|
|
383
378
|
await git(staging, ['reset', '-q', '--hard']);
|
|
@@ -391,7 +386,6 @@ export class LocalCopies {
|
|
|
391
386
|
}
|
|
392
387
|
await this.save({
|
|
393
388
|
key: scope.key,
|
|
394
|
-
private: scope.private,
|
|
395
389
|
room_ref: read.anchor,
|
|
396
390
|
room_handle: scope.handle,
|
|
397
391
|
repository,
|
|
@@ -407,11 +401,11 @@ export class LocalCopies {
|
|
|
407
401
|
});
|
|
408
402
|
}
|
|
409
403
|
async received(copy) {
|
|
410
|
-
const read = await this.read(copy);
|
|
404
|
+
const read = await this.read(copy.room_handle);
|
|
411
405
|
if (read === null)
|
|
412
406
|
return copy;
|
|
413
407
|
const house = await importCommits(copy.repository, [
|
|
414
|
-
{ message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
|
|
408
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
|
|
415
409
|
]);
|
|
416
410
|
const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
|
|
417
411
|
const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
|
|
@@ -434,7 +428,7 @@ export class LocalCopies {
|
|
|
434
428
|
let answer;
|
|
435
429
|
try {
|
|
436
430
|
answer = await this.house.post('/kit/door/enumerate', {
|
|
437
|
-
prefixes: [rootOf(copy)],
|
|
431
|
+
prefixes: [rootOf(copy.room_handle)],
|
|
438
432
|
positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
|
|
439
433
|
});
|
|
440
434
|
}
|
|
@@ -444,7 +438,6 @@ export class LocalCopies {
|
|
|
444
438
|
return this.received(copy);
|
|
445
439
|
if (code !== 'replica_behind')
|
|
446
440
|
throw error;
|
|
447
|
-
setTimeout(() => this.deliver(copy.key), REPLICA_RETRY_MS).unref();
|
|
448
441
|
return copy;
|
|
449
442
|
}
|
|
450
443
|
const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
|
|
@@ -484,7 +477,7 @@ export class LocalCopies {
|
|
|
484
477
|
let { house, manifests } = copy;
|
|
485
478
|
if (files.size > 0) {
|
|
486
479
|
house = await importCommits(copy.repository, [
|
|
487
|
-
{ message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
|
|
480
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
|
|
488
481
|
]);
|
|
489
482
|
manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
|
|
490
483
|
}
|
|
@@ -515,7 +508,7 @@ export class LocalCopies {
|
|
|
515
508
|
copy = await this.save(without(copy));
|
|
516
509
|
}
|
|
517
510
|
if (!copy.active) {
|
|
518
|
-
return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
|
|
511
|
+
return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
|
|
519
512
|
}
|
|
520
513
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
521
514
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
@@ -549,7 +542,7 @@ export class LocalCopies {
|
|
|
549
542
|
return this.settle(submitted, caller);
|
|
550
543
|
}
|
|
551
544
|
async derived(copy, base, commit) {
|
|
552
|
-
const root = rootOf(copy);
|
|
545
|
+
const root = rootOf(copy.room_handle);
|
|
553
546
|
const before = await tree(copy.repository, base);
|
|
554
547
|
const after = await tree(copy.repository, commit);
|
|
555
548
|
const moved = movedPaths(copy.moves, before, after);
|
|
@@ -597,7 +590,7 @@ export class LocalCopies {
|
|
|
597
590
|
const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
|
|
598
591
|
return {
|
|
599
592
|
refused: true,
|
|
600
|
-
text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house
|
|
593
|
+
text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house push again`,
|
|
601
594
|
};
|
|
602
595
|
}
|
|
603
596
|
if ('refused' in outcome) {
|
|
@@ -631,12 +624,12 @@ export class LocalCopies {
|
|
|
631
624
|
const push = copy.push;
|
|
632
625
|
const current = await this.save(await this.caughtUp(copy));
|
|
633
626
|
const house = await tree(copy.repository, current.house);
|
|
634
|
-
const root = rootOf(current);
|
|
627
|
+
const root = rootOf(current.room_handle);
|
|
635
628
|
if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
|
|
636
629
|
await this.save(without(current));
|
|
637
630
|
return {
|
|
638
631
|
refused: true,
|
|
639
|
-
text: `House does not hold ${short(push.commit)}; run house
|
|
632
|
+
text: `House does not hold ${short(push.commit)}; run house push again`,
|
|
640
633
|
};
|
|
641
634
|
}
|
|
642
635
|
const written = [];
|
|
@@ -664,7 +657,7 @@ export class LocalCopies {
|
|
|
664
657
|
catch {
|
|
665
658
|
return {
|
|
666
659
|
refused: false,
|
|
667
|
-
text: `House accepted ${short(commit)}; run house
|
|
660
|
+
text: `House accepted ${short(commit)}; run house push again to finish integrating it here`,
|
|
668
661
|
};
|
|
669
662
|
}
|
|
670
663
|
}
|
|
@@ -687,7 +680,7 @@ export class LocalCopies {
|
|
|
687
680
|
written = [
|
|
688
681
|
...written,
|
|
689
682
|
{
|
|
690
|
-
path: `${rootOf(copy)}/${path}`,
|
|
683
|
+
path: `${rootOf(copy.room_handle)}/${path}`,
|
|
691
684
|
revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
|
|
692
685
|
content: entry === undefined ? null : await blobText(copy.repository, entry, path),
|
|
693
686
|
},
|
|
@@ -707,7 +700,7 @@ export class LocalCopies {
|
|
|
707
700
|
}
|
|
708
701
|
const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
|
|
709
702
|
const received = [...files.keys()].filter((path) => revisionAt(copy, copy.house, path) === revisionAt(copy, push.house, path));
|
|
710
|
-
const accepted = `House ${rootOf(copy)} with ${push.commit} accepted`;
|
|
703
|
+
const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
|
|
711
704
|
const house = await importCommits(copy.repository, [
|
|
712
705
|
...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
|
|
713
706
|
{
|
package/dist/end.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { killDescendants } from './acp.js';
|
|
2
|
+
export const REPLACED = 'another Kit started for this Environment';
|
|
3
|
+
export const SIGN_IN_AGAIN = 'sign in again';
|
|
4
|
+
export function end(reason) {
|
|
5
|
+
process.stderr.write(`kit: ${reason}\n`);
|
|
6
|
+
killDescendants();
|
|
7
|
+
process.exit(0);
|
|
8
|
+
}
|
package/dist/files.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { relayed } from './relay.js';
|
|
3
4
|
export async function placeFiles(house, directory, files, signal) {
|
|
4
5
|
const paths = [];
|
|
5
6
|
for (const file of files) {
|
|
6
7
|
const original = await house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal);
|
|
7
|
-
const answer = await fetch(original.download.url, { signal });
|
|
8
|
+
const answer = await fetch(relayed(original.download.url), { signal });
|
|
8
9
|
if (!answer.ok)
|
|
9
10
|
throw new Error(`${file.name} did not download: ${answer.status}`);
|
|
10
11
|
const folder = join(directory, '.house', 'files', original.message, file.version);
|
package/dist/home.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
const ENROLMENT = 'credential.json';
|
|
6
|
+
const DISCONNECTED = 'disconnected.json';
|
|
5
7
|
const CONFIGURATION = 'kit.json';
|
|
6
8
|
const OWN_AGENT = 'own-agent.json';
|
|
7
9
|
export function kitHome() {
|
|
@@ -33,8 +35,19 @@ export async function writeHome(name, value) {
|
|
|
33
35
|
export function readEnrolment() {
|
|
34
36
|
return readHome(ENROLMENT);
|
|
35
37
|
}
|
|
36
|
-
export function writeEnrolment(enrolment) {
|
|
37
|
-
|
|
38
|
+
export async function writeEnrolment(enrolment) {
|
|
39
|
+
await writeHome(ENROLMENT, enrolment);
|
|
40
|
+
await rm(join(kitHome(), DISCONNECTED), { force: true });
|
|
41
|
+
}
|
|
42
|
+
export function readDisconnected() {
|
|
43
|
+
return readHome(DISCONNECTED);
|
|
44
|
+
}
|
|
45
|
+
export async function forgetEnrolment({ house, environment }) {
|
|
46
|
+
await writeHome(DISCONNECTED, { house, environment });
|
|
47
|
+
await rm(join(kitHome(), ENROLMENT), { force: true });
|
|
48
|
+
}
|
|
49
|
+
export function enrolled() {
|
|
50
|
+
return existsSync(join(kitHome(), ENROLMENT));
|
|
38
51
|
}
|
|
39
52
|
export function readOwnAgent() {
|
|
40
53
|
return readHome(OWN_AGENT);
|
package/dist/house-main.js
CHANGED
|
@@ -7,7 +7,8 @@ import { kitHome, readOwnAgent } from './home.js';
|
|
|
7
7
|
import { callHouse, rpc, UNREACHABLE } from './mcp.js';
|
|
8
8
|
import { refusalOf } from './refusals.js';
|
|
9
9
|
const USAGE = "usage: house <tool> ['<arguments as JSON>']";
|
|
10
|
-
const
|
|
10
|
+
const PUSH_USAGE = 'usage: house push [commit] [--owner]';
|
|
11
|
+
const PUSH = "push [commit]: sends the Local copy's committed changes to House";
|
|
11
12
|
const ARGUMENTS = `the arguments are one JSON object in single quotes, like house search '{"query":"invoice"}'`;
|
|
12
13
|
const CLOSED = "this conversation's House connection is closed; nothing to do from here";
|
|
13
14
|
const REVOKED = 'this conversation no longer has House access; nothing to do from here';
|
|
@@ -97,17 +98,15 @@ async function upload(path, roomRef, missing) {
|
|
|
97
98
|
throw new Error(`${path} was not saved: ${saved.upload_failure}; call again later`);
|
|
98
99
|
return saved.attachment;
|
|
99
100
|
}
|
|
100
|
-
async function
|
|
101
|
+
async function push(args) {
|
|
101
102
|
if (args.includes('--help'))
|
|
102
|
-
return
|
|
103
|
-
if (args[0] !== 'push')
|
|
104
|
-
throw new Error('only house git push [commit] exists; use plain git for the rest');
|
|
103
|
+
return PUSH_USAGE;
|
|
105
104
|
const owner = args.includes('--owner');
|
|
106
|
-
const commit = args.
|
|
105
|
+
const commit = args.find((arg) => arg !== '--owner') ?? 'HEAD';
|
|
107
106
|
if (owner && socketPath !== undefined)
|
|
108
|
-
throw new Error('--owner is not for Agent conversations; run house
|
|
107
|
+
throw new Error('--owner is not for Agent conversations; run house push without it');
|
|
109
108
|
if (!owner && socketPath === undefined)
|
|
110
|
-
throw new Error('outside an Agent conversation, house
|
|
109
|
+
throw new Error('outside an Agent conversation, house push needs --owner');
|
|
111
110
|
const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit }, undefined, owner ? join(kitHome(), 'owner.sock') : socketPath));
|
|
112
111
|
if (pushed.refused)
|
|
113
112
|
throw new Error(pushed.text);
|
|
@@ -128,14 +127,14 @@ function argumentsOf(argument) {
|
|
|
128
127
|
return parsed;
|
|
129
128
|
}
|
|
130
129
|
async function house([verb, argument]) {
|
|
131
|
-
if (verb === undefined || verb === '--help') {
|
|
132
|
-
return [USAGE, '
|
|
130
|
+
if (verb === undefined || verb === '--help' || verb === 'help') {
|
|
131
|
+
return [USAGE, 'help', PUSH, ...(await tools()).map((tool) => (tool.description === undefined ? tool.name : `${tool.name}: ${tool.description}`))].join('\n');
|
|
133
132
|
}
|
|
134
133
|
if (argument === '--help') {
|
|
135
134
|
const tool = (await tools()).find((listed) => listed.name === verb);
|
|
136
135
|
if (tool === undefined)
|
|
137
136
|
throw new Error(`${verb} is no Tool; house find_command '{"query":"${verb}"}' finds Commands`);
|
|
138
|
-
return
|
|
137
|
+
return [...(tool.description === undefined ? [] : [tool.description]), JSON.stringify(tool.inputSchema)].join('\n');
|
|
139
138
|
}
|
|
140
139
|
const args = argumentsOf(argument);
|
|
141
140
|
if (socketPath !== undefined && verb === 'upload_attachment') {
|
|
@@ -155,7 +154,7 @@ async function house([verb, argument]) {
|
|
|
155
154
|
return text;
|
|
156
155
|
}
|
|
157
156
|
const argv = process.argv.slice(2);
|
|
158
|
-
(argv[0] === '
|
|
157
|
+
(argv[0] === 'push' ? push(argv.slice(1)) : house(argv)).then((text) => {
|
|
159
158
|
process.stdout.write(`${text}\n`);
|
|
160
159
|
}, (error) => {
|
|
161
160
|
process.stderr.write(`house: ${error instanceof Error ? error.message : String(error)}\n`);
|
package/dist/instructions.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { HOW_WE_WORK } from './skills.js';
|
|
2
2
|
const HOUSE_LINE = 'Work with House through the `house` CLI: run `house --help`.';
|
|
3
3
|
const FILE_LINE = 'To give the User a file or a page, upload it with `house upload_attachment` and link it; never start a server.';
|
|
4
|
-
const READ_LINE = 'Before working in a Room, read `/private/rooms/<handle>/AGENTS.md`.';
|
|
5
|
-
const COPIES_LINE = 'House Local copies; commit, then `house
|
|
4
|
+
const READ_LINE = 'Before working in a Room, read `/rooms/private/rooms/<handle>/AGENTS.md`.';
|
|
5
|
+
const COPIES_LINE = 'House Local copies; commit, then `house push`:';
|
|
6
6
|
const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
|
|
7
7
|
const FAILED = /^exit: [1-9]\d*$/;
|
|
8
8
|
const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
|
package/dist/kit-main.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { setGlobalProxyFromEnv } from 'node:http';
|
|
3
|
+
import { answerGitCredential } from './app-git.js';
|
|
3
4
|
import { login } from './login.js';
|
|
5
|
+
import { logout } from './logout.js';
|
|
4
6
|
import { resident } from './resident.js';
|
|
5
7
|
const [command, ...argv] = process.argv.slice(2);
|
|
6
8
|
setGlobalProxyFromEnv();
|
|
7
9
|
async function run() {
|
|
8
10
|
if (command === 'login')
|
|
9
11
|
return login(argv);
|
|
12
|
+
if (command === 'logout')
|
|
13
|
+
return logout();
|
|
10
14
|
if (command === 'resident')
|
|
11
15
|
return resident();
|
|
12
|
-
|
|
16
|
+
if (command === 'git-credential')
|
|
17
|
+
return answerGitCredential(argv[0]);
|
|
18
|
+
process.stderr.write('usage: kit <login|logout|resident>\n');
|
|
13
19
|
process.exitCode = 2;
|
|
14
20
|
}
|
|
15
21
|
run().catch((error) => {
|