@agentshouse/kit 0.2.1-c3cf4b145dcda → 0.2.1-c46722ad48058

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/events.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const COMMAND = { shown: 'command' };
2
2
  const HIDDEN = { shown: 'hidden' };
3
3
  const FINISHED = new Set(['completed', 'failed']);
4
- const RUNNING_JOB = new Set(['running', 'paused']);
4
+ export const RUNNING_JOB = new Set(['running', 'paused']);
5
5
  const ROUTINE_TARGETS = { send_routine_request: 'access', cancel_routine_request: 'request' };
6
6
  function words(line) {
7
7
  const found = [];
@@ -113,9 +113,6 @@ class Reader {
113
113
  refused(option) {
114
114
  this.requestedOptions.delete(option);
115
115
  }
116
- turnStarted() {
117
- return [];
118
- }
119
116
  closeTurn() {
120
117
  const events = [];
121
118
  for (const [id, call] of this.calls) {
@@ -187,10 +184,7 @@ class Reader {
187
184
  const label = this.jobs.get(id) ?? '';
188
185
  if (state !== 'running')
189
186
  this.jobs.delete(id);
190
- return [
191
- { kind: 'busy', key: `job:${id}`, running: state === 'running' },
192
- { kind: 'marker', marker: 'job', text: `${label}: ${state}` },
193
- ];
187
+ return [{ kind: 'marker', marker: 'job', text: `${label}: ${state}` }];
194
188
  }
195
189
  optionsChanged(options) {
196
190
  const events = [];
@@ -275,7 +269,6 @@ class Reader {
275
269
  class ClaudeReader extends Reader {
276
270
  watchedOptions = { model: 'model' };
277
271
  tasks = new Map();
278
- scheduled = new Map();
279
272
  own(message) {
280
273
  if (message.params?.update?._meta?.claudeCode?.parentToolUseId !== undefined)
281
274
  return [];
@@ -295,10 +288,6 @@ class ClaudeReader extends Reader {
295
288
  return [];
296
289
  const sdk = message.params.message;
297
290
  switch (sdk.subtype) {
298
- case 'background_tasks_changed':
299
- return [{ kind: 'busy', key: 'tasks', running: sdk.tasks.length > 0 }];
300
- case 'session_state_changed':
301
- return [{ kind: 'busy', key: 'state', running: sdk.state === 'running' }];
302
291
  case 'task_started':
303
292
  if (sdk.tool_use_id !== undefined)
304
293
  this.tasks.set(sdk.task_id, sdk.tool_use_id);
@@ -309,16 +298,6 @@ class ClaudeReader extends Reader {
309
298
  return [];
310
299
  }
311
300
  }
312
- turnStarted() {
313
- const events = [];
314
- for (const [id, entry] of this.scheduled) {
315
- if (entry.recurring)
316
- continue;
317
- this.scheduled.delete(id);
318
- events.push({ kind: 'busy', key: `scheduled:${id}`, running: false });
319
- }
320
- return events;
321
- }
322
301
  decide(call, final) {
323
302
  const tool = call.meta.claudeCode?.toolName;
324
303
  if (tool === 'Agent' || tool === 'Task' || call.meta.jetbrains?.air?.subagent === true) {
@@ -350,22 +329,7 @@ class ClaudeReader extends Reader {
350
329
  if (call.status !== 'completed')
351
330
  return [];
352
331
  if (tool === 'CronCreate' || (tool === 'ScheduleWakeup' && call.rawInput?.stop !== true)) {
353
- this.scheduled.set(call.id, {
354
- recurring: tool === 'CronCreate' && call.rawInput?.recurring !== false,
355
- text: contentText(call.content) ?? '',
356
- });
357
- return [
358
- { kind: 'busy', key: `scheduled:${call.id}`, running: true },
359
- { kind: 'marker', marker: 'job', text: `${call.rawInput?.prompt ?? call.title ?? ''}: scheduled` },
360
- ];
361
- }
362
- if (tool === 'CronDelete') {
363
- for (const [id, entry] of this.scheduled) {
364
- if (!entry.text.includes(String(call.rawInput?.id)))
365
- continue;
366
- this.scheduled.delete(id);
367
- return [{ kind: 'busy', key: `scheduled:${id}`, running: false }];
368
- }
332
+ return [{ kind: 'marker', marker: 'job', text: `${call.rawInput?.prompt ?? call.title ?? ''}: scheduled` }];
369
333
  }
370
334
  return [];
371
335
  }
@@ -402,15 +366,9 @@ class CodexReader extends Reader {
402
366
  case 'subagent_spawned':
403
367
  this.children.set(update.subagentSessionId, { id: null, text: '' });
404
368
  this.subagents.add(update.subagentSessionId);
405
- return [
406
- { kind: 'busy', key: `subagent:${update.subagentSessionId}`, running: true },
407
- { kind: 'subagent', id: update.subagentSessionId, description: update.name },
408
- ];
369
+ return [{ kind: 'subagent', id: update.subagentSessionId, description: update.name }];
409
370
  case 'subagent_state_update':
410
- return [
411
- { kind: 'busy', key: `subagent:${update.subagentSessionId}`, running: false },
412
- ...this.finish(update.subagentSessionId, this.children.get(update.subagentSessionId)?.text || null),
413
- ];
371
+ return this.finish(update.subagentSessionId, this.children.get(update.subagentSessionId)?.text || null);
414
372
  default:
415
373
  return super.update(update);
416
374
  }
@@ -427,10 +385,6 @@ class CodexReader extends Reader {
427
385
  child.id = id;
428
386
  child.text += update.content.text;
429
387
  }
430
- const status = update._meta?.codex?.threadStatus?.type;
431
- if (update.sessionUpdate === 'session_info_update' && status !== undefined) {
432
- return [{ kind: 'busy', key: `thread:${session}`, running: status === 'active' }];
433
- }
434
388
  return [];
435
389
  }
436
390
  decide(call) {
@@ -460,7 +414,6 @@ class CodexReader extends Reader {
460
414
  class GrokReader extends Reader {
461
415
  watchedOptions = {};
462
416
  children = new Map();
463
- queues = new Map();
464
417
  models = [];
465
418
  model = null;
466
419
  answered(result) {
@@ -473,15 +426,10 @@ class GrokReader extends Reader {
473
426
  if (this.session !== null && session !== undefined && session !== this.session && !this.children.has(session))
474
427
  return [];
475
428
  if (message.method === '_x.ai/queue/changed') {
476
- const running = message.params.runningPromptId ?? null;
477
- this.queues.set(session, { running, entries: message.params.entries.length });
478
- return [this.queued(session), ...(running !== null && session === this.session ? [{ kind: 'started' }] : [])];
429
+ return (message.params.runningPromptId ?? null) !== null && session === this.session ? [{ kind: 'started' }] : [];
479
430
  }
480
431
  if (message.params?.update?.sessionUpdate === 'turn_completed') {
481
- const queue = this.queues.get(session);
482
- if (queue !== undefined && queue.running === message.params.update.prompt_id)
483
- queue.running = null;
484
- return [this.queued(session), ...(session === this.session ? [{ kind: 'ended' }] : [])];
432
+ return session === this.session ? [{ kind: 'ended' }] : [];
485
433
  }
486
434
  if (message.method === '_x.ai/models/update') {
487
435
  this.offered(message.params);
@@ -489,10 +437,6 @@ class GrokReader extends Reader {
489
437
  }
490
438
  return super.read(message);
491
439
  }
492
- queued(session) {
493
- const queue = this.queues.get(session);
494
- return { kind: 'busy', key: `queue:${session}`, running: queue !== undefined && (queue.running !== null || queue.entries > 0) };
495
- }
496
440
  offered(models) {
497
441
  this.models = models.availableModels ?? this.models;
498
442
  this.model = models.currentModelId ?? this.model;
@@ -508,16 +452,9 @@ class GrokReader extends Reader {
508
452
  case '_x.ai/task_completed':
509
453
  return this.job(update.task_snapshot.task_id, null, update.task_snapshot.exit_code === 0 ? 'completed' : 'failed');
510
454
  case '_x.ai/scheduled_task_created':
511
- return [
512
- { kind: 'busy', key: `scheduled:${update.task_id}`, running: true },
513
- { kind: 'marker', marker: 'job', text: `${update.prompt}: next run ${update.next_fire_at}` },
514
- ];
515
- case '_x.ai/scheduled_task_deleted':
516
- return [{ kind: 'busy', key: `scheduled:${update.task_id}`, running: false }];
455
+ return [{ kind: 'marker', marker: 'job', text: `${update.prompt}: next run ${update.next_fire_at}` }];
517
456
  case '_x.ai/session_notification':
518
457
  return this.notification(update);
519
- case '_x.ai/mcp_initialized':
520
- return [{ kind: 'servers' }];
521
458
  default:
522
459
  return [];
523
460
  }
@@ -538,14 +475,11 @@ class GrokReader extends Reader {
538
475
  candidate.decided.description === update.description) ??
539
476
  [...this.calls.values()].find((candidate) => candidate.decided?.shown === 'subagent' && ![...this.children.values()].includes(candidate.id));
540
477
  this.children.set(update.child_session_id, call?.id ?? null);
541
- return [{ kind: 'busy', key: `subagent:${update.child_session_id}`, running: true }];
478
+ return [];
542
479
  }
543
480
  case 'subagent_finished': {
544
481
  const call = this.children.get(update.child_session_id);
545
- return [
546
- { kind: 'busy', key: `subagent:${update.child_session_id}`, running: false },
547
- ...(typeof call === 'string' ? this.finish(call, update.output ?? null) : []),
548
- ];
482
+ return typeof call === 'string' ? this.finish(call, update.output ?? null) : [];
549
483
  }
550
484
  case 'auto_compact_completed':
551
485
  return [{ kind: 'marker', marker: 'compaction', text: '' }];
package/dist/home.js CHANGED
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
+ import { selectRelay } from './relay.js';
5
6
  const ENROLMENT = 'credential.json';
6
7
  const DISCONNECTED = 'disconnected.json';
7
8
  const CONFIGURATION = 'kit.json';
@@ -32,8 +33,11 @@ export async function writeHome(name, value) {
32
33
  await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
33
34
  await rename(temporary, join(home, name));
34
35
  }
35
- export function readEnrolment() {
36
- return readHome(ENROLMENT);
36
+ export async function readEnrolment() {
37
+ const enrolment = await readHome(ENROLMENT);
38
+ if (enrolment !== null)
39
+ selectRelay(process.env.HOUSE_KIT_RELAY || enrolment.relay);
40
+ return enrolment;
37
41
  }
38
42
  export async function writeEnrolment(enrolment) {
39
43
  await writeHome(ENROLMENT, enrolment);
@@ -3,15 +3,20 @@ import { createHash } from 'node:crypto';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { request, setGlobalProxyFromEnv } from 'node:http';
5
5
  import { basename, join } from 'node:path';
6
- import { kitHome, readOwnAgent } from './home.js';
6
+ import { HOUSE_REF } from './git.js';
7
+ import { kitHome, readEnrolment, readOwnAgent } from './home.js';
7
8
  import { callHouse, rpc, UNREACHABLE } from './mcp.js';
8
9
  import { refusalOf } from './refusals.js';
10
+ import { relayed } from './relay.js';
11
+ import { parse } from 'yaml';
9
12
  const USAGE = "usage: house <tool> ['<arguments as JSON>']";
10
13
  const PUSH_USAGE = 'usage: house push [commit] [--owner]';
11
14
  const PUSH = "push [commit]: sends the Local copy's committed changes to House";
15
+ const FETCH_USAGE = 'usage: house fetch';
16
+ const FETCH = `fetch: records House's changes on each Local copy's ${HOUSE_REF}`;
12
17
  const ARGUMENTS = `the arguments are one JSON object in single quotes, like house search '{"query":"invoice"}'`;
13
18
  const CLOSED = "this conversation's House connection is closed; nothing to do from here";
14
- const SANDBOXED = "your CLI's sandbox kept house from House; run house as a command of its own, with no pipe, chain or redirect";
19
+ const SANDBOXED = "your CLI's sandbox kept house from House; run house directly, not through another program";
15
20
  const REVOKED = 'this conversation no longer has House access; nothing to do from here';
16
21
  const UNCONNECTED = "this computer's own agent has no House connection; run kit login";
17
22
  const DISCONNECTED = "House no longer accepts this computer's own agent; run kit login";
@@ -51,6 +56,7 @@ async function direct(message) {
51
56
  const ownAgent = await readOwnAgent();
52
57
  if (ownAgent === null)
53
58
  throw new Error(UNCONNECTED);
59
+ await readEnrolment();
54
60
  setGlobalProxyFromEnv();
55
61
  const answer = await callHouse(ownAgent.house, ownAgent.credential, message).catch(() => {
56
62
  throw new Error(UNREACHABLE);
@@ -79,11 +85,24 @@ async function localFile(path) {
79
85
  : `${path} is not a readable file`);
80
86
  }
81
87
  }
88
+ async function sent(url, bytes, operation) {
89
+ const headers = { 'content-type': 'application/octet-stream', 'x-house-byte-operation': operation };
90
+ if (socketPath !== undefined)
91
+ return bridged(url, bytes, headers);
92
+ setGlobalProxyFromEnv();
93
+ const answer = await fetch(relayed(url), { method: 'POST', headers, body: bytes }).catch(() => {
94
+ throw new Error(UNREACHABLE);
95
+ });
96
+ const text = await answer.text();
97
+ if (answer.status === 200)
98
+ return text;
99
+ throw new Error(refused(url, answer.status, text));
100
+ }
82
101
  async function upload(path, roomRef, missing) {
83
102
  if (typeof path !== 'string' || path === '')
84
103
  throw new Error(missing);
85
104
  const bytes = await localFile(path);
86
- const declared = JSON.parse(await bridged('/kit/attachments/upload', {
105
+ const declared = parse(await called('upload', {
87
106
  ...(roomRef === undefined ? {} : { room_ref: roomRef }),
88
107
  name: basename(path),
89
108
  bytes: bytes.length,
@@ -91,10 +110,7 @@ async function upload(path, roomRef, missing) {
91
110
  }));
92
111
  if (declared.upload === undefined)
93
112
  return declared.attachment;
94
- const saved = JSON.parse(await bridged(declared.upload.url, bytes, {
95
- 'content-type': 'application/octet-stream',
96
- 'x-house-byte-operation': declared.upload.operation,
97
- }));
113
+ const saved = JSON.parse(await sent(declared.upload.url, bytes, declared.upload.operation));
98
114
  if (saved.upload_failure !== undefined)
99
115
  throw new Error(`${path} was not saved: ${saved.upload_failure}; call again later`);
100
116
  return saved.attachment;
@@ -113,6 +129,14 @@ async function push(args) {
113
129
  throw new Error(pushed.text);
114
130
  return pushed.text;
115
131
  }
132
+ async function fetched(args) {
133
+ if (args.includes('--help'))
134
+ return FETCH_USAGE;
135
+ const answer = JSON.parse(await bridged('/git/fetch', {}, undefined, socketPath ?? join(kitHome(), 'owner.sock')));
136
+ if (answer.refused)
137
+ throw new Error(answer.text);
138
+ return answer.text;
139
+ }
116
140
  function argumentsOf(argument) {
117
141
  if (argument === undefined)
118
142
  return {};
@@ -129,7 +153,7 @@ function argumentsOf(argument) {
129
153
  }
130
154
  async function house([verb, argument]) {
131
155
  if (verb === undefined || verb === '--help' || verb === 'help') {
132
- return [USAGE, 'help', PUSH, ...(await tools()).map((tool) => (tool.description === undefined ? tool.name : `${tool.name}: ${tool.description}`))].join('\n');
156
+ return [USAGE, 'help', PUSH, FETCH, ...(await tools()).map((tool) => (tool.description === undefined ? tool.name : `${tool.name}: ${tool.description}`))].join('\n');
133
157
  }
134
158
  if (argument === '--help') {
135
159
  const tool = (await tools()).find((listed) => listed.name === verb);
@@ -138,24 +162,27 @@ async function house([verb, argument]) {
138
162
  return [...(tool.description === undefined ? [] : [tool.description]), JSON.stringify(tool.inputSchema)].join('\n');
139
163
  }
140
164
  const args = argumentsOf(argument);
141
- if (socketPath !== undefined && verb === 'upload_attachment') {
142
- return JSON.stringify({ attachment: await upload(args.path, args.room_ref, 'upload_attachment needs "path", a local file') });
165
+ if (verb === 'upload') {
166
+ return JSON.stringify({ attachment: await upload(args.path, args.room_ref, 'upload needs "path", a local file') });
143
167
  }
144
- if (socketPath !== undefined && verb === 'append_record' && Array.isArray(args.attachments)) {
168
+ if (verb === 'append_record' && Array.isArray(args.attachments)) {
145
169
  const attachments = [];
146
170
  for (const path of args.attachments) {
147
171
  attachments.push(await upload(path, args.room_ref, 'each attachments entry is the path of a local file'));
148
172
  }
149
173
  args.attachments = attachments;
150
174
  }
151
- const result = await mcp('tools/call', { name: verb, arguments: args });
175
+ return called(verb, args);
176
+ }
177
+ async function called(name, args) {
178
+ const result = await mcp('tools/call', { name, arguments: args });
152
179
  const text = result.content.map((content) => content.text).join('\n');
153
180
  if (result.isError === true)
154
181
  throw new Error(text.replace(/\n+$/, ''));
155
182
  return text;
156
183
  }
157
184
  const argv = process.argv.slice(2);
158
- (argv[0] === 'push' ? push(argv.slice(1)) : house(argv)).then((text) => {
185
+ (argv[0] === 'push' ? push(argv.slice(1)) : argv[0] === 'fetch' ? fetched(argv.slice(1)) : house(argv)).then((text) => {
159
186
  process.stdout.write(`${text}\n`);
160
187
  if (argv[0] === 'shell')
161
188
  process.exitCode = Number(/^exit: (\d+)(?:\n|$)/.exec(text)?.[1] ?? 0);
@@ -1,8 +1,10 @@
1
+ import { HOUSE_REF } from './git.js';
1
2
  import { HOW_WE_WORK } from './skills.js';
3
+ const SOURCE_LINE = "From House, not the User's message:";
2
4
  const HOUSE_LINE = 'Work with House through the `house` CLI: run `house --help`.';
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 `/rooms/private/rooms/<handle>/AGENTS.md`.';
5
- const COPIES_LINE = 'House Local copies; commit, then `house push`:';
5
+ const FILE_LINE = 'To give the User a file or a page, upload it with `house upload` and link it; never start a server.';
6
+ const READ_LINE = "Your User's instructions for a Room, if any: `/rooms/private/rooms/<handle>/AGENTS.md`.";
7
+ const COPIES_LINE = `House Local copies; before work run \`house fetch\` and rebase onto \`${HOUSE_REF}\`; after it commit, then \`house push\`:`;
6
8
  const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
7
9
  const FAILED = /^exit: [1-9]\d*$/;
8
10
  const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
@@ -29,6 +31,6 @@ async function howWeWork(bridge) {
29
31
  return document.replace(/\n$/, '');
30
32
  }
31
33
  export async function instructions(bridge, base, copies) {
32
- const lines = [HOUSE_LINE, FILE_LINE, READ_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
34
+ const lines = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
33
35
  return [lines.join('\n'), base, await howWeWork(bridge)].filter((part) => part !== '').join('\n\n');
34
36
  }
package/dist/login.js CHANGED
@@ -1,54 +1,20 @@
1
+ import { spawn } from 'node:child_process';
1
2
  import { createHash, randomBytes } from 'node:crypto';
2
- import { createServer } from 'node:http';
3
3
  import { hostname } from 'node:os';
4
- import { createInterface } from 'node:readline/promises';
5
4
  import { parseArgs } from 'node:util';
6
- import { post } from './api.js';
5
+ import { post, retrying } from './api.js';
7
6
  import { placeAppGit } from './app-git.js';
8
7
  import { forgetOwnAgent, readDisconnected, readEnrolment, readOwnAgent, writeEnrolment, writeOwnAgent } from './home.js';
9
8
  import { callHouse, rpc } from './mcp.js';
9
+ import { selectRelay } from './relay.js';
10
10
  const DEFAULT_HOUSE = 'https://agents.house';
11
- function listen() {
12
- return new Promise((resolve, reject) => {
13
- let deliver = () => undefined;
14
- let held = null;
15
- const code = new Promise((settle) => {
16
- deliver = settle;
17
- });
18
- const server = createServer((request, response) => {
19
- const received = new URL(request.url ?? '/', 'http://127.0.0.1').searchParams.get('code');
20
- if (received === null || held !== null) {
21
- response.writeHead(404).end();
22
- return;
23
- }
24
- held = response;
25
- deliver(received);
26
- });
27
- const close = () => {
28
- server.close();
29
- server.closeAllConnections();
30
- };
31
- const port = Number(process.env.HOUSE_KIT_LOGIN_PORT ?? 0);
32
- server.once('error', reject);
33
- server.listen(port, port === 0 ? '127.0.0.1' : '0.0.0.0', () => {
34
- const address = server.address();
35
- if (address === null || typeof address === 'string') {
36
- reject(new Error('the login callback has no port'));
37
- return;
38
- }
39
- resolve({
40
- uri: `http://127.0.0.1:${address.port}/callback`,
41
- code,
42
- answer: (text) => new Promise((written) => {
43
- if (held === null || held.destroyed || !held.socket || held.socket.destroyed)
44
- return written();
45
- held.once('close', () => written());
46
- held.writeHead(200, { 'content-type': 'text/plain; charset=utf-8', connection: 'close' }).end(`${text}\n`);
47
- }),
48
- close,
49
- });
50
- });
51
- });
11
+ const OPENER = process.platform === 'darwin' ? 'open' : 'xdg-open';
12
+ // Polling every two seconds brings the User's confirmation to the waiting Kit moments after they give it.
13
+ const POLL_INTERVAL_MS = 2000;
14
+ function open(link) {
15
+ spawn(OPENER, [link], { detached: true, stdio: 'ignore' })
16
+ .on('error', () => undefined)
17
+ .unref();
52
18
  }
53
19
  async function accepted(house, credential) {
54
20
  const checked = await callHouse(house, credential, rpc('tools/list', {}));
@@ -58,17 +24,19 @@ async function accepted(house, credential) {
58
24
  return false;
59
25
  throw new Error(`House answered ${checked.status} when Kit checked this computer's own agent's connection: ${checked.text}`);
60
26
  }
61
- async function typed(question) {
62
- const reader = createInterface({ input: process.stdin, output: process.stdout });
63
- const closed = new Promise((_, reject) => {
64
- reader.once('close', () => reject(new Error('no code was typed')));
65
- });
66
- try {
67
- return (await Promise.race([reader.question(question), closed])).trim();
68
- }
69
- finally {
70
- reader.close();
71
- }
27
+ const USAGE = `usage: kit login [--house <origin>] [--environment <id>] [--rooms <ref>[,<ref>...] | --no-private] [--code <code>]
28
+ --house <origin> the House to connect to, ${DEFAULT_HOUSE} unless one is stored
29
+ --environment <id> reconnect this Environment
30
+ --rooms <ref>[,<ref>...] give this Environment only these Rooms
31
+ --no-private give this Environment every Room but your Private Room
32
+ --code <code> connect this machine with a login code
33
+ `;
34
+ function chosenRooms(rooms, noPrivate) {
35
+ if (rooms !== undefined && noPrivate)
36
+ throw new Error('--rooms and --no-private are two choices of Rooms; pass one');
37
+ if (rooms !== undefined)
38
+ return { rooms: { reach: 'selected', refs: rooms.split(',') } };
39
+ return noPrivate ? { rooms: { reach: 'all_but_private' } } : {};
72
40
  }
73
41
  export async function login(argv) {
74
42
  const { values } = parseArgs({
@@ -76,65 +44,66 @@ export async function login(argv) {
76
44
  options: {
77
45
  house: { type: 'string' },
78
46
  environment: { type: 'string' },
79
- manual: { type: 'boolean', default: false },
47
+ rooms: { type: 'string' },
48
+ 'no-private': { type: 'boolean', default: false },
49
+ code: { type: 'string' },
50
+ help: { type: 'boolean', default: false },
80
51
  },
81
52
  });
53
+ if (values.help) {
54
+ process.stdout.write(USAGE);
55
+ return;
56
+ }
57
+ if (values.code !== undefined && values.rooms !== undefined)
58
+ throw new Error('--code cannot be used with --rooms');
59
+ if (values.code !== undefined && values['no-private'])
60
+ throw new Error('--code cannot be used with --no-private');
61
+ if (values.code !== undefined && values.environment !== undefined)
62
+ throw new Error('--code cannot be used with --environment');
63
+ const rooms = chosenRooms(values.rooms, values['no-private']);
82
64
  const enrolled = (await readEnrolment()) ?? (await readDisconnected());
65
+ if (values.code !== undefined && enrolled !== null) {
66
+ throw new Error(`--code connects a new machine; this Kit home records Environment ${enrolled.environment}, so run kit login to reconnect it`);
67
+ }
68
+ const relay = process.env.HOUSE_KIT_RELAY || undefined;
69
+ selectRelay(relay);
83
70
  const house = values.house ?? enrolled?.house ?? DEFAULT_HOUSE;
84
71
  const replaces = process.env.HOUSE_KIT_REPLACES;
85
72
  const environment = replaces === undefined ? (values.environment ?? (enrolled?.house === house ? enrolled.environment : undefined)) : undefined;
86
73
  const ownAgent = await readOwnAgent();
87
- const held = ownAgent !== null && ownAgent.house === house && (await accepted(house, ownAgent.credential));
88
- const verifier = randomBytes(32).toString('base64url');
89
- const started = {
90
- ...(environment === undefined
91
- ? { act: 'new_environment', label: hostname(), ...(replaces === undefined ? {} : { replaces }) }
92
- : { act: 'existing_environment', environment_id: environment }),
93
- code_challenge: createHash('sha256').update(verifier).digest('base64url'),
94
- ...(held ? {} : { headless: true }),
95
- };
96
- const exchange = async (code) => {
97
- const exchanged = await post(house, '/kit/token', { code, code_verifier: verifier });
98
- await writeEnrolment({ house, environment: exchanged.environment, credential: exchanged.credential });
99
- process.stdout.write(`Environment ${exchanged.environment} is connected to ${house}.\n`);
100
- if (exchanged.headless_credential !== undefined) {
101
- await writeOwnAgent({ house, user: exchanged.user, credential: exchanged.headless_credential });
102
- }
103
- else if (held && ownAgent.user !== exchanged.user) {
104
- await forgetOwnAgent();
105
- process.stdout.write("This computer's own agent was another User's; run kit login again to connect it to you.\n");
106
- }
107
- };
108
- const placeGit = async () => placeAppGit(ownAgent, await readOwnAgent());
109
- if (values.manual) {
110
- const { start_url } = await post(house, '/kit', {
111
- ...started,
112
- presentation: 'manual',
113
- });
114
- process.stdout.write(`Open this link and confirm: ${start_url}\n`);
115
- await exchange(await typed('Code: '));
116
- await placeGit();
117
- return;
74
+ let held = false;
75
+ let exchanged;
76
+ if (values.code !== undefined) {
77
+ exchanged = await post(house, '/kit/token', { code: values.code, label: hostname() });
118
78
  }
119
- const callback = await listen();
120
- try {
121
- const { start_url } = await post(house, '/kit', {
122
- ...started,
123
- presentation: 'loopback',
124
- loopback_uri: callback.uri,
125
- });
126
- process.stdout.write(`Open this link and confirm: ${start_url}\n`);
127
- try {
128
- await exchange(await callback.code);
129
- }
130
- catch (error) {
131
- await callback.answer(`This Environment was not connected: ${error.message}`);
132
- throw error;
133
- }
134
- await callback.answer('This Environment is connected. You can close this tab.');
79
+ else {
80
+ held = ownAgent !== null && ownAgent.house === house && (await accepted(house, ownAgent.credential));
81
+ const verifier = randomBytes(32).toString('base64url');
82
+ const started = await retrying(() => post(house, '/kit', {
83
+ ...(environment === undefined
84
+ ? { act: 'new_environment', label: hostname(), ...(replaces === undefined ? {} : { replaces }) }
85
+ : { act: 'existing_environment', environment_id: environment }),
86
+ ...rooms,
87
+ code_challenge: createHash('sha256').update(verifier).digest('base64url'),
88
+ ...(held ? {} : { headless: true }),
89
+ }));
90
+ process.stdout.write(`Open this link and confirm: ${started.start_url}\n`);
91
+ open(started.start_url);
92
+ exchanged = await retrying(() => post(house, '/kit/token', { login: started.login, code_verifier: verifier }), undefined, () => POLL_INTERVAL_MS);
93
+ }
94
+ await writeEnrolment({
95
+ house,
96
+ ...(relay === undefined ? {} : { relay }),
97
+ environment: exchanged.environment,
98
+ credential: exchanged.credential,
99
+ });
100
+ process.stdout.write(`Environment ${exchanged.environment} is connected to ${house}.\n`);
101
+ if (exchanged.headless_credential !== undefined) {
102
+ await writeOwnAgent({ house, user: exchanged.user, credential: exchanged.headless_credential });
135
103
  }
136
- finally {
137
- callback.close();
104
+ else if (held && ownAgent !== null && ownAgent.user !== exchanged.user) {
105
+ await forgetOwnAgent();
106
+ process.stdout.write("This computer's own agent was another User's; run kit login again to connect it to you.\n");
138
107
  }
139
- await placeGit();
108
+ await placeAppGit(ownAgent, await readOwnAgent());
140
109
  }
package/dist/logout.js CHANGED
@@ -1,21 +1,21 @@
1
1
  import { HouseRefusal, post } from './api.js';
2
2
  import { forgetAppGit } from './app-git.js';
3
- import { forgetEnrolment, readEnrolment } from './home.js';
3
+ import { forgetEnrolment, forgetOwnAgent, readEnrolment } from './home.js';
4
4
  export async function logout() {
5
5
  const enrolled = await readEnrolment();
6
- if (enrolled === null) {
7
- await forgetAppGit();
8
- process.stdout.write('This computer is not connected to House.\n');
9
- return;
6
+ if (enrolled !== null) {
7
+ try {
8
+ await post(enrolled.house, '/kit/logout', {}, enrolled.credential);
9
+ }
10
+ catch (error) {
11
+ if (!(error instanceof HouseRefusal && error.status === 401))
12
+ throw error;
13
+ }
14
+ await forgetEnrolment(enrolled);
10
15
  }
11
- try {
12
- await post(enrolled.house, '/kit/logout', {}, enrolled.credential);
13
- }
14
- catch (error) {
15
- if (!(error instanceof HouseRefusal && error.status === 401))
16
- throw error;
17
- }
18
- await forgetEnrolment(enrolled);
19
16
  await forgetAppGit();
20
- process.stdout.write(`Environment ${enrolled.environment} is disconnected; kit login reconnects it.\n`);
17
+ await forgetOwnAgent();
18
+ process.stdout.write(enrolled === null
19
+ ? 'This computer is not connected to House.\n'
20
+ : `Environment ${enrolled.environment} is disconnected; kit login reconnects it.\n`);
21
21
  }
package/dist/relay.js CHANGED
@@ -1,7 +1,17 @@
1
+ let selected;
2
+ export function selectRelay(relay) {
3
+ selected = relay === undefined || relay === '' ? null : relay;
4
+ }
5
+ function relayOrigin() {
6
+ if (selected !== undefined)
7
+ return selected ?? undefined;
8
+ const ambient = process.env.HOUSE_KIT_RELAY;
9
+ return ambient === undefined || ambient === '' ? undefined : ambient;
10
+ }
1
11
  export function relayed(target) {
2
12
  const url = new URL(target);
3
- const relay = process.env.HOUSE_KIT_RELAY;
4
- if (relay === undefined || relay === '')
13
+ const relay = relayOrigin();
14
+ if (relay === undefined)
5
15
  return url;
6
16
  const through = new URL(`${url.host}${url.pathname}${url.search}`, `${relay}/`);
7
17
  if (url.protocol === 'wss:' || url.protocol === 'ws:')