@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/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) => resolve({ status: null, stdout, output: error.message }));
133
- child.on('close', (status) => resolve({ status, stdout, output }));
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);
@@ -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
- if (turn.text !== '') {
393
- turn.unsentFrom = 0;
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(copy) {
44
- return copy.private ? '/private' : `/rooms/${copy.room_handle}`;
43
+ function rootOf(handle) {
44
+ return `/rooms/${handle}`;
45
45
  }
46
46
  function relative(path) {
47
- return path.replace(/^\/private\//, '').replace(/^\/rooms\/[^/]+\//, '');
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 instanceof HouseRefusal) || error.status >= 500)
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 [...names.map((name) => join(home, 'rooms', name)), join(home, 'private.json')]) {
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 git push inside one` };
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 git push inside one` };
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', copy.private ? 'private.json' : `${copy.key}.json`);
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(selected) {
299
- const prefixes = [rootOf(selected)];
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({ private: scope.private, room_handle: scope.handle });
358
+ const read = await this.read(scope.handle);
364
359
  if (read === null) {
365
- logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
360
+ logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
366
361
  return;
367
362
  }
368
- const repository = scope.private ? join(localCopies(), 'private') : join(localCopies(), 'rooms', read.anchor);
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({ private: scope.private, room_handle: scope.handle })}`, from: null, files: read.writable },
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 git push again`,
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 git push again`,
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 git push again to finish integrating it here`,
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
- return writeHome(ENROLMENT, enrolment);
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);
@@ -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 GIT_USAGE = 'usage: house git push [commit] [--owner]';
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 gitPush(args) {
101
+ async function push(args) {
101
102
  if (args.includes('--help'))
102
- return GIT_USAGE;
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.slice(1).find((arg) => arg !== '--owner') ?? 'HEAD';
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 git push without it');
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 git push needs --owner');
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, 'git push [commit]', ...(await tools()).map((tool) => `${tool.name}: ${tool.description}`)].join('\n');
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 `${tool.description}\n${JSON.stringify(tool.inputSchema)}`;
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] === 'git' ? gitPush(argv.slice(1)) : house(argv)).then((text) => {
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`);
@@ -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 git push`:';
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
- process.stderr.write('usage: kit <login|resident>\n');
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) => {