@agentshouse/kit 0.2.1-cd9cbd8b6428c → 0.2.1-cfe564e35b4a5

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 ADDED
@@ -0,0 +1,521 @@
1
+ const COMMAND = { shown: 'command' };
2
+ const HIDDEN = { shown: 'hidden' };
3
+ const FINISHED = new Set(['completed', 'failed']);
4
+ export const RUNNING_JOB = new Set(['running', 'paused']);
5
+ const ROUTINE_TARGETS = { send_routine_request: 'access', cancel_routine_request: 'request' };
6
+ function words(line) {
7
+ const found = [];
8
+ let word = null;
9
+ let quote = null;
10
+ for (let index = 0; index < line.length; index++) {
11
+ const character = line[index];
12
+ if (quote !== null) {
13
+ if (character === quote)
14
+ quote = null;
15
+ else if (character === '\\' && quote === '"')
16
+ word += line[++index] ?? '';
17
+ else
18
+ word += character;
19
+ }
20
+ else if (character === "'" || character === '"') {
21
+ quote = character;
22
+ word ??= '';
23
+ }
24
+ else if (character === '\\') {
25
+ word = (word ?? '') + (line[++index] ?? '');
26
+ }
27
+ else if (/\s/.test(character)) {
28
+ if (word !== null)
29
+ found.push(word);
30
+ word = null;
31
+ }
32
+ else {
33
+ word = (word ?? '') + character;
34
+ }
35
+ }
36
+ if (word !== null)
37
+ found.push(word);
38
+ return found;
39
+ }
40
+ export function houseMessage(line) {
41
+ const [program, verb, json] = words(line.trim());
42
+ if (program !== 'house' || verb !== 'run_command' || json === undefined)
43
+ return null;
44
+ let call;
45
+ try {
46
+ call = JSON.parse(json);
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ const argument = ROUTINE_TARGETS[call.command];
52
+ if (argument === undefined)
53
+ return null;
54
+ const target = call.arguments?.[argument];
55
+ return { shown: 'message', command: call.command, target: typeof target === 'string' ? target : null };
56
+ }
57
+ function decidedByLine(command, final) {
58
+ const line = typeof command === 'string' ? command : Array.isArray(command) ? command.at(-1) : undefined;
59
+ if (typeof line !== 'string')
60
+ return final ? COMMAND : null;
61
+ return houseMessage(line) ?? COMMAND;
62
+ }
63
+ function contentText(content) {
64
+ const text = (content ?? [])
65
+ .map((entry) => (entry.type === 'content' && entry.content?.type === 'text' ? entry.content.text : ''))
66
+ .filter((part) => part !== '')
67
+ .join('\n');
68
+ return text === '' ? null : text;
69
+ }
70
+ function plan(entries) {
71
+ return entries.map((entry) => ({ content: entry.content, status: entry.status }));
72
+ }
73
+ function merged(into, from) {
74
+ if (from === null || from === undefined)
75
+ return into;
76
+ const result = { ...into };
77
+ for (const [key, value] of Object.entries(from)) {
78
+ result[key] =
79
+ typeof value === 'object' && value !== null && !Array.isArray(value) && typeof into[key] === 'object'
80
+ ? merged(into[key], value)
81
+ : value;
82
+ }
83
+ return result;
84
+ }
85
+ function names(input, ids) {
86
+ const values = JSON.stringify(input ?? {});
87
+ for (const id of ids)
88
+ if (values.includes(id))
89
+ return true;
90
+ return false;
91
+ }
92
+ class Reader {
93
+ session = null;
94
+ calls = new Map();
95
+ subagents = new Set();
96
+ done = new Set();
97
+ seenOptions = new Map();
98
+ requestedOptions = new Map();
99
+ compactions = new Set();
100
+ jobs = new Map();
101
+ read(message) {
102
+ if (message.method === undefined)
103
+ return this.answered(message.result);
104
+ const session = message.params?.sessionId;
105
+ if (session !== undefined && this.session !== null && session !== this.session) {
106
+ return this.elsewhere(session, message);
107
+ }
108
+ return this.own(message);
109
+ }
110
+ requested(option, value) {
111
+ this.requestedOptions.set(option, value);
112
+ }
113
+ refused(option) {
114
+ this.requestedOptions.delete(option);
115
+ }
116
+ closeTurn() {
117
+ const events = [];
118
+ for (const [id, call] of this.calls) {
119
+ if (call.decided?.shown === 'subagent')
120
+ continue;
121
+ if (call.decided === null)
122
+ events.push(...this.shown(call, this.decide(call, true)));
123
+ this.calls.delete(id);
124
+ }
125
+ return events;
126
+ }
127
+ answered(result) {
128
+ if (typeof result?.sessionId === 'string' && this.session === null)
129
+ this.session = result.sessionId;
130
+ for (const option of (result?.configOptions ?? [])) {
131
+ this.seenOptions.set(option.id, option.currentValue);
132
+ this.requestedOptions.delete(option.id);
133
+ }
134
+ return [];
135
+ }
136
+ elsewhere(_session, _message) {
137
+ return [];
138
+ }
139
+ own(message) {
140
+ if (message.method !== 'session/update')
141
+ return this.extension(message);
142
+ return this.update(message.params.update);
143
+ }
144
+ extension(_message) {
145
+ return [];
146
+ }
147
+ update(update) {
148
+ switch (update.sessionUpdate) {
149
+ case 'agent_message_chunk':
150
+ if (update.content.type === 'image')
151
+ return [{ kind: 'image', data: update.content.data, mediaType: update.content.mimeType }];
152
+ return update.content.type === 'text' ? [{ kind: 'text', id: update.messageId ?? null, text: update.content.text }] : [];
153
+ case 'agent_thought_chunk':
154
+ return update.content.type === 'text' ? [{ kind: 'reasoning', id: update.messageId ?? null, text: update.content.text }] : [];
155
+ case 'tool_call':
156
+ case 'tool_call_update':
157
+ return this.tool(update);
158
+ case 'plan':
159
+ return [{ kind: 'plan', entries: plan(update.entries) }];
160
+ case 'usage_update':
161
+ return [{ kind: 'context', used: update.used, window: update.size }];
162
+ case 'current_mode_update':
163
+ return [{ kind: 'marker', marker: 'mode', text: update.currentModeId }];
164
+ case 'available_commands_update':
165
+ return [{ kind: 'commands', commands: update.availableCommands }];
166
+ case 'config_option_update':
167
+ return [{ kind: 'options', options: update.configOptions }, ...this.optionsChanged(update.configOptions)];
168
+ case 'compaction_update':
169
+ if (update.status !== 'completed' || this.compactions.has(update.compactionId))
170
+ return [];
171
+ this.compactions.add(update.compactionId);
172
+ return [{ kind: 'marker', marker: 'compaction', text: '' }];
173
+ case 'async_task_spawned':
174
+ return this.job(update.asyncTaskId, update.name ?? update.description ?? '', 'running');
175
+ case 'async_task_state_update':
176
+ return RUNNING_JOB.has(update.state) ? [] : this.job(update.asyncTaskId, null, update.state);
177
+ default:
178
+ return [];
179
+ }
180
+ }
181
+ job(id, name, state) {
182
+ if (name !== null)
183
+ this.jobs.set(id, name);
184
+ const label = this.jobs.get(id) ?? '';
185
+ if (state !== 'running')
186
+ this.jobs.delete(id);
187
+ return [{ kind: 'marker', marker: 'job', text: `${label}: ${state}` }];
188
+ }
189
+ optionsChanged(options) {
190
+ const events = [];
191
+ for (const option of options) {
192
+ const marker = this.watchedOptions[option.id];
193
+ const before = this.seenOptions.get(option.id);
194
+ this.seenOptions.set(option.id, option.currentValue);
195
+ if (marker === undefined || before === undefined || before === option.currentValue)
196
+ continue;
197
+ if (this.requestedOptions.get(option.id) === option.currentValue) {
198
+ this.requestedOptions.delete(option.id);
199
+ continue;
200
+ }
201
+ events.push({ kind: 'marker', marker, text: String(option.currentValue) });
202
+ }
203
+ return events;
204
+ }
205
+ tool(update) {
206
+ let call = this.calls.get(update.toolCallId);
207
+ if (call === undefined) {
208
+ if (update.sessionUpdate !== 'tool_call')
209
+ return [];
210
+ call = { id: update.toolCallId, decided: null, meta: {}, imaged: false, status: 'in_progress' };
211
+ this.calls.set(call.id, call);
212
+ }
213
+ if (update.kind !== undefined)
214
+ call.kind = update.kind;
215
+ if (update.title !== undefined)
216
+ call.title = update.title;
217
+ if (update.status !== undefined)
218
+ call.status = update.status;
219
+ if (update.rawInput !== undefined)
220
+ call.rawInput = merged(call.rawInput ?? {}, update.rawInput);
221
+ if (update.content !== undefined)
222
+ call.content = update.content;
223
+ call.meta = merged(call.meta, update._meta);
224
+ const events = [];
225
+ const finished = FINISHED.has(call.status);
226
+ if (call.decided === null) {
227
+ const decided = this.decide(call, finished);
228
+ if (decided !== null)
229
+ events.push(...this.shown(call, decided));
230
+ }
231
+ events.push(...this.produced(call));
232
+ if (call.decided?.shown === 'subagent' && finished)
233
+ events.push(...this.subagentFinished(call));
234
+ else if (finished)
235
+ this.calls.delete(call.id);
236
+ return events;
237
+ }
238
+ shown(call, decided) {
239
+ call.decided = decided;
240
+ switch (decided.shown) {
241
+ case 'command':
242
+ return [{ kind: 'command' }];
243
+ case 'hidden':
244
+ return [];
245
+ case 'message':
246
+ return [{ kind: 'message', command: decided.command, target: decided.target }];
247
+ case 'marker':
248
+ return [{ kind: 'marker', marker: decided.marker, text: decided.text }];
249
+ case 'subagent':
250
+ this.subagents.add(call.id);
251
+ return [{ kind: 'subagent', id: call.id, description: decided.description }];
252
+ }
253
+ }
254
+ produced(_call) {
255
+ return [];
256
+ }
257
+ subagentFinished(call) {
258
+ this.calls.delete(call.id);
259
+ return this.finish(call.id, contentText(call.content));
260
+ }
261
+ finish(id, result) {
262
+ if (!this.subagents.has(id) || this.done.has(id))
263
+ return [];
264
+ this.done.add(id);
265
+ this.calls.delete(id);
266
+ return [{ kind: 'subagent-done', id, result }];
267
+ }
268
+ }
269
+ class ClaudeReader extends Reader {
270
+ watchedOptions = { model: 'model' };
271
+ tasks = new Map();
272
+ own(message) {
273
+ if (message.params?.update?._meta?.claudeCode?.parentToolUseId !== undefined)
274
+ return [];
275
+ return super.own(message);
276
+ }
277
+ update(update) {
278
+ if (update.sessionUpdate === 'usage_update' && update._meta?.['_claude/origin'] !== undefined) {
279
+ return [...super.update(update), { kind: 'ended' }];
280
+ }
281
+ if (update.sessionUpdate === 'session_info_update' && update._meta?.jetbrains?.air?.sessionFailure !== undefined) {
282
+ return [{ kind: 'marker', marker: 'retry', text: update._meta.jetbrains.air.sessionFailure.title }];
283
+ }
284
+ return super.update(update);
285
+ }
286
+ extension(message) {
287
+ if (message.method !== '_claude/sdkMessage')
288
+ return [];
289
+ const sdk = message.params.message;
290
+ switch (sdk.subtype) {
291
+ case 'task_started':
292
+ if (sdk.tool_use_id !== undefined)
293
+ this.tasks.set(sdk.task_id, sdk.tool_use_id);
294
+ return [];
295
+ case 'task_notification':
296
+ return sdk.tool_use_id === undefined ? [] : this.finish(sdk.tool_use_id, sdk.summary ?? null);
297
+ default:
298
+ return [];
299
+ }
300
+ }
301
+ decide(call, final) {
302
+ const tool = call.meta.claudeCode?.toolName;
303
+ if (tool === 'Agent' || tool === 'Task' || call.meta.jetbrains?.air?.subagent === true) {
304
+ const description = call.rawInput?.description;
305
+ if (description === undefined && !final)
306
+ return null;
307
+ return { shown: 'subagent', description: description ?? call.title ?? '' };
308
+ }
309
+ if (tool === 'TodoWrite' || tool === 'CronCreate')
310
+ return HIDDEN;
311
+ if (tool === 'ScheduleWakeup') {
312
+ if (call.rawInput === undefined)
313
+ return final ? COMMAND : null;
314
+ return call.rawInput.stop === true ? COMMAND : HIDDEN;
315
+ }
316
+ if (call.kind === 'switch_mode')
317
+ return { shown: 'marker', marker: 'mode', text: call.title ?? '' };
318
+ if (tool === 'Bash')
319
+ return decidedByLine(call.rawInput?.command, final);
320
+ if (tool === 'SendMessage' || tool === 'TaskStop') {
321
+ if (call.rawInput === undefined)
322
+ return final ? COMMAND : null;
323
+ return names(call.rawInput, this.tasks.keys()) ? HIDDEN : COMMAND;
324
+ }
325
+ return COMMAND;
326
+ }
327
+ produced(call) {
328
+ const tool = call.meta.claudeCode?.toolName;
329
+ if (call.status !== 'completed')
330
+ return [];
331
+ if (tool === 'CronCreate' || (tool === 'ScheduleWakeup' && call.rawInput?.stop !== true)) {
332
+ return [{ kind: 'marker', marker: 'job', text: `${call.rawInput?.prompt ?? call.title ?? ''}: scheduled` }];
333
+ }
334
+ return [];
335
+ }
336
+ subagentFinished(call) {
337
+ if (call.meta.claudeCode?.toolResponse?.isAsync === true)
338
+ return [];
339
+ return super.subagentFinished(call);
340
+ }
341
+ }
342
+ const COLLABORATION = new Set(['spawnAgent', 'sendInput', 'resumeAgent', 'wait', 'closeAgent', 'followupTask']);
343
+ class CodexReader extends Reader {
344
+ watchedOptions = { mode: 'mode', collaboration_mode: 'mode' };
345
+ children = new Map();
346
+ own(message) {
347
+ if (message.method === 'session/request_permission' && message.params.toolCall?.kind === 'switch_mode') {
348
+ return [{ kind: 'started' }, { kind: 'marker', marker: 'mode', text: message.params.toolCall.title ?? '' }];
349
+ }
350
+ return super.own(message);
351
+ }
352
+ update(update) {
353
+ switch (update.sessionUpdate) {
354
+ case 'session_info_update': {
355
+ const codex = update._meta?.codex;
356
+ if (codex?.threadStatus?.type === 'active')
357
+ return [{ kind: 'started' }];
358
+ if (codex?.threadStatus?.type === 'idle')
359
+ return [{ kind: 'ended' }];
360
+ if (codex?.error !== undefined)
361
+ return [{ kind: 'marker', marker: 'retry', text: codex.error.message ?? '' }];
362
+ return [];
363
+ }
364
+ case 'notice':
365
+ return update.severity === 'info' ? [{ kind: 'marker', marker: 'model', text: update.description ?? update.title }] : [];
366
+ case 'subagent_spawned':
367
+ this.children.set(update.subagentSessionId, { id: null, text: '' });
368
+ this.subagents.add(update.subagentSessionId);
369
+ return [{ kind: 'subagent', id: update.subagentSessionId, description: update.name }];
370
+ case 'subagent_state_update':
371
+ return this.finish(update.subagentSessionId, this.children.get(update.subagentSessionId)?.text || null);
372
+ default:
373
+ return super.update(update);
374
+ }
375
+ }
376
+ elsewhere(session, message) {
377
+ const child = this.children.get(session);
378
+ const update = message.params?.update;
379
+ if (child === undefined || update === undefined)
380
+ return [];
381
+ if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
382
+ const id = update.messageId ?? null;
383
+ if (child.id !== id)
384
+ child.text = '';
385
+ child.id = id;
386
+ child.text += update.content.text;
387
+ }
388
+ return [];
389
+ }
390
+ decide(call) {
391
+ const air = call.meta.jetbrains?.air;
392
+ if (COLLABORATION.has(call.title ?? ''))
393
+ return HIDDEN;
394
+ if (air?.subagent !== undefined)
395
+ return { shown: 'subagent', description: call.title ?? '' };
396
+ if (call.kind === 'switch_mode')
397
+ return { shown: 'marker', marker: 'mode', text: call.title ?? '' };
398
+ if (call.kind === 'execute')
399
+ return decidedByLine(call.rawInput?.command, true);
400
+ if (call.title === 'Image generation')
401
+ return HIDDEN;
402
+ return COMMAND;
403
+ }
404
+ produced(call) {
405
+ if (call.imaged || call.kind === 'read')
406
+ return [];
407
+ const image = (call.content ?? []).find((entry) => entry.type === 'content' && entry.content?.type === 'image');
408
+ if (image === undefined)
409
+ return [];
410
+ call.imaged = true;
411
+ return [{ kind: 'image', data: image.content.data, mediaType: image.content.mimeType }];
412
+ }
413
+ }
414
+ class GrokReader extends Reader {
415
+ watchedOptions = {};
416
+ children = new Map();
417
+ models = [];
418
+ model = null;
419
+ answered(result) {
420
+ if (result?.models !== undefined)
421
+ this.offered(result.models);
422
+ return super.answered(result);
423
+ }
424
+ read(message) {
425
+ const session = message.params?.sessionId;
426
+ if (this.session !== null && session !== undefined && session !== this.session && !this.children.has(session))
427
+ return [];
428
+ if (message.method === '_x.ai/queue/changed') {
429
+ return (message.params.runningPromptId ?? null) !== null && session === this.session ? [{ kind: 'started' }] : [];
430
+ }
431
+ if (message.params?.update?.sessionUpdate === 'turn_completed') {
432
+ return session === this.session ? [{ kind: 'ended' }] : [];
433
+ }
434
+ if (message.method === '_x.ai/models/update') {
435
+ this.offered(message.params);
436
+ return [];
437
+ }
438
+ return super.read(message);
439
+ }
440
+ offered(models) {
441
+ this.models = models.availableModels ?? this.models;
442
+ this.model = models.currentModelId ?? this.model;
443
+ }
444
+ window() {
445
+ return this.models.find((model) => model.modelId === this.model)?._meta?.totalContextTokens ?? null;
446
+ }
447
+ extension(message) {
448
+ const update = message.params?.update;
449
+ switch (message.method) {
450
+ case '_x.ai/task_backgrounded':
451
+ return this.job(update.task_id, update.description ?? update.command ?? '', 'running');
452
+ case '_x.ai/task_completed':
453
+ return this.job(update.task_snapshot.task_id, null, update.task_snapshot.exit_code === 0 ? 'completed' : 'failed');
454
+ case '_x.ai/scheduled_task_created':
455
+ return [{ kind: 'marker', marker: 'job', text: `${update.prompt}: next run ${update.next_fire_at}` }];
456
+ case '_x.ai/session_notification':
457
+ return this.notification(update);
458
+ default:
459
+ return [];
460
+ }
461
+ }
462
+ notification(update) {
463
+ switch (update.sessionUpdate) {
464
+ case 'response_completed': {
465
+ const usage = update.usage;
466
+ const window = this.window();
467
+ if (window === null)
468
+ return [];
469
+ const used = usage.input_tokens + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + usage.output_tokens;
470
+ return [{ kind: 'context', used, window }];
471
+ }
472
+ case 'subagent_spawned': {
473
+ const call = [...this.calls.values()].find((candidate) => candidate.decided?.shown === 'subagent' &&
474
+ ![...this.children.values()].includes(candidate.id) &&
475
+ candidate.decided.description === update.description) ??
476
+ [...this.calls.values()].find((candidate) => candidate.decided?.shown === 'subagent' && ![...this.children.values()].includes(candidate.id));
477
+ this.children.set(update.child_session_id, call?.id ?? null);
478
+ return [];
479
+ }
480
+ case 'subagent_finished': {
481
+ const call = this.children.get(update.child_session_id);
482
+ return typeof call === 'string' ? this.finish(call, update.output ?? null) : [];
483
+ }
484
+ case 'auto_compact_completed':
485
+ return [{ kind: 'marker', marker: 'compaction', text: '' }];
486
+ case 'model_auto_switched':
487
+ this.model = update.new_model_id;
488
+ return [{ kind: 'marker', marker: 'model', text: update.new_model_id }];
489
+ case 'retry_state':
490
+ return [{ kind: 'marker', marker: 'retry', text: update.message ?? update.reason ?? update.error_type ?? '' }];
491
+ default:
492
+ return [];
493
+ }
494
+ }
495
+ decide(call) {
496
+ const tool = (call.meta['x.ai/tool'] ?? {});
497
+ if (tool.name === 'spawn_subagent' || tool.kind === 'task') {
498
+ return { shown: 'subagent', description: call.rawInput?.description ?? call.title ?? '' };
499
+ }
500
+ if (tool.name === 'todo_write' || tool.name === 'scheduler_create')
501
+ return HIDDEN;
502
+ if (tool.name === 'enter_plan_mode' || tool.name === 'exit_plan_mode') {
503
+ return { shown: 'marker', marker: 'mode', text: tool.name };
504
+ }
505
+ if (tool.name === 'kill_command_or_subagent')
506
+ return names(call.rawInput, this.children.keys()) ? HIDDEN : COMMAND;
507
+ if (tool.name === 'run_terminal_command')
508
+ return decidedByLine(call.rawInput?.command, true);
509
+ return COMMAND;
510
+ }
511
+ subagentFinished(_call) {
512
+ return [];
513
+ }
514
+ }
515
+ export function readerFor(kind) {
516
+ if (kind === 'claude-agent-acp')
517
+ return new ClaudeReader();
518
+ if (kind === 'grok-build')
519
+ return new GrokReader();
520
+ return new CodexReader();
521
+ }
package/dist/files.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { HouseRefusal, retrying } from './api.js';
3
4
  import { relayed } from './relay.js';
4
5
  export async function placeFiles(house, directory, files, signal) {
5
6
  const paths = [];
@@ -20,3 +21,16 @@ export async function placeFiles(house, directory, files, signal) {
20
21
  }
21
22
  return paths;
22
23
  }
24
+ export function uploadBytes(upload, bytes) {
25
+ return retrying(async () => {
26
+ const answer = await fetch(relayed(upload.url), {
27
+ method: 'POST',
28
+ headers: { 'content-type': 'application/octet-stream', 'x-house-byte-operation': upload.operation },
29
+ body: bytes,
30
+ });
31
+ const text = await answer.text();
32
+ if (!answer.ok)
33
+ throw new HouseRefusal(upload.url, answer.status, text);
34
+ return JSON.parse(text);
35
+ });
36
+ }
@@ -3,14 +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 { HOUSE_REF } from './git.js';
6
7
  import { kitHome, 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";
19
+ const SANDBOXED = "your CLI's sandbox kept house from House; run house directly, not through another program";
14
20
  const REVOKED = 'this conversation no longer has House access; nothing to do from here';
15
21
  const UNCONNECTED = "this computer's own agent has no House connection; run kit login";
16
22
  const DISCONNECTED = "House no longer accepts this computer's own agent; run kit login";
@@ -42,7 +48,7 @@ function bridged(path, body, headers = { 'content-type': 'application/json' }, t
42
48
  reject(new Error(refused(path, answer.statusCode, text)));
43
49
  });
44
50
  });
45
- sent.on('error', () => reject(new Error(CLOSED)));
51
+ sent.on('error', (error) => reject(new Error(error.code === 'EPERM' || error.code === 'EACCES' ? SANDBOXED : CLOSED)));
46
52
  sent.end(body instanceof Buffer ? body : JSON.stringify(body));
47
53
  });
48
54
  }
@@ -78,11 +84,24 @@ async function localFile(path) {
78
84
  : `${path} is not a readable file`);
79
85
  }
80
86
  }
87
+ async function sent(url, bytes, operation) {
88
+ const headers = { 'content-type': 'application/octet-stream', 'x-house-byte-operation': operation };
89
+ if (socketPath !== undefined)
90
+ return bridged(url, bytes, headers);
91
+ setGlobalProxyFromEnv();
92
+ const answer = await fetch(relayed(url), { method: 'POST', headers, body: bytes }).catch(() => {
93
+ throw new Error(UNREACHABLE);
94
+ });
95
+ const text = await answer.text();
96
+ if (answer.status === 200)
97
+ return text;
98
+ throw new Error(refused(url, answer.status, text));
99
+ }
81
100
  async function upload(path, roomRef, missing) {
82
101
  if (typeof path !== 'string' || path === '')
83
102
  throw new Error(missing);
84
103
  const bytes = await localFile(path);
85
- const declared = JSON.parse(await bridged('/kit/attachments/upload', {
104
+ const declared = parse(await called('upload', {
86
105
  ...(roomRef === undefined ? {} : { room_ref: roomRef }),
87
106
  name: basename(path),
88
107
  bytes: bytes.length,
@@ -90,10 +109,7 @@ async function upload(path, roomRef, missing) {
90
109
  }));
91
110
  if (declared.upload === undefined)
92
111
  return declared.attachment;
93
- const saved = JSON.parse(await bridged(declared.upload.url, bytes, {
94
- 'content-type': 'application/octet-stream',
95
- 'x-house-byte-operation': declared.upload.operation,
96
- }));
112
+ const saved = JSON.parse(await sent(declared.upload.url, bytes, declared.upload.operation));
97
113
  if (saved.upload_failure !== undefined)
98
114
  throw new Error(`${path} was not saved: ${saved.upload_failure}; call again later`);
99
115
  return saved.attachment;
@@ -112,6 +128,14 @@ async function push(args) {
112
128
  throw new Error(pushed.text);
113
129
  return pushed.text;
114
130
  }
131
+ async function fetched(args) {
132
+ if (args.includes('--help'))
133
+ return FETCH_USAGE;
134
+ const answer = JSON.parse(await bridged('/git/fetch', {}, undefined, socketPath ?? join(kitHome(), 'owner.sock')));
135
+ if (answer.refused)
136
+ throw new Error(answer.text);
137
+ return answer.text;
138
+ }
115
139
  function argumentsOf(argument) {
116
140
  if (argument === undefined)
117
141
  return {};
@@ -128,7 +152,7 @@ function argumentsOf(argument) {
128
152
  }
129
153
  async function house([verb, argument]) {
130
154
  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');
155
+ return [USAGE, 'help', PUSH, FETCH, ...(await tools()).map((tool) => (tool.description === undefined ? tool.name : `${tool.name}: ${tool.description}`))].join('\n');
132
156
  }
133
157
  if (argument === '--help') {
134
158
  const tool = (await tools()).find((listed) => listed.name === verb);
@@ -137,25 +161,30 @@ async function house([verb, argument]) {
137
161
  return [...(tool.description === undefined ? [] : [tool.description]), JSON.stringify(tool.inputSchema)].join('\n');
138
162
  }
139
163
  const args = argumentsOf(argument);
140
- if (socketPath !== undefined && verb === 'upload_attachment') {
141
- return JSON.stringify({ attachment: await upload(args.path, args.room_ref, 'upload_attachment needs "path", a local file') });
164
+ if (verb === 'upload') {
165
+ return JSON.stringify({ attachment: await upload(args.path, args.room_ref, 'upload needs "path", a local file') });
142
166
  }
143
- if (socketPath !== undefined && verb === 'append_record' && Array.isArray(args.attachments)) {
167
+ if (verb === 'append_record' && Array.isArray(args.attachments)) {
144
168
  const attachments = [];
145
169
  for (const path of args.attachments) {
146
170
  attachments.push(await upload(path, args.room_ref, 'each attachments entry is the path of a local file'));
147
171
  }
148
172
  args.attachments = attachments;
149
173
  }
150
- const result = await mcp('tools/call', { name: verb, arguments: args });
174
+ return called(verb, args);
175
+ }
176
+ async function called(name, args) {
177
+ const result = await mcp('tools/call', { name, arguments: args });
151
178
  const text = result.content.map((content) => content.text).join('\n');
152
179
  if (result.isError === true)
153
180
  throw new Error(text.replace(/\n+$/, ''));
154
181
  return text;
155
182
  }
156
183
  const argv = process.argv.slice(2);
157
- (argv[0] === 'push' ? push(argv.slice(1)) : house(argv)).then((text) => {
184
+ (argv[0] === 'push' ? push(argv.slice(1)) : argv[0] === 'fetch' ? fetched(argv.slice(1)) : house(argv)).then((text) => {
158
185
  process.stdout.write(`${text}\n`);
186
+ if (argv[0] === 'shell')
187
+ process.exitCode = Number(/^exit: (\d+)(?:\n|$)/.exec(text)?.[1] ?? 0);
159
188
  }, (error) => {
160
189
  process.stderr.write(`house: ${error instanceof Error ? error.message : String(error)}\n`);
161
190
  process.exitCode = 1;
@@ -1,8 +1,9 @@
1
+ import { HOUSE_REF } from './git.js';
1
2
  import { HOW_WE_WORK } from './skills.js';
2
3
  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 `/private/rooms/<handle>/AGENTS.md`.';
5
- const COPIES_LINE = 'House Local copies; commit, then `house push`:';
4
+ const FILE_LINE = 'To give the User a file or a page, upload it with `house upload` and link it; never start a server.';
5
+ const READ_LINE = 'Before working in a Room, read `/rooms/private/rooms/<handle>/AGENTS.md`.';
6
+ const COPIES_LINE = `House Local copies; before work run \`house fetch\` and rebase onto \`${HOUSE_REF}\`; after it commit, then \`house push\`:`;
6
7
  const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
7
8
  const FAILED = /^exit: [1-9]\d*$/;
8
9
  const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;