@agentshouse/kit 0.1.0-alpha.9 → 0.2.1-alpha.1

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.
Files changed (93) hide show
  1. package/README.md +14 -1740
  2. package/dist/acp.js +120 -0
  3. package/dist/agents.js +90 -0
  4. package/dist/api.js +45 -0
  5. package/dist/blocks.js +51 -0
  6. package/dist/bridge.js +124 -0
  7. package/dist/clis.js +110 -0
  8. package/dist/conversations.js +538 -0
  9. package/dist/files.js +21 -0
  10. package/dist/home.js +24 -0
  11. package/dist/house-main.js +92 -0
  12. package/dist/instructions.js +28 -0
  13. package/dist/kit-main.js +16 -0
  14. package/dist/login.js +112 -0
  15. package/dist/resident.js +66 -0
  16. package/dist/secret-input.js +16 -0
  17. package/dist/sign-in.js +83 -0
  18. package/dist/stream.js +47 -0
  19. package/package.json +34 -10
  20. package/bin/connect-linux.sh +0 -134
  21. package/bin/kit.js +0 -1023
  22. package/bootstrap.json +0 -17
  23. package/src/acp.js +0 -99
  24. package/src/adapter-mcp.js +0 -44
  25. package/src/adapter.js +0 -252
  26. package/src/agent-logins.js +0 -65
  27. package/src/agents.js +0 -504
  28. package/src/apply.js +0 -29
  29. package/src/attach.js +0 -52
  30. package/src/binding.js +0 -90
  31. package/src/blocks.js +0 -156
  32. package/src/bridge.js +0 -449
  33. package/src/bytes.js +0 -89
  34. package/src/channel.js +0 -37
  35. package/src/command.js +0 -109
  36. package/src/configuration.js +0 -465
  37. package/src/connector-login.js +0 -91
  38. package/src/connector.js +0 -107
  39. package/src/continuation.js +0 -61
  40. package/src/contract.js +0 -363
  41. package/src/conversation-originals.js +0 -143
  42. package/src/custom/authorization.js +0 -63
  43. package/src/declaration.js +0 -35
  44. package/src/delivery.js +0 -191
  45. package/src/directory.js +0 -102
  46. package/src/draft.js +0 -89
  47. package/src/google/calendar.js +0 -177
  48. package/src/google/declared.js +0 -146
  49. package/src/google/drive.js +0 -435
  50. package/src/google/gateway.js +0 -166
  51. package/src/google/gmail.js +0 -249
  52. package/src/google/id.js +0 -1
  53. package/src/google/oauth.js +0 -273
  54. package/src/google/source.js +0 -25
  55. package/src/hook.js +0 -376
  56. package/src/host.js +0 -121
  57. package/src/house-main.js +0 -79
  58. package/src/house.js +0 -1090
  59. package/src/inbound.js +0 -175
  60. package/src/instance/google.js +0 -1
  61. package/src/kit-work-stream.js +0 -84
  62. package/src/launcher-main.js +0 -19
  63. package/src/launcher.js +0 -276
  64. package/src/local-files.js +0 -113
  65. package/src/login.js +0 -274
  66. package/src/mcp.js +0 -71
  67. package/src/migrator.js +0 -248
  68. package/src/mount.js +0 -582
  69. package/src/operation.js +0 -20
  70. package/src/original.js +0 -631
  71. package/src/peer.js +0 -124
  72. package/src/plan.js +0 -57
  73. package/src/question.js +0 -316
  74. package/src/refusal.js +0 -79
  75. package/src/repository.js +0 -521
  76. package/src/session-bin/house +0 -2
  77. package/src/settlement.js +0 -57
  78. package/src/setup.js +0 -190
  79. package/src/source-originals.js +0 -116
  80. package/src/source.js +0 -286
  81. package/src/starter-set/grilling/LICENSE +0 -21
  82. package/src/starter-set/grilling/SKILL.md +0 -30
  83. package/src/starter-set/grilling/agents/openai.yaml +0 -3
  84. package/src/starter-set.js +0 -17
  85. package/src/state.js +0 -157
  86. package/src/sync-library.js +0 -293
  87. package/src/sync-state.js +0 -120
  88. package/src/sync.js +0 -482
  89. package/src/telegram.js +0 -1045
  90. package/src/terminal.js +0 -57
  91. package/src/tmux.js +0 -119
  92. package/src/worker.js +0 -925
  93. package/src/workspace.js +0 -500
@@ -0,0 +1,538 @@
1
+ import { client, } from '@agentclientprotocol/sdk';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdir } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { killMarked, startAdapter, TURN_ENDED, TURN_STARTED } from './acp.js';
6
+ import { blocksOf, firstChange } from './blocks.js';
7
+ import { openBridge } from './bridge.js';
8
+ import { CLIS } from './clis.js';
9
+ import { placeFiles } from './files.js';
10
+ import { instructions } from './instructions.js';
11
+ import { holdSecretInput } from './secret-input.js';
12
+ const LAUNCH_BASE = '/agents/house';
13
+ const KILLED = 'the conversation was killed';
14
+ function causeOf(error) {
15
+ return error instanceof Error ? error.message : String(error);
16
+ }
17
+ function logged(error) {
18
+ process.stderr.write(`kit: ${causeOf(error)}\n`);
19
+ }
20
+ function unlessKilled(work, signal) {
21
+ return new Promise((resolve, reject) => {
22
+ const killed = () => reject(signal.reason);
23
+ signal.addEventListener('abort', killed, { once: true });
24
+ if (signal.aborted)
25
+ killed();
26
+ work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
27
+ });
28
+ }
29
+ function stop(bridge) {
30
+ killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
31
+ }
32
+ class Turn {
33
+ id = randomUUID();
34
+ text = '';
35
+ sent = [];
36
+ unsentFrom = null;
37
+ draftSequence = 0;
38
+ planSequence = 0;
39
+ questions = 0;
40
+ secrets = 0;
41
+ prompt = null;
42
+ ended = false;
43
+ ready = Promise.resolve();
44
+ }
45
+ class Conversation {
46
+ id;
47
+ running = null;
48
+ turn = null;
49
+ commands = null;
50
+ jobs = new Set();
51
+ queued = [];
52
+ late = [];
53
+ opening = null;
54
+ kills = 0;
55
+ killed = new AbortController();
56
+ open = 0;
57
+ queue = Promise.resolve();
58
+ reports = Promise.resolve();
59
+ constructor(id) {
60
+ this.id = id;
61
+ }
62
+ }
63
+ function formProperties(request) {
64
+ if (request.mode !== 'form')
65
+ return {};
66
+ return request.requestedSchema?.properties ?? {};
67
+ }
68
+ function asksSecret(request) {
69
+ return Object.values(formProperties(request)).some((property) => Object.values(property._meta ?? {}).some((meta) => typeof meta === 'object' && meta !== null && meta.isSecret === true));
70
+ }
71
+ export class Conversations {
72
+ kit;
73
+ conversations = new Map();
74
+ acks = new Map();
75
+ questions = new Map();
76
+ carrying = 0;
77
+ reporting = 0;
78
+ constructor(kit) {
79
+ this.kit = kit;
80
+ }
81
+ input(input) {
82
+ this.carrying++;
83
+ const carried = this.acks.get(input.input_id) ??
84
+ (() => {
85
+ const conversation = this.conversation(input.conversation_id);
86
+ if (input.kind === 'kill') {
87
+ conversation.kills++;
88
+ conversation.killed.abort(new Error(KILLED));
89
+ this.halt(conversation);
90
+ }
91
+ const work = conversation.queue.then(() => this.carry(conversation, input));
92
+ conversation.queue = work.catch(() => undefined);
93
+ this.acks.set(input.input_id, work);
94
+ return work;
95
+ })();
96
+ carried
97
+ .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
98
+ .catch(logged)
99
+ .finally(() => {
100
+ this.carrying--;
101
+ this.kit.changed();
102
+ });
103
+ }
104
+ idle() {
105
+ if (this.carrying > 0 || this.reporting > 0)
106
+ return false;
107
+ for (const conversation of this.conversations.values()) {
108
+ const turn = conversation.turn;
109
+ const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
110
+ if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
111
+ return false;
112
+ }
113
+ return true;
114
+ }
115
+ opened() {
116
+ for (const conversation of this.conversations.values()) {
117
+ if (conversation.turn !== null)
118
+ conversation.turn.unsentFrom = 0;
119
+ }
120
+ }
121
+ conversation(id) {
122
+ let conversation = this.conversations.get(id);
123
+ if (conversation === undefined) {
124
+ conversation = new Conversation(id);
125
+ this.conversations.set(id, conversation);
126
+ }
127
+ return conversation;
128
+ }
129
+ async carry(conversation, input) {
130
+ if (input.kind === 'open')
131
+ return this.open(conversation, input);
132
+ if (input.kind === 'message')
133
+ return this.message(conversation, input);
134
+ if (input.kind === 'interrupt')
135
+ await this.interrupt(conversation, String(input.turn_id));
136
+ if (input.kind === 'kill') {
137
+ await this.kill(conversation);
138
+ if (--conversation.kills === 0)
139
+ conversation.killed = new AbortController();
140
+ }
141
+ if (input.kind === 'option')
142
+ await this.option(conversation, String(input.option), input.value);
143
+ if (input.kind === 'answer')
144
+ this.questions.get(String(input.interaction_id))?.answer(input.response);
145
+ return {};
146
+ }
147
+ async open(conversation, input) {
148
+ if (conversation.running !== null)
149
+ return {};
150
+ try {
151
+ return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
152
+ }
153
+ catch (error) {
154
+ return { refused: causeOf(error) };
155
+ }
156
+ }
157
+ async message(conversation, input) {
158
+ let ack = {};
159
+ const prompt = [];
160
+ try {
161
+ const route = await this.route(conversation, input.agent_id);
162
+ const paths = await placeFiles(this.kit.house, route.working_directory, input.files, conversation.killed.signal);
163
+ if (conversation.running === null) {
164
+ const opened = await this.start(conversation, route, input.provider_session_id);
165
+ if (input.provider_session_id === null)
166
+ ack = { provider_session_id: opened };
167
+ }
168
+ if (input.first === true) {
169
+ prompt.push({ type: 'text', text: await instructions(conversation.running.bridge, route.base_instructions) });
170
+ }
171
+ prompt.push({ type: 'text', text: String(input.text) });
172
+ if (paths.length > 0)
173
+ prompt.push({ type: 'text', text: paths.join('\n') });
174
+ if (conversation.kills > 0)
175
+ throw new Error(KILLED);
176
+ }
177
+ catch (error) {
178
+ if ('provider_session_id' in ack)
179
+ await this.kill(conversation);
180
+ return { refused: conversation.kills > 0 ? KILLED : causeOf(error) };
181
+ }
182
+ this.prompt(conversation, prompt);
183
+ return ack;
184
+ }
185
+ async interrupt(conversation, turnId) {
186
+ if (conversation.running === null || conversation.turn?.id !== turnId)
187
+ return;
188
+ await conversation.running.adapter.connection.agent.notify('session/cancel', {
189
+ sessionId: conversation.running.sessionId,
190
+ });
191
+ }
192
+ halt(conversation) {
193
+ if (conversation.opening !== null)
194
+ stop(conversation.opening);
195
+ if (conversation.running === null)
196
+ return;
197
+ conversation.running.killed = true;
198
+ stop(conversation.running.bridge);
199
+ }
200
+ async kill(conversation) {
201
+ const running = conversation.running;
202
+ this.halt(conversation);
203
+ await running?.adapter.exited;
204
+ }
205
+ async option(conversation, option, value) {
206
+ const running = conversation.running;
207
+ if (running === null)
208
+ return;
209
+ try {
210
+ const answered = await running.adapter.connection.agent.request('session/set_config_option', {
211
+ sessionId: running.sessionId,
212
+ configId: option,
213
+ ...(typeof value === 'boolean' ? { type: 'boolean', value } : { value: String(value) }),
214
+ });
215
+ this.options(conversation, answered.configOptions);
216
+ }
217
+ catch (error) {
218
+ logged(error);
219
+ }
220
+ }
221
+ async route(conversation, agentId) {
222
+ const signal = conversation.killed.signal;
223
+ await unlessKilled(this.kit.agents.read(), signal);
224
+ if (this.kit.agents.route(agentId) === undefined)
225
+ await unlessKilled(this.kit.agents.refresh(), signal);
226
+ const route = this.kit.agents.route(agentId);
227
+ if (route === undefined)
228
+ throw new Error(`this Environment hosts no Agent ${agentId}`);
229
+ if (route.working_directory === join(LAUNCH_BASE, route.agent_id)) {
230
+ await mkdir(route.working_directory, { recursive: true });
231
+ }
232
+ return route;
233
+ }
234
+ async start(conversation, route, sessionId) {
235
+ const app = client({ name: '@agentshouse/kit' })
236
+ .onNotification('session/update', ({ params }) => this.update(conversation, params))
237
+ .onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
238
+ .onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
239
+ .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
240
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
241
+ await unlessKilled(this.kit.agents.installed(), conversation.killed.signal);
242
+ const bridge = await openBridge(this.kit.house, conversation.id, conversation.killed.signal);
243
+ if (conversation.kills > 0) {
244
+ bridge.close();
245
+ throw new Error(KILLED);
246
+ }
247
+ conversation.opening = bridge;
248
+ try {
249
+ const adapter = await startAdapter(route.kind, route.working_directory, app, (job) => this.job(conversation, job), { ...process.env, ...bridge.env });
250
+ void adapter.exited.then(() => bridge.close());
251
+ const agent = adapter.connection.agent;
252
+ const cwd = route.working_directory;
253
+ const opened = sessionId === null
254
+ ? await agent.request('session/new', { cwd, mcpServers: [] })
255
+ : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [] })), sessionId };
256
+ const running = {
257
+ adapter,
258
+ bridge,
259
+ sessionId: opened.sessionId,
260
+ killed: false,
261
+ queues: CLIS[route.kind].queues,
262
+ };
263
+ const options = await this.launchSettings(running, route, opened.configOptions ?? []);
264
+ conversation.running = running;
265
+ this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
266
+ if (conversation.commands !== null)
267
+ this.commands(conversation, conversation.commands);
268
+ this.options(conversation, options);
269
+ void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
270
+ return running.sessionId;
271
+ }
272
+ catch (error) {
273
+ stop(bridge);
274
+ bridge.close();
275
+ throw conversation.kills > 0 ? new Error(KILLED) : error;
276
+ }
277
+ finally {
278
+ conversation.opening = null;
279
+ }
280
+ }
281
+ async launchSettings(running, route, offered) {
282
+ let options = offered;
283
+ const settings = [
284
+ ['model', route.model],
285
+ ['thought_level', route.effort],
286
+ ];
287
+ for (const [category, value] of settings) {
288
+ if (value === null)
289
+ continue;
290
+ const option = options.find((candidate) => candidate.category === category);
291
+ const answered = await running.adapter.connection.agent.request('session/set_config_option', {
292
+ sessionId: running.sessionId,
293
+ configId: option?.id ?? category,
294
+ value,
295
+ });
296
+ options = answered.configOptions;
297
+ }
298
+ return options;
299
+ }
300
+ exited(conversation, adapter, cause) {
301
+ const running = conversation.running;
302
+ if (running?.adapter !== adapter)
303
+ return;
304
+ conversation.running = null;
305
+ conversation.commands = null;
306
+ conversation.jobs.clear();
307
+ conversation.queued.length = 0;
308
+ for (const [id, question] of this.questions) {
309
+ if (question.conversation === conversation)
310
+ this.questions.delete(id);
311
+ }
312
+ this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
313
+ if (conversation.turn !== null) {
314
+ this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
315
+ }
316
+ this.kit.changed();
317
+ }
318
+ commands(conversation, commands) {
319
+ conversation.commands = commands;
320
+ if (conversation.running === null)
321
+ return;
322
+ this.kit.send({ type: 'commands', conversation_id: conversation.id, commands });
323
+ }
324
+ options(conversation, options) {
325
+ if (conversation.running === null)
326
+ return;
327
+ this.kit.send({ type: 'options', conversation_id: conversation.id, options });
328
+ }
329
+ deliver(conversation, turn, event, body) {
330
+ this.reporting++;
331
+ const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
332
+ conversation.reports = delivered
333
+ .catch(() => undefined)
334
+ .finally(() => {
335
+ this.reporting--;
336
+ this.kit.changed();
337
+ });
338
+ return delivered;
339
+ }
340
+ report(conversation) {
341
+ const turn = new Turn();
342
+ conversation.open++;
343
+ turn.ready = this.deliver(conversation, turn, 'started', {}).then(() => undefined, logged);
344
+ return turn;
345
+ }
346
+ begin(conversation) {
347
+ const turn = this.report(conversation);
348
+ conversation.turn = turn;
349
+ this.kit.changed();
350
+ return turn;
351
+ }
352
+ end(conversation, turn, outcome) {
353
+ if (turn.ended)
354
+ return;
355
+ turn.ended = true;
356
+ conversation.open--;
357
+ if (conversation.turn === turn)
358
+ conversation.turn = null;
359
+ void this.deliver(conversation, turn, 'ended', outcome).catch(logged);
360
+ const late = conversation.open === 0 ? conversation.late.shift() : undefined;
361
+ if (late !== undefined)
362
+ this.failed(conversation, late);
363
+ this.kit.changed();
364
+ }
365
+ failed(conversation, cause) {
366
+ if (conversation.open > 0)
367
+ conversation.late.push(cause);
368
+ else
369
+ this.end(conversation, this.report(conversation), { failed: cause });
370
+ }
371
+ prompt(conversation, prompt) {
372
+ const running = conversation.running;
373
+ const sent = { turn: null };
374
+ if (running.queues && (conversation.turn !== null || conversation.queued.length > 0)) {
375
+ conversation.queued.push(sent);
376
+ }
377
+ else {
378
+ this.attach(conversation.turn ?? this.begin(conversation), sent);
379
+ }
380
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
381
+ if (!running.adapter.connection.signal.aborted)
382
+ this.answered(conversation, sent, causeOf(error));
383
+ else if (sent.turn !== null) {
384
+ this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
385
+ }
386
+ });
387
+ }
388
+ attach(turn, sent) {
389
+ turn.prompt = sent;
390
+ sent.turn = turn;
391
+ }
392
+ answered(conversation, sent, failed) {
393
+ const turn = sent.turn;
394
+ if (turn === null) {
395
+ const queued = conversation.queued.indexOf(sent);
396
+ if (queued < 0)
397
+ return;
398
+ conversation.queued.splice(queued, 1);
399
+ if (failed !== null)
400
+ this.failed(conversation, failed);
401
+ }
402
+ else if (failed !== null) {
403
+ if (turn.ended)
404
+ this.failed(conversation, failed);
405
+ else
406
+ this.end(conversation, turn, { failed });
407
+ }
408
+ else if (turn.prompt === sent) {
409
+ this.end(conversation, turn, { text: turn.text });
410
+ }
411
+ }
412
+ started(conversation) {
413
+ if (conversation.turn !== null)
414
+ return conversation.turn;
415
+ const turn = this.begin(conversation);
416
+ const queued = conversation.queued.shift();
417
+ if (queued !== undefined)
418
+ this.attach(turn, queued);
419
+ return turn;
420
+ }
421
+ ended(conversation) {
422
+ const turn = conversation.turn;
423
+ if (turn === null)
424
+ return;
425
+ if (turn.prompt === null) {
426
+ this.end(conversation, turn, { text: turn.text });
427
+ }
428
+ else {
429
+ conversation.turn = null;
430
+ this.kit.changed();
431
+ }
432
+ }
433
+ async ask(conversation, method, params, secret, signal) {
434
+ const turn = this.started(conversation);
435
+ const interactionId = randomUUID();
436
+ const answered = new Promise((answer, fail) => {
437
+ this.questions.set(interactionId, { conversation, answer: (response) => answer(response) });
438
+ signal.addEventListener('abort', () => answer(null), { once: true });
439
+ this.deliver(conversation, turn, 'interactions', {
440
+ interaction_id: interactionId,
441
+ request: { method, params },
442
+ secret,
443
+ }).then(() => {
444
+ if (secret)
445
+ void this.holdSecret(interactionId, params);
446
+ }, fail);
447
+ });
448
+ turn.questions++;
449
+ if (secret)
450
+ turn.secrets++;
451
+ this.kit.changed();
452
+ try {
453
+ const response = await answered;
454
+ if (response === null)
455
+ throw signal.reason;
456
+ return response;
457
+ }
458
+ finally {
459
+ this.questions.delete(interactionId);
460
+ turn.questions--;
461
+ if (secret)
462
+ turn.secrets--;
463
+ this.kit.changed();
464
+ }
465
+ }
466
+ async holdSecret(interactionId, request) {
467
+ const steps = Object.entries(formProperties(request)).map(([name, property]) => ({
468
+ kind: 'collect',
469
+ label: property.title ?? name,
470
+ name,
471
+ ...(property.description ? { description: property.description } : {}),
472
+ }));
473
+ const content = await holdSecretInput(this.kit.house, interactionId, steps, () => this.questions.has(interactionId));
474
+ if (content !== null)
475
+ this.questions.get(interactionId)?.answer({ action: 'accept', content });
476
+ }
477
+ update(conversation, notification) {
478
+ const update = notification.update;
479
+ if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
480
+ const turn = this.started(conversation);
481
+ turn.text += update.content.text;
482
+ const text = turn.text;
483
+ void turn.ready.then(() => this.draft(conversation, turn, text));
484
+ }
485
+ else if (update.sessionUpdate === 'plan') {
486
+ const turn = this.started(conversation);
487
+ void turn.ready.then(() => this.plan(conversation, turn, update.entries));
488
+ }
489
+ else if (update.sessionUpdate === 'agent_thought_chunk' || update.sessionUpdate === 'tool_call') {
490
+ this.started(conversation);
491
+ }
492
+ else if (update.sessionUpdate === 'available_commands_update') {
493
+ this.commands(conversation, update.availableCommands);
494
+ }
495
+ else if (update.sessionUpdate === 'config_option_update') {
496
+ this.options(conversation, update.configOptions);
497
+ }
498
+ }
499
+ job(conversation, job) {
500
+ if (job.running)
501
+ conversation.jobs.add(job.id);
502
+ else
503
+ conversation.jobs.delete(job.id);
504
+ this.kit.changed();
505
+ }
506
+ draft(conversation, turn, text) {
507
+ const blocks = blocksOf(text);
508
+ const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
509
+ const sent = this.kit.send({
510
+ type: 'draft',
511
+ conversation_id: conversation.id,
512
+ turn_id: turn.id,
513
+ sequence: turn.draftSequence + 1,
514
+ from,
515
+ blocks: blocks.slice(from),
516
+ });
517
+ if (sent) {
518
+ turn.draftSequence++;
519
+ turn.sent = blocks;
520
+ turn.unsentFrom = null;
521
+ }
522
+ else {
523
+ turn.unsentFrom = from;
524
+ }
525
+ }
526
+ plan(conversation, turn, entries) {
527
+ const sent = this.kit.send({
528
+ type: 'plan',
529
+ conversation_id: conversation.id,
530
+ turn_id: turn.id,
531
+ sequence: turn.planSequence + 1,
532
+ from: 0,
533
+ steps: entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
534
+ });
535
+ if (sent)
536
+ turn.planSequence++;
537
+ }
538
+ }
package/dist/files.js ADDED
@@ -0,0 +1,21 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ export async function placeFiles(house, directory, files, signal) {
4
+ const paths = [];
5
+ for (const file of files) {
6
+ 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
+ if (!answer.ok)
9
+ throw new Error(`${file.name} did not download: ${answer.status}`);
10
+ const folder = join(directory, '.house', 'files', original.message, file.version);
11
+ await mkdir(join(directory, '.house')).catch((error) => {
12
+ if (error.code !== 'EEXIST')
13
+ throw error;
14
+ });
15
+ await mkdir(folder, { recursive: true });
16
+ const path = join(folder, file.name);
17
+ await writeFile(path, Buffer.from(await answer.arrayBuffer()));
18
+ paths.push(path);
19
+ }
20
+ return paths;
21
+ }
package/dist/home.js ADDED
@@ -0,0 +1,24 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const ENROLMENT = 'credential.json';
5
+ export function kitHome() {
6
+ return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
7
+ }
8
+ export async function readEnrolment() {
9
+ try {
10
+ return JSON.parse(await readFile(join(kitHome(), ENROLMENT), 'utf8'));
11
+ }
12
+ catch (error) {
13
+ if (error.code === 'ENOENT')
14
+ return null;
15
+ throw error;
16
+ }
17
+ }
18
+ export async function writeEnrolment(enrolment) {
19
+ const home = kitHome();
20
+ await mkdir(home, { recursive: true, mode: 0o700 });
21
+ const temporary = join(home, `${ENROLMENT}.${process.pid}`);
22
+ await writeFile(temporary, `${JSON.stringify(enrolment)}\n`, { mode: 0o600 });
23
+ await rename(temporary, join(home, ENROLMENT));
24
+ }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { request } from 'node:http';
5
+ import { basename } from 'node:path';
6
+ const USAGE = "usage: house <tool> ['<arguments as JSON>']";
7
+ const socketPath = process.env.HOUSE_BRIDGE;
8
+ function bridged(path, body) {
9
+ return new Promise((resolve, reject) => {
10
+ const sent = request({ socketPath, path, method: 'POST', headers: { 'content-type': 'application/json' } }, (answer) => {
11
+ let text = '';
12
+ answer.setEncoding('utf8');
13
+ answer.on('data', (chunk) => {
14
+ text += chunk;
15
+ });
16
+ answer.on('end', () => {
17
+ if (answer.statusCode === 200)
18
+ resolve(text);
19
+ else
20
+ reject(new Error(`${path} answered ${answer.statusCode}: ${text}`));
21
+ });
22
+ });
23
+ sent.on('error', reject);
24
+ sent.end(JSON.stringify(body));
25
+ });
26
+ }
27
+ async function mcp(method, params) {
28
+ const answer = JSON.parse(await bridged('/', { jsonrpc: '2.0', id: 1, method, params }));
29
+ if (answer.error !== undefined)
30
+ throw new Error(answer.error.message);
31
+ return answer.result;
32
+ }
33
+ async function tools() {
34
+ return (await mcp('tools/list', {})).tools;
35
+ }
36
+ async function upload(path, roomRef) {
37
+ const bytes = await readFile(path);
38
+ const declared = JSON.parse(await bridged('/kit/attachments/upload', {
39
+ ...(roomRef === undefined ? {} : { room_ref: roomRef }),
40
+ version: randomUUID(),
41
+ name: basename(path),
42
+ bytes: bytes.length,
43
+ sha256: createHash('sha256').update(bytes).digest('hex'),
44
+ }));
45
+ const answer = await fetch(declared.upload.url, {
46
+ method: 'POST',
47
+ headers: { 'content-type': 'application/octet-stream', 'x-house-byte-operation': declared.upload.operation },
48
+ body: bytes,
49
+ });
50
+ const text = await answer.text();
51
+ if (!answer.ok)
52
+ throw new Error(`House refused the bytes of ${path} with ${answer.status}: ${text}`);
53
+ return text;
54
+ }
55
+ async function house([verb, argument]) {
56
+ if (verb === undefined || verb === '--help') {
57
+ return [USAGE, ...(await tools()).map((tool) => `${tool.name}: ${tool.description}`)].join('\n');
58
+ }
59
+ if (argument === '--help') {
60
+ const tool = (await tools()).find((listed) => listed.name === verb);
61
+ if (tool === undefined)
62
+ throw new Error(`House has no tool ${verb}`);
63
+ return `${tool.description}\n${JSON.stringify(tool.inputSchema, null, 2)}`;
64
+ }
65
+ const args = (argument === undefined ? {} : JSON.parse(argument));
66
+ if (verb === 'upload_attachment')
67
+ return upload(String(args.path), args.room_ref);
68
+ if (verb === 'append_record' && Array.isArray(args.attachments)) {
69
+ const attachments = [];
70
+ for (const path of args.attachments) {
71
+ attachments.push(JSON.parse(await upload(String(path), args.room_ref)).attachment);
72
+ }
73
+ args.attachments = attachments;
74
+ }
75
+ const result = await mcp('tools/call', { name: verb, arguments: args });
76
+ const text = result.content.map((content) => content.text).join('\n');
77
+ if (result.isError === true)
78
+ throw new Error(text);
79
+ return text;
80
+ }
81
+ if (socketPath === undefined) {
82
+ process.stderr.write('house: house runs inside an Agent conversation\n');
83
+ process.exitCode = 1;
84
+ }
85
+ else {
86
+ house(process.argv.slice(2)).then((text) => {
87
+ process.stdout.write(`${text}\n`);
88
+ }, (error) => {
89
+ process.stderr.write(`house: ${error instanceof Error ? error.message : String(error)}\n`);
90
+ process.exitCode = 1;
91
+ });
92
+ }