@agentshouse/kit 0.2.1-cbabe8ad337ff → 0.2.1-cbff58027f078
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -46
- package/dist/acp.js +90 -19
- package/dist/afk.js +17 -0
- package/dist/agents.js +154 -44
- package/dist/api.js +7 -3
- package/dist/app-git.js +10 -26
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +11 -25
- package/dist/building.js +50 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +32 -0
- package/dist/chunks.js +49 -0
- package/dist/clis.js +95 -48
- package/dist/connection.js +105 -0
- package/dist/conversations.js +574 -179
- package/dist/copies.js +431 -218
- package/dist/end.js +29 -4
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +70 -8
- package/dist/files.js +139 -31
- package/dist/git.js +57 -21
- package/dist/home.js +89 -7
- package/dist/house-cli.js +299 -0
- package/dist/house-main.js +7 -177
- package/dist/instructions.js +8 -10
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -22
- package/dist/login.js +113 -23
- package/dist/logout.js +0 -2
- package/dist/mcp.js +6 -2
- package/dist/parts.js +44 -16
- package/dist/placement.js +31 -0
- package/dist/relay.js +33 -2
- package/dist/resident.js +111 -44
- package/dist/scope.js +15 -4
- package/dist/scripts.js +255 -0
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/sign-in.js +22 -39
- package/dist/silence.js +28 -0
- package/dist/skills.js +49 -35
- package/dist/transcription.js +85 -45
- package/dist/translate.js +2 -8
- package/dist/update.js +113 -105
- package/dist/wire.js +40 -0
- package/dist/work.js +3 -5
- package/package.json +20 -20
- package/dist/bootstrap.json +0 -24
- package/dist/clis-main.js +0 -23
- package/dist/stream.js +0 -72
- package/how-we-work.md +0 -23
- package/skills/ask-matt/LICENSE +0 -21
- package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
- package/skills/ask-matt/SKILL.md +0 -91
- package/skills/ask-matt/agents/openai.yaml +0 -4
- package/skills/code-review/LICENSE +0 -21
- package/skills/code-review/SKILL.md +0 -89
- package/skills/code-review/agents/openai.yaml +0 -3
- package/skills/codebase-design/DEEPENING.md +0 -37
- package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
- package/skills/codebase-design/LICENSE +0 -21
- package/skills/codebase-design/SKILL.md +0 -116
- package/skills/codebase-design/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/LICENSE +0 -21
- package/skills/diagnosing-bugs/SKILL.md +0 -140
- package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
- package/skills/domain-modeling/ADR-FORMAT.md +0 -47
- package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
- package/skills/domain-modeling/LICENSE +0 -21
- package/skills/domain-modeling/SKILL.md +0 -76
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/LICENSE +0 -21
- package/skills/grill-me/SKILL.md +0 -8
- package/skills/grill-me/agents/openai.yaml +0 -4
- package/skills/grill-with-docs/LICENSE +0 -21
- package/skills/grill-with-docs/SKILL.md +0 -8
- package/skills/grill-with-docs/agents/openai.yaml +0 -4
- package/skills/grilling/LICENSE +0 -21
- package/skills/grilling/SKILL.md +0 -30
- package/skills/grilling/agents/openai.yaml +0 -3
- package/skills/handoff/LICENSE +0 -21
- package/skills/handoff/SKILL.md +0 -17
- package/skills/handoff/agents/openai.yaml +0 -4
- package/skills/implement/LICENSE +0 -21
- package/skills/implement/SKILL.md +0 -16
- package/skills/implement/agents/openai.yaml +0 -4
- package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
- package/skills/improve-codebase-architecture/LICENSE +0 -21
- package/skills/improve-codebase-architecture/SKILL.md +0 -72
- package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
- package/skills/prototype/LICENSE +0 -21
- package/skills/prototype/LOGIC.md +0 -67
- package/skills/prototype/SKILL.md +0 -28
- package/skills/prototype/UI.md +0 -112
- package/skills/prototype/agents/openai.yaml +0 -3
- package/skills/research/LICENSE +0 -21
- package/skills/research/SKILL.md +0 -14
- package/skills/research/agents/openai.yaml +0 -3
- package/skills/resolving-merge-conflicts/LICENSE +0 -21
- package/skills/resolving-merge-conflicts/SKILL.md +0 -16
- package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
- package/skills/tdd/LICENSE +0 -21
- package/skills/tdd/SKILL.md +0 -40
- package/skills/tdd/agents/openai.yaml +0 -3
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/tests.md +0 -77
- package/skills/teach/GLOSSARY-FORMAT.md +0 -35
- package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
- package/skills/teach/LICENSE +0 -21
- package/skills/teach/MISSION-FORMAT.md +0 -31
- package/skills/teach/RESOURCES-FORMAT.md +0 -32
- package/skills/teach/SKILL.md +0 -141
- package/skills/teach/agents/openai.yaml +0 -4
- package/skills/to-questionnaire/LICENSE +0 -21
- package/skills/to-questionnaire/SKILL.md +0 -55
- package/skills/to-questionnaire/agents/openai.yaml +0 -4
- package/skills/to-spec/LICENSE +0 -21
- package/skills/to-spec/SKILL.md +0 -76
- package/skills/to-spec/agents/openai.yaml +0 -4
- package/skills/to-tickets/LICENSE +0 -21
- package/skills/to-tickets/SKILL.md +0 -106
- package/skills/to-tickets/agents/openai.yaml +0 -4
- package/skills/wait-what/LICENSE +0 -21
- package/skills/wait-what/SKILL.md +0 -8
- package/skills/wait-what/agents/openai.yaml +0 -4
- package/skills/wayfinder/LICENSE +0 -21
- package/skills/wayfinder/SKILL.md +0 -129
- package/skills/wayfinder/agents/openai.yaml +0 -4
- package/skills/writing-for-agents/LICENSE +0 -21
- package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
- package/skills/writing-for-agents/SKILL.md +0 -83
- package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/end.js
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
|
+
import { KIT_RELEASE } from './clis.js';
|
|
2
|
+
import { forgetRejected } from './home.js';
|
|
3
|
+
import { installCommand } from './placement.js';
|
|
1
4
|
import { killDescendants } from './scope.js';
|
|
2
|
-
export const REPLACED = 'another Kit started for this Environment';
|
|
3
|
-
|
|
4
|
-
export function end(reason) {
|
|
5
|
+
export const REPLACED = 'another Kit process started for this Environment';
|
|
6
|
+
function exit(reason, status) {
|
|
5
7
|
process.stderr.write(`kit: ${reason}\n`);
|
|
6
8
|
killDescendants();
|
|
7
|
-
process.exit(
|
|
9
|
+
process.exit(status);
|
|
10
|
+
}
|
|
11
|
+
export function end(reason) {
|
|
12
|
+
exit(reason, process.env.HOUSE_KIT_RESIDENT === String(process.pid) ? 0 : 1);
|
|
13
|
+
}
|
|
14
|
+
export function restart(reason) {
|
|
15
|
+
exit(reason, 1);
|
|
16
|
+
}
|
|
17
|
+
export function updateLine(lead) {
|
|
18
|
+
const command = installCommand();
|
|
19
|
+
return command === null ? `${lead}; install it again.` : `${lead}. Update it: ${command}`;
|
|
20
|
+
}
|
|
21
|
+
export function updateRequired() {
|
|
22
|
+
end(updateLine('This Kit is too old for House'));
|
|
23
|
+
}
|
|
24
|
+
export function unknownMethod(kind) {
|
|
25
|
+
return updateLine(`This Kit does not know ${kind}`);
|
|
26
|
+
}
|
|
27
|
+
export function newerThanHouse(release, command) {
|
|
28
|
+
end(`Kit ${KIT_RELEASE} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
|
|
29
|
+
}
|
|
30
|
+
export async function signInAgain(rejected) {
|
|
31
|
+
await forgetRejected(rejected);
|
|
32
|
+
restart('sign in again');
|
|
8
33
|
}
|
package/dist/enrol-main.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { writeEnrolment } from './home.js';
|
|
3
3
|
const [house, environment] = process.argv.slice(2);
|
|
4
|
-
|
|
4
|
+
const relay = process.env.HOUSE_KIT_RELAY || undefined;
|
|
5
|
+
await writeEnrolment({
|
|
6
|
+
house: house,
|
|
7
|
+
...(relay === undefined ? {} : { relay }),
|
|
8
|
+
environment: environment,
|
|
9
|
+
credential: readFileSync(0, 'utf8').trim(),
|
|
10
|
+
});
|
package/dist/events.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
const AUTONOMOUS_ORIGINS = new Set(['task-notification', 'peer', 'coordinator', 'observer', 'observer-activity']);
|
|
1
2
|
const COMMAND = { shown: 'command' };
|
|
2
3
|
const HIDDEN = { shown: 'hidden' };
|
|
3
4
|
const FINISHED = new Set(['completed', 'failed']);
|
|
4
5
|
export const RUNNING_JOB = new Set(['running', 'paused']);
|
|
5
|
-
const
|
|
6
|
+
const MESSAGE_TARGETS = new Map([
|
|
7
|
+
['send_agent_function_request', ['agent_function', 'agent']],
|
|
8
|
+
['cancel_agent_function_request', ['request']],
|
|
9
|
+
['send_mail', ['conversation', 'request']],
|
|
10
|
+
]);
|
|
11
|
+
function counted(reports) {
|
|
12
|
+
const whole = reports.every((tokens) => Object.values(tokens).every((count) => Number.isSafeInteger(count) && count >= 0));
|
|
13
|
+
return whole ? reports.map((tokens) => ({ kind: 'tokens', tokens })) : [];
|
|
14
|
+
}
|
|
6
15
|
function words(line) {
|
|
7
16
|
const found = [];
|
|
8
17
|
let word = null;
|
|
@@ -48,11 +57,13 @@ export function houseMessage(line) {
|
|
|
48
57
|
catch {
|
|
49
58
|
return null;
|
|
50
59
|
}
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
if (call === null)
|
|
61
|
+
return null;
|
|
62
|
+
const targets = MESSAGE_TARGETS.get(call.command);
|
|
63
|
+
if (targets === undefined)
|
|
53
64
|
return null;
|
|
54
|
-
const target = call.arguments?.[argument];
|
|
55
|
-
return { shown: 'message', command: call.command, target:
|
|
65
|
+
const target = targets.map((argument) => call.arguments?.[argument]).find((value) => typeof value === 'string');
|
|
66
|
+
return { shown: 'message', command: call.command, target: target ?? null };
|
|
56
67
|
}
|
|
57
68
|
function decidedByLine(command, final) {
|
|
58
69
|
const line = typeof command === 'string' ? command : Array.isArray(command) ? command.at(-1) : undefined;
|
|
@@ -269,6 +280,17 @@ class Reader {
|
|
|
269
280
|
class ClaudeReader extends Reader {
|
|
270
281
|
watchedOptions = { model: 'model' };
|
|
271
282
|
tasks = new Map();
|
|
283
|
+
stop = null;
|
|
284
|
+
answered(result) {
|
|
285
|
+
const models = (result?._meta?.quota?.model_usage ?? []);
|
|
286
|
+
const spent = counted(models.map(({ token_count: count }) => ({
|
|
287
|
+
input: count?.inputTokens,
|
|
288
|
+
output: count?.outputTokens,
|
|
289
|
+
cache_read: count?.cachedInputTokens,
|
|
290
|
+
cache_write: count?.cachedWriteTokens,
|
|
291
|
+
})));
|
|
292
|
+
return [...spent, ...super.answered(result)];
|
|
293
|
+
}
|
|
272
294
|
own(message) {
|
|
273
295
|
if (message.params?.update?._meta?.claudeCode?.parentToolUseId !== undefined)
|
|
274
296
|
return [];
|
|
@@ -276,7 +298,9 @@ class ClaudeReader extends Reader {
|
|
|
276
298
|
}
|
|
277
299
|
update(update) {
|
|
278
300
|
if (update.sessionUpdate === 'usage_update' && update._meta?.['_claude/origin'] !== undefined) {
|
|
279
|
-
|
|
301
|
+
const stop = this.stop;
|
|
302
|
+
this.stop = null;
|
|
303
|
+
return [...super.update(update), { kind: 'ended', stop }];
|
|
280
304
|
}
|
|
281
305
|
if (update.sessionUpdate === 'session_info_update' && update._meta?.jetbrains?.air?.sessionFailure !== undefined) {
|
|
282
306
|
return [{ kind: 'marker', marker: 'retry', text: update._meta.jetbrains.air.sessionFailure.title }];
|
|
@@ -287,6 +311,13 @@ class ClaudeReader extends Reader {
|
|
|
287
311
|
if (message.method !== '_claude/sdkMessage')
|
|
288
312
|
return [];
|
|
289
313
|
const sdk = message.params.message;
|
|
314
|
+
if (sdk.type === 'result') {
|
|
315
|
+
if (AUTONOMOUS_ORIGINS.has(sdk.origin?.kind))
|
|
316
|
+
this.stop = null;
|
|
317
|
+
else
|
|
318
|
+
this.stop = sdk.stop_reason === 'refusal' ? 'refusal' : sdk.is_error === true ? null : 'end_turn';
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
290
321
|
switch (sdk.subtype) {
|
|
291
322
|
case 'task_started':
|
|
292
323
|
if (sdk.tool_use_id !== undefined)
|
|
@@ -294,6 +325,10 @@ class ClaudeReader extends Reader {
|
|
|
294
325
|
return [];
|
|
295
326
|
case 'task_notification':
|
|
296
327
|
return sdk.tool_use_id === undefined ? [] : this.finish(sdk.tool_use_id, sdk.summary ?? null);
|
|
328
|
+
case 'task_progress':
|
|
329
|
+
if (!this.subagents.has(sdk.tool_use_id) || this.done.has(sdk.tool_use_id) || typeof sdk.description !== 'string')
|
|
330
|
+
return [];
|
|
331
|
+
return [{ kind: 'subagent-activity', id: sdk.tool_use_id, activity: sdk.description }];
|
|
297
332
|
default:
|
|
298
333
|
return [];
|
|
299
334
|
}
|
|
@@ -343,6 +378,20 @@ const COLLABORATION = new Set(['spawnAgent', 'sendInput', 'resumeAgent', 'wait',
|
|
|
343
378
|
class CodexReader extends Reader {
|
|
344
379
|
watchedOptions = { mode: 'mode', collaboration_mode: 'mode' };
|
|
345
380
|
children = new Map();
|
|
381
|
+
answered(result) {
|
|
382
|
+
const usage = result?.usage;
|
|
383
|
+
const spent = usage === null || usage === undefined
|
|
384
|
+
? []
|
|
385
|
+
: counted([
|
|
386
|
+
{
|
|
387
|
+
input: usage.inputTokens,
|
|
388
|
+
output: usage.outputTokens,
|
|
389
|
+
cache_read: usage.cachedReadTokens,
|
|
390
|
+
cache_write: usage.cachedWriteTokens,
|
|
391
|
+
},
|
|
392
|
+
]);
|
|
393
|
+
return [...spent, ...super.answered(result)];
|
|
394
|
+
}
|
|
346
395
|
own(message) {
|
|
347
396
|
if (message.method === 'session/request_permission' && message.params.toolCall?.kind === 'switch_mode') {
|
|
348
397
|
return [{ kind: 'started' }, { kind: 'marker', marker: 'mode', text: message.params.toolCall.title ?? '' }];
|
|
@@ -356,7 +405,7 @@ class CodexReader extends Reader {
|
|
|
356
405
|
if (codex?.threadStatus?.type === 'active')
|
|
357
406
|
return [{ kind: 'started' }];
|
|
358
407
|
if (codex?.threadStatus?.type === 'idle')
|
|
359
|
-
return [{ kind: 'ended' }];
|
|
408
|
+
return [{ kind: 'ended', stop: null }];
|
|
360
409
|
if (codex?.error !== undefined)
|
|
361
410
|
return [{ kind: 'marker', marker: 'retry', text: codex.error.message ?? '' }];
|
|
362
411
|
return [];
|
|
@@ -429,7 +478,9 @@ class GrokReader extends Reader {
|
|
|
429
478
|
return (message.params.runningPromptId ?? null) !== null && session === this.session ? [{ kind: 'started' }] : [];
|
|
430
479
|
}
|
|
431
480
|
if (message.params?.update?.sessionUpdate === 'turn_completed') {
|
|
432
|
-
|
|
481
|
+
if (session !== this.session)
|
|
482
|
+
return [];
|
|
483
|
+
return [...this.spent(message.params.update.usage), { kind: 'ended', stop: null }];
|
|
433
484
|
}
|
|
434
485
|
if (message.method === '_x.ai/models/update') {
|
|
435
486
|
this.offered(message.params);
|
|
@@ -437,6 +488,13 @@ class GrokReader extends Reader {
|
|
|
437
488
|
}
|
|
438
489
|
return super.read(message);
|
|
439
490
|
}
|
|
491
|
+
spent(usage) {
|
|
492
|
+
if (usage === null || usage === undefined)
|
|
493
|
+
return [];
|
|
494
|
+
const { inputTokens, outputTokens, cachedReadTokens, cacheCreationTokens } = usage;
|
|
495
|
+
const input = inputTokens - cachedReadTokens - cacheCreationTokens;
|
|
496
|
+
return counted([{ input, output: outputTokens, cache_read: cachedReadTokens, cache_write: cacheCreationTokens }]);
|
|
497
|
+
}
|
|
440
498
|
offered(models) {
|
|
441
499
|
this.models = models.availableModels ?? this.models;
|
|
442
500
|
this.model = models.currentModelId ?? this.model;
|
|
@@ -512,6 +570,10 @@ class GrokReader extends Reader {
|
|
|
512
570
|
return [];
|
|
513
571
|
}
|
|
514
572
|
}
|
|
573
|
+
export function answeredFailure(answer) {
|
|
574
|
+
const failure = answer._meta?.jetbrains?.air?.sessionFailure;
|
|
575
|
+
return typeof failure?.title === 'string' ? { title: failure.title, limit: failure.category === 'limit' } : null;
|
|
576
|
+
}
|
|
515
577
|
export function readerFor(kind) {
|
|
516
578
|
if (kind === 'claude-agent-acp')
|
|
517
579
|
return new ClaudeReader();
|
package/dist/files.js
CHANGED
|
@@ -1,36 +1,144 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { mkdir, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
|
+
import { Readable, Transform } from 'node:stream';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import { parseDocument } from 'yaml';
|
|
8
|
+
import { retryDelay } from './api.js';
|
|
9
|
+
import { Unsettled } from './mcp.js';
|
|
4
10
|
import { relayed } from './relay.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const STALLED_ROUNDS = 8;
|
|
12
|
+
const PART_CHECKSUM = 'x-amz-checksum-sha256';
|
|
13
|
+
const LINK = /\]\((\/rooms\/[^/)\s]+\/[^)\s]+)\)/gu;
|
|
14
|
+
export function linkedPaths(text) {
|
|
15
|
+
return [...new Set(Array.from(text.matchAll(LINK), (link) => link[1]))];
|
|
16
|
+
}
|
|
17
|
+
export function inspectedFile(answer) {
|
|
18
|
+
const document = parseDocument(answer);
|
|
19
|
+
if (document.errors.length > 0)
|
|
20
|
+
return null;
|
|
21
|
+
const inspected = document.toJS();
|
|
22
|
+
if (typeof inspected?.download !== 'string')
|
|
23
|
+
return null;
|
|
24
|
+
return {
|
|
25
|
+
download: inspected.download,
|
|
26
|
+
sha256: String(inspected.sha256),
|
|
27
|
+
media_type: typeof inspected.media_type === 'string' ? inspected.media_type : null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function paused(milliseconds) {
|
|
31
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
32
|
+
}
|
|
33
|
+
export function storeRefusal(act, status) {
|
|
34
|
+
return new Error(`the ${act} answered ${status}; ${act} the file again`);
|
|
35
|
+
}
|
|
36
|
+
async function partDigests(uploading, partBytes) {
|
|
37
|
+
const whole = createHash('sha256');
|
|
38
|
+
const digests = [];
|
|
39
|
+
for (let start = 0; digests.length === 0 || start < uploading.bytes; start += partBytes) {
|
|
40
|
+
const end = Math.min(start + partBytes, uploading.bytes);
|
|
41
|
+
const digest = createHash('sha256');
|
|
42
|
+
for await (const chunk of uploading.read(start, end)) {
|
|
43
|
+
digest.update(chunk);
|
|
44
|
+
whole.update(chunk);
|
|
45
|
+
}
|
|
46
|
+
digests.push(digest.digest());
|
|
47
|
+
}
|
|
48
|
+
if (whole.digest('hex') !== uploading.sha256)
|
|
49
|
+
throw new Error(`${uploading.name} changed while it was uploaded; upload it again`);
|
|
50
|
+
return digests;
|
|
51
|
+
}
|
|
52
|
+
export async function uploaded(declared, uploading) {
|
|
53
|
+
const partBytes = declared.part_bytes;
|
|
54
|
+
const digests = await partDigests(uploading, partBytes);
|
|
55
|
+
const partSha256 = digests.map((digest) => digest.toString('hex'));
|
|
56
|
+
let answer = await finishing(uploading, declared.id, partSha256);
|
|
57
|
+
let idle = 0;
|
|
58
|
+
let stalled = 0;
|
|
59
|
+
while ('upload' in answer) {
|
|
60
|
+
const { id, parts } = answer.upload;
|
|
61
|
+
let sent = 0;
|
|
62
|
+
let failure = null;
|
|
63
|
+
for (const { part, url } of parts ?? []) {
|
|
64
|
+
const start = (part - 1) * partBytes;
|
|
65
|
+
try {
|
|
66
|
+
await uploading.send(url, start, Math.min(start + partBytes, uploading.bytes), {
|
|
67
|
+
[PART_CHECKSUM]: digests[part - 1].toString('base64'),
|
|
68
|
+
});
|
|
69
|
+
sent++;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
failure = error;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (sent > 0) {
|
|
77
|
+
idle = 0;
|
|
78
|
+
stalled = 0;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
if (failure !== null && ++stalled >= STALLED_ROUNDS)
|
|
82
|
+
throw failure;
|
|
83
|
+
// A round that sent no part, its store unreachable, is followed by the next finishing call on the jittered curve.
|
|
84
|
+
await paused(retryDelay(idle++));
|
|
85
|
+
}
|
|
86
|
+
answer = await finishing(uploading, id, partSha256);
|
|
87
|
+
}
|
|
88
|
+
return answer;
|
|
89
|
+
}
|
|
90
|
+
async function finishing(uploading, upload, partSha256) {
|
|
91
|
+
for (let unsettled = 1;; unsettled++) {
|
|
92
|
+
try {
|
|
93
|
+
return await uploading.finish(upload, partSha256);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (!(error instanceof Unsettled) || unsettled === STALLED_ROUNDS)
|
|
15
97
|
throw error;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
await writeFile(path, Buffer.from(await answer.arrayBuffer()));
|
|
20
|
-
paths.push(path);
|
|
98
|
+
}
|
|
99
|
+
// A finishing call House did not settle, unreached or refused short of final, is sent again on the jittered curve as a conversation resends it, so a dropped connection or a House restart resumes the upload instead of ending it.
|
|
100
|
+
await paused(retryDelay(unsettled - 1));
|
|
21
101
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
102
|
+
}
|
|
103
|
+
async function drained(body) {
|
|
104
|
+
for await (const _chunk of body)
|
|
105
|
+
;
|
|
106
|
+
}
|
|
107
|
+
export async function downloaded(address, fetching, destination, name) {
|
|
108
|
+
for (let asked = 1;; asked++) {
|
|
109
|
+
const { download, sha256 } = await address();
|
|
110
|
+
const answer = await fetching(download);
|
|
111
|
+
if (answer.status === 403 && asked === 1) {
|
|
112
|
+
await drained(answer.body);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (answer.status !== 200) {
|
|
116
|
+
await drained(answer.body);
|
|
117
|
+
throw storeRefusal('download', answer.status);
|
|
118
|
+
}
|
|
119
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
120
|
+
const partial = join(dirname(destination), `.${basename(destination)}.${randomUUID()}`);
|
|
121
|
+
const digest = createHash('sha256');
|
|
122
|
+
const hashed = new Transform({
|
|
123
|
+
transform(chunk, _encoding, done) {
|
|
124
|
+
digest.update(chunk);
|
|
125
|
+
done(null, chunk);
|
|
126
|
+
},
|
|
30
127
|
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
128
|
+
try {
|
|
129
|
+
await pipeline(answer.body, hashed, createWriteStream(partial, { flags: 'wx' }));
|
|
130
|
+
if (digest.digest('hex') !== sha256)
|
|
131
|
+
throw new Error(`${name} did not arrive whole; download it again`);
|
|
132
|
+
await rename(partial, destination);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
await rm(partial, { force: true });
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export async function fetched(url, signal) {
|
|
142
|
+
const answer = await fetch(relayed(url), { signal });
|
|
143
|
+
return { status: answer.status, body: answer.body === null ? Readable.from([]) : Readable.fromWeb(answer.body) };
|
|
36
144
|
}
|
package/dist/git.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { isUtf8 } from 'node:buffer';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
|
-
import { withoutProxy } from './clis.js';
|
|
5
5
|
import { workspaceRoot } from './home.js';
|
|
6
|
+
import { childEnvironment } from './relay.js';
|
|
6
7
|
export function localCopies() {
|
|
7
8
|
return join(workspaceRoot(), 'local-copies');
|
|
8
9
|
}
|
|
9
10
|
export const HOUSE_REF = 'refs/house/received';
|
|
10
|
-
const COMMITTER = '
|
|
11
|
+
const COMMITTER = 'Kit <house-kit@localhost>';
|
|
11
12
|
export function run(cwd, args, input, env = {}) {
|
|
12
13
|
return new Promise((resolve, reject) => {
|
|
13
|
-
const child = spawn('git', args, { cwd, env: { ...
|
|
14
|
+
const child = spawn('git', args, { cwd, env: { ...childEnvironment(), ...env } });
|
|
14
15
|
const stdout = [];
|
|
15
16
|
let stderr = '';
|
|
16
17
|
child.stdout.on('data', (chunk) => stdout.push(chunk));
|
|
@@ -48,14 +49,15 @@ export async function tree(repository, commit) {
|
|
|
48
49
|
}
|
|
49
50
|
return entries;
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
async function blob(repository, object) {
|
|
53
|
+
return (await run(repository, ['cat-file', 'blob', object])).stdout;
|
|
54
|
+
}
|
|
55
|
+
export async function blobText(repository, object) {
|
|
56
|
+
return (await blob(repository, object)).toString('utf8');
|
|
57
|
+
}
|
|
58
|
+
export async function isText(repository, object, maxBytes) {
|
|
59
|
+
const bytes = await blob(repository, object);
|
|
60
|
+
return bytes.length <= maxBytes && isUtf8(bytes) && !bytes.includes(0);
|
|
59
61
|
}
|
|
60
62
|
export function blobId(content) {
|
|
61
63
|
const bytes = Buffer.from(content, 'utf8');
|
|
@@ -68,18 +70,52 @@ function data(text) {
|
|
|
68
70
|
const bytes = Buffer.from(text, 'utf8');
|
|
69
71
|
return [Buffer.from(`data ${bytes.length}\n`), bytes, Buffer.from('\n')];
|
|
70
72
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
function written(stream, bytes) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
stream.write(bytes, (error) => (error ? reject(error) : resolve()));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function imported(stream, commits) {
|
|
79
|
+
await written(stream, Buffer.from('feature done\n'));
|
|
73
80
|
for (const [index, commit] of commits.entries()) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
await written(stream, Buffer.concat([
|
|
82
|
+
Buffer.from(`commit ${HOUSE_REF}\nmark :${index + 1}\ncommitter ${COMMITTER} ${Math.floor(Date.now() / 1000)} +0000\n`),
|
|
83
|
+
...data(commit.message),
|
|
84
|
+
...(commit.from === null ? [] : [Buffer.from(`from ${commit.from}\n`)]),
|
|
85
|
+
...(commit.merge === undefined ? [] : [Buffer.from(`merge ${commit.merge}\n`)]),
|
|
86
|
+
...(commit.replaces === true ? [Buffer.from('deleteall\n')] : []),
|
|
87
|
+
]));
|
|
88
|
+
for await (const [path, content] of commit.files) {
|
|
89
|
+
await written(stream, content === null
|
|
90
|
+
? Buffer.from(`D ${quoted(path)}\n`)
|
|
91
|
+
: Buffer.concat([Buffer.from(`M 100644 inline ${quoted(path)}\n`), ...data(content)]));
|
|
80
92
|
}
|
|
81
|
-
stream
|
|
93
|
+
await written(stream, Buffer.from('\n'));
|
|
94
|
+
}
|
|
95
|
+
await written(stream, Buffer.from('done\n'));
|
|
96
|
+
}
|
|
97
|
+
export async function importCommits(repository, commits) {
|
|
98
|
+
const child = spawn('git', ['fast-import', '--quiet', '--force'], { cwd: repository, env: childEnvironment() });
|
|
99
|
+
let stderr = '';
|
|
100
|
+
child.stdout.resume();
|
|
101
|
+
child.stderr.on('data', (chunk) => {
|
|
102
|
+
stderr += chunk.toString('utf8');
|
|
103
|
+
});
|
|
104
|
+
child.stdin.on('error', () => undefined);
|
|
105
|
+
const closed = new Promise((resolve, reject) => {
|
|
106
|
+
child.on('error', reject);
|
|
107
|
+
child.on('close', resolve);
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
await imported(child.stdin, commits);
|
|
111
|
+
child.stdin.end();
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
child.stdin.destroy();
|
|
115
|
+
await closed;
|
|
116
|
+
throw error.code === 'EPIPE' ? new Error(`git fast-import failed: ${stderr.trim()}`) : error;
|
|
82
117
|
}
|
|
83
|
-
|
|
118
|
+
if ((await closed) !== 0)
|
|
119
|
+
throw new Error(`git fast-import failed: ${stderr.trim()}`);
|
|
84
120
|
return revision(repository, HOUSE_REF);
|
|
85
121
|
}
|
package/dist/home.js
CHANGED
|
@@ -1,13 +1,62 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { accessSync, chmodSync, constants, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync } from 'node:fs';
|
|
2
3
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { homedir, tmpdir } from 'node:os';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { selectRelay } from './relay.js';
|
|
5
7
|
const ENROLMENT = 'credential.json';
|
|
6
8
|
const DISCONNECTED = 'disconnected.json';
|
|
7
9
|
const CONFIGURATION = 'kit.json';
|
|
8
10
|
const OWN_AGENT = 'own-agent.json';
|
|
11
|
+
let defaulted;
|
|
12
|
+
function writable(directory) {
|
|
13
|
+
try {
|
|
14
|
+
accessSync(directory, constants.W_OK | constants.X_OK);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function privateDirectory(path) {
|
|
22
|
+
try {
|
|
23
|
+
mkdirSync(path, { mode: 0o700 });
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code !== 'EEXIST')
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
const found = lstatSync(path);
|
|
30
|
+
if (found.isSymbolicLink())
|
|
31
|
+
throw new Error(`${path} is a symbolic link`);
|
|
32
|
+
if (!found.isDirectory())
|
|
33
|
+
throw new Error(`${path} is not a directory`);
|
|
34
|
+
if (found.uid !== process.geteuid())
|
|
35
|
+
throw new Error(`${path} is not owned by this User`);
|
|
36
|
+
chmodSync(path, 0o700);
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
function ownTemporaryHome() {
|
|
40
|
+
return privateDirectory(join(tmpdir(), `house-kit-${process.getuid()}`));
|
|
41
|
+
}
|
|
42
|
+
function defaultHome() {
|
|
43
|
+
const home = join(homedir(), '.house-kit');
|
|
44
|
+
return existsSync(home) || writable(homedir()) ? home : ownTemporaryHome();
|
|
45
|
+
}
|
|
9
46
|
export function kitHome() {
|
|
10
|
-
return process.env.HOUSE_KIT_HOME
|
|
47
|
+
return process.env.HOUSE_KIT_HOME || (defaulted ??= defaultHome());
|
|
48
|
+
}
|
|
49
|
+
export function socketDirectory() {
|
|
50
|
+
const sockets = privateDirectory(join(realpathSync('/tmp'), `hk-${process.geteuid()}`));
|
|
51
|
+
const home = kitHome();
|
|
52
|
+
const physical = existsSync(home) ? realpathSync(home) : resolve(home);
|
|
53
|
+
return privateDirectory(join(sockets, createHash('sha256').update(physical).digest('hex').slice(0, 12)));
|
|
54
|
+
}
|
|
55
|
+
export function ownerSocket() {
|
|
56
|
+
return join(socketDirectory(), 'owner.sock');
|
|
57
|
+
}
|
|
58
|
+
export function ownerLock() {
|
|
59
|
+
return join(socketDirectory(), 'owner.lock');
|
|
11
60
|
}
|
|
12
61
|
export function workspaceRoot() {
|
|
13
62
|
return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
|
|
@@ -28,12 +77,15 @@ export async function readHome(name) {
|
|
|
28
77
|
export async function writeHome(name, value) {
|
|
29
78
|
const home = kitHome();
|
|
30
79
|
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
31
|
-
const temporary = join(home, `${name}.${
|
|
80
|
+
const temporary = join(home, `${name}.${randomUUID()}`);
|
|
32
81
|
await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
33
82
|
await rename(temporary, join(home, name));
|
|
34
83
|
}
|
|
35
|
-
export function readEnrolment() {
|
|
36
|
-
|
|
84
|
+
export async function readEnrolment() {
|
|
85
|
+
const enrolment = await readHome(ENROLMENT);
|
|
86
|
+
if (enrolment !== null)
|
|
87
|
+
selectRelay(process.env.HOUSE_KIT_RELAY || enrolment.relay);
|
|
88
|
+
return enrolment;
|
|
37
89
|
}
|
|
38
90
|
export async function writeEnrolment(enrolment) {
|
|
39
91
|
await writeHome(ENROLMENT, enrolment);
|
|
@@ -42,10 +94,40 @@ export async function writeEnrolment(enrolment) {
|
|
|
42
94
|
export function readDisconnected() {
|
|
43
95
|
return readHome(DISCONNECTED);
|
|
44
96
|
}
|
|
97
|
+
export function forgetDisconnected() {
|
|
98
|
+
return rm(join(kitHome(), DISCONNECTED), { force: true });
|
|
99
|
+
}
|
|
45
100
|
export async function forgetEnrolment({ house, environment }) {
|
|
46
101
|
await writeHome(DISCONNECTED, { house, environment });
|
|
47
102
|
await rm(join(kitHome(), ENROLMENT), { force: true });
|
|
48
103
|
}
|
|
104
|
+
export async function forgetRejected(rejected) {
|
|
105
|
+
const held = await readHome(ENROLMENT);
|
|
106
|
+
if (held?.credential !== rejected)
|
|
107
|
+
return;
|
|
108
|
+
await writeHome(DISCONNECTED, { house: held.house, environment: held.environment });
|
|
109
|
+
const enrolment = join(kitHome(), ENROLMENT);
|
|
110
|
+
const taken = `${enrolment}.${process.pid}`;
|
|
111
|
+
try {
|
|
112
|
+
renameSync(enrolment, taken);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error.code === 'ENOENT')
|
|
116
|
+
return;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
if (JSON.parse(readFileSync(taken, 'utf8')).credential !== rejected) {
|
|
120
|
+
try {
|
|
121
|
+
linkSync(taken, enrolment);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error.code !== 'EEXIST')
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
rmSync(join(kitHome(), DISCONNECTED), { force: true });
|
|
128
|
+
}
|
|
129
|
+
rmSync(taken);
|
|
130
|
+
}
|
|
49
131
|
export function enrolled() {
|
|
50
132
|
return existsSync(join(kitHome(), ENROLMENT));
|
|
51
133
|
}
|