@agentshouse/kit 0.1.0-alpha.2 → 0.1.0-alpha.21
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 +299 -26
- package/bin/connect-linux.sh +85 -13
- package/bin/kit.js +23 -3
- package/bootstrap.json +17 -0
- package/package.json +7 -2
- package/src/adapter-mcp.js +0 -13
- package/src/agents.js +63 -8
- package/src/binding.js +41 -0
- package/src/bridge.js +1 -1
- package/src/contract.js +21 -3
- package/src/git-main.js +138 -0
- package/src/git.js +62 -0
- package/src/host.js +6 -2
- package/src/house-main.js +65 -8
- package/src/house.js +345 -73
- package/src/inbound.js +2 -18
- package/src/kit-work-stream.js +88 -0
- package/src/launcher.js +9 -3
- package/src/local-files.js +3 -11
- package/src/login.js +2 -1
- package/src/mcp.js +13 -0
- package/src/mount.js +13 -21
- package/src/repository.js +3 -61
- package/src/session-bin/git +2 -0
- package/src/setup.js +7 -2
- package/src/source.js +44 -30
- package/src/sync-library.js +7 -11
- package/src/sync-state.js +4 -0
- package/src/sync.js +15 -10
- package/src/telegram.js +29 -11
- package/src/worker.js +529 -116
- package/src/working-copy.js +1210 -0
package/bootstrap.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"version": "0.1.0-alpha.21",
|
|
4
|
+
"hosts": {
|
|
5
|
+
"linux": {
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.21/connect-linux.sh)",
|
|
7
|
+
"asset": {
|
|
8
|
+
"name": "connect-linux.sh",
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.21/connect-linux.sh"
|
|
10
|
+
},
|
|
11
|
+
"workspace": {
|
|
12
|
+
"argument": "--workspace",
|
|
13
|
+
"quoting": "posix"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentshouse/kit",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,11 +9,16 @@
|
|
|
9
9
|
"private": false,
|
|
10
10
|
"type": "module",
|
|
11
11
|
"bin": {
|
|
12
|
+
"house": "src/session-bin/house",
|
|
12
13
|
"kit": "bin/kit.js"
|
|
13
14
|
},
|
|
15
|
+
"exports": {
|
|
16
|
+
"./bootstrap": "./bootstrap.json"
|
|
17
|
+
},
|
|
14
18
|
"dependencies": {
|
|
15
19
|
"@agentclientprotocol/sdk": "1.5.0",
|
|
16
|
-
"@agentshouse/mdmodel": "0.1.0-alpha.
|
|
20
|
+
"@agentshouse/mdmodel": "0.1.0-alpha.15",
|
|
21
|
+
"ws": "8.22.0",
|
|
17
22
|
"yaml": "2.9.1",
|
|
18
23
|
"zod": "4.6.5"
|
|
19
24
|
},
|
package/src/adapter-mcp.js
CHANGED
|
@@ -29,16 +29,3 @@ export async function commandHouseInstructions(bridge) {
|
|
|
29
29
|
}
|
|
30
30
|
return frame.result.instructions;
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
export async function commandHouseRequest(bridge) {
|
|
34
|
-
const frame = await commandHouseReply(bridge, { method: "tools/call", params: { name: "session_get_request", arguments: {} } });
|
|
35
|
-
if (frame.error !== undefined || frame.result?.isError === true) throw new KitRefusal("kit_run_input_unreadable");
|
|
36
|
-
try {
|
|
37
|
-
const content = frame.result.content;
|
|
38
|
-
const input = JSON.parse(content.find((item) => item.type === "text").text);
|
|
39
|
-
if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error();
|
|
40
|
-
return input;
|
|
41
|
-
} catch {
|
|
42
|
-
throw new KitRefusal("kit_run_input_unreadable");
|
|
43
|
-
}
|
|
44
|
-
}
|
package/src/agents.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { readFile, readdir, rm } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "./adapter.js";
|
|
12
12
|
import { inheritedEnvironment } from "./command.js";
|
|
13
13
|
import {
|
|
14
|
+
ADAPTER_KINDS,
|
|
14
15
|
AGENT_HOME,
|
|
15
16
|
AGENT_INSTALL_TIMEOUT_MS,
|
|
16
17
|
AGENT_RELEASES_HOME,
|
|
@@ -261,6 +262,60 @@ export async function updateDeclaredAgents(options, configuration, dependencies
|
|
|
261
262
|
return updated;
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
export async function removeUndeclaredAgents(directory, wanted, keep = false) {
|
|
266
|
+
const home = join(directory, AGENT_HOME);
|
|
267
|
+
let entries;
|
|
268
|
+
try {
|
|
269
|
+
entries = await readdir(home, { withFileTypes: true });
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error.code === "ENOENT") return [];
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
const retained = [];
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
if (!wanted.includes(entry.name)) {
|
|
277
|
+
if (keep && ADAPTER_KINDS.includes(entry.name)) retained.push(entry.name);
|
|
278
|
+
else await rm(join(home, entry.name), { recursive: true, force: true });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return retained;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function reconcileAgentKinds(
|
|
285
|
+
options,
|
|
286
|
+
session,
|
|
287
|
+
configuration,
|
|
288
|
+
dependencies = {},
|
|
289
|
+
signal = session.signal,
|
|
290
|
+
state = {},
|
|
291
|
+
) {
|
|
292
|
+
const selected = { ...configuration, agents: await desiredAgents(session, signal) };
|
|
293
|
+
state.onDesired?.(selected.agents);
|
|
294
|
+
const newlyWanted = selected.agents.filter((kind) => !state.lastDesired?.includes(kind));
|
|
295
|
+
const retained = await removeUndeclaredAgents(options.directory, selected.agents, state.busy);
|
|
296
|
+
const pointerKinds = state.busy
|
|
297
|
+
? (
|
|
298
|
+
await Promise.all(
|
|
299
|
+
newlyWanted.map(async (kind) =>
|
|
300
|
+
(await readAgentState(options.directory, kind)).release === null ? kind : null,
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
).filter((kind) => kind !== null)
|
|
304
|
+
: newlyWanted;
|
|
305
|
+
const updated = await updateDeclaredAgents(
|
|
306
|
+
{ directory: options.directory, at: dependencies.now?.(), forced: state.forced },
|
|
307
|
+
{ ...selected, agents: state.pointer ? pointerKinds : selected.agents },
|
|
308
|
+
dependencies,
|
|
309
|
+
);
|
|
310
|
+
const readiness = await agentReadiness(
|
|
311
|
+
{ directory: options.directory },
|
|
312
|
+
{ ...selected, agents: [...selected.agents, ...retained] },
|
|
313
|
+
dependencies,
|
|
314
|
+
);
|
|
315
|
+
await reportAgentCapabilities(session, readiness.agents, signal);
|
|
316
|
+
return { selected, updated, reported: readiness.agents, refused: readiness.refused, retained };
|
|
317
|
+
}
|
|
318
|
+
|
|
264
319
|
export async function agentSignedIn(directory, kind, dependencies = {}) {
|
|
265
320
|
const contract = compiledContract(kind);
|
|
266
321
|
const state = await readAgentState(directory, kind);
|
|
@@ -341,15 +396,15 @@ export async function updateAgents(options, dependencies = {}) {
|
|
|
341
396
|
return maintainControl(
|
|
342
397
|
session,
|
|
343
398
|
async (signal) => {
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
399
|
+
const result = await reconcileAgentKinds(
|
|
400
|
+
options,
|
|
401
|
+
session,
|
|
402
|
+
configuration,
|
|
348
403
|
dependencies,
|
|
404
|
+
signal,
|
|
405
|
+
{ forced: true },
|
|
349
406
|
);
|
|
350
|
-
|
|
351
|
-
await reportAgentCapabilities(session, agents, signal);
|
|
352
|
-
return { updated, reported: agents };
|
|
407
|
+
return { updated: result.updated, reported: result.reported };
|
|
353
408
|
},
|
|
354
409
|
options.signal,
|
|
355
410
|
);
|
package/src/binding.js
CHANGED
|
@@ -15,6 +15,18 @@ function bindingPath(directory, tmux) {
|
|
|
15
15
|
return join(bindingDirectory(directory), `${tmux}.json`);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function stoppedNote(stopped) {
|
|
19
|
+
if (stopped === null || stopped === undefined) return null;
|
|
20
|
+
return {
|
|
21
|
+
turn_completed: stopped.turnCompleted,
|
|
22
|
+
capacity_exhausted: stopped.capacityExhausted,
|
|
23
|
+
provider_refused: stopped.providerRefused,
|
|
24
|
+
continuable: stopped.continuable,
|
|
25
|
+
failure: stopped.failure,
|
|
26
|
+
text_accepted: stopped.textAccepted,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
export async function writeBinding(directory, binding) {
|
|
19
31
|
const reported = binding.reported ?? null;
|
|
20
32
|
await writeNote(bindingPath(directory, binding.tmux), {
|
|
@@ -29,6 +41,7 @@ export async function writeBinding(directory, binding) {
|
|
|
29
41
|
sealed: binding.sealed === null || binding.sealed === undefined
|
|
30
42
|
? null
|
|
31
43
|
: { operation: binding.sealed.operation, blocks: binding.sealed.blocks },
|
|
44
|
+
stopped: stoppedNote(binding.stopped),
|
|
32
45
|
reported: reported === null ? null : reportedNote(reported),
|
|
33
46
|
});
|
|
34
47
|
}
|
|
@@ -45,6 +58,31 @@ function retainedSeal(stored) {
|
|
|
45
58
|
return { operation: stored.operation, blocks: stored.blocks };
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
function retainedFailure(stored) {
|
|
62
|
+
if (stored === null) return null;
|
|
63
|
+
if (typeof stored !== "object" || Array.isArray(stored)) return undefined;
|
|
64
|
+
if (typeof stored.code !== "string" || stored.code.length === 0) return undefined;
|
|
65
|
+
if (stored.detail !== null && typeof stored.detail !== "string") return undefined;
|
|
66
|
+
return { code: stored.code, detail: stored.detail };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function retainedStop(stored) {
|
|
70
|
+
if (stored === null || stored === undefined) return null;
|
|
71
|
+
if (typeof stored !== "object" || Array.isArray(stored)) return undefined;
|
|
72
|
+
const flags = ["turn_completed", "capacity_exhausted", "provider_refused", "continuable", "text_accepted"];
|
|
73
|
+
if (flags.some((flag) => typeof stored[flag] !== "boolean")) return undefined;
|
|
74
|
+
const failure = retainedFailure(stored.failure);
|
|
75
|
+
if (failure === undefined) return undefined;
|
|
76
|
+
return {
|
|
77
|
+
turnCompleted: stored.turn_completed,
|
|
78
|
+
capacityExhausted: stored.capacity_exhausted,
|
|
79
|
+
providerRefused: stored.provider_refused,
|
|
80
|
+
continuable: stored.continuable,
|
|
81
|
+
failure,
|
|
82
|
+
textAccepted: stored.text_accepted,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
48
86
|
function completeBinding(directory, stored) {
|
|
49
87
|
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return null;
|
|
50
88
|
const named = [stored.run, stored.session, stored.tmux, stored.socket, stored.nonce];
|
|
@@ -54,6 +92,8 @@ function completeBinding(directory, stored) {
|
|
|
54
92
|
if (stored.owner_stop !== null && typeof stored.owner_stop !== "string") return null;
|
|
55
93
|
const sealed = retainedSeal(stored.sealed);
|
|
56
94
|
if (sealed === undefined) return null;
|
|
95
|
+
const stopped = retainedStop(stored.stopped);
|
|
96
|
+
if (stopped === undefined) return null;
|
|
57
97
|
const reported = retainedReport(stored.reported);
|
|
58
98
|
if (reported === undefined) return null;
|
|
59
99
|
return {
|
|
@@ -65,6 +105,7 @@ function completeBinding(directory, stored) {
|
|
|
65
105
|
conversation: stored.conversation === true,
|
|
66
106
|
ownerStop: stored.owner_stop,
|
|
67
107
|
sealed,
|
|
108
|
+
stopped,
|
|
68
109
|
reported,
|
|
69
110
|
};
|
|
70
111
|
}
|
package/src/bridge.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { operationId } from "./operation.js";
|
|
19
19
|
import { houseFailed } from "./refusal.js";
|
|
20
20
|
|
|
21
|
-
function bridgedFrame(request, operation) {
|
|
21
|
+
export function bridgedFrame(request, operation) {
|
|
22
22
|
if (typeof request.method !== "string") return request;
|
|
23
23
|
const params = typeof request.params === "object" && request.params !== null ? request.params : {};
|
|
24
24
|
const declared = typeof params._meta === "object" && params._meta !== null ? params._meta : {};
|
package/src/contract.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CLAUDE_PASTED_CODE, CODEX_DEVICE_CODE, GROK_DEVICE_CODE } from "./agent-logins.js";
|
|
2
2
|
|
|
3
|
-
export const KIT_CONTRACT_VERSION = "
|
|
3
|
+
export const KIT_CONTRACT_VERSION = "1";
|
|
4
4
|
export const KIT_CAPTURE_BATCH_MAX = 100;
|
|
5
5
|
export const COMMAND_INPUT_MAX_BYTES = 4 * 1024;
|
|
6
6
|
export const AUTHORIZATION_OUTPUT_MAX_BYTES = 4 * 1024;
|
|
@@ -40,6 +40,10 @@ export function runOutcomePath(run) {
|
|
|
40
40
|
return `/kit/runs/${encodeURIComponent(run)}/outcome`;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export function runRequestPath(run) {
|
|
44
|
+
return `/kit/runs/${encodeURIComponent(run)}/request`;
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
export const KIT_SESSIONS_REPORT_PATH = "/kit/sessions/report";
|
|
44
48
|
export const KIT_SESSIONS_REPORT_MAX = 256;
|
|
45
49
|
|
|
@@ -195,7 +199,7 @@ export const ADAPTER_CONTRACTS = Object.freeze({
|
|
|
195
199
|
guidedLogin: CLAUDE_PASTED_CODE,
|
|
196
200
|
identity: Object.freeze({ agentInfo: "@agentclientprotocol/claude-agent-acp" }),
|
|
197
201
|
nativeQuestion: null,
|
|
198
|
-
sessionLoad:
|
|
202
|
+
sessionLoad: true,
|
|
199
203
|
sessionConfig: Object.freeze({ model: "model", effort: "effort" }),
|
|
200
204
|
capabilities: Object.freeze({
|
|
201
205
|
field_kinds: ELICITATION_FIELD_KINDS,
|
|
@@ -288,9 +292,23 @@ export const KIT_DOOR_DISCOVER_PATH = "/kit/door/discover";
|
|
|
288
292
|
export const KIT_DOOR_BOOTSTRAP_PATH = "/kit/door/bootstrap";
|
|
289
293
|
export const KIT_DOOR_ENUMERATE_PATH = "/kit/door/enumerate";
|
|
290
294
|
export const KIT_DOOR_BATCH_PATH = "/kit/door/batch";
|
|
291
|
-
export const
|
|
295
|
+
export const KIT_DOOR_REVISIONS_PATH = "/kit/door/revisions";
|
|
296
|
+
export const EXPLICIT_SYNC_VERSION = "explicit-sync/2";
|
|
292
297
|
export const KIT_DOOR_UNMOUNT_PATH = "/kit/door/unmount";
|
|
293
298
|
|
|
299
|
+
export const KIT_WORKING_COPY_SELECTION_PATH = "/kit/working-copy/selection";
|
|
300
|
+
export const WORKING_COPY_VERSION = "working-copy-state/3";
|
|
301
|
+
export const WORKING_COPIES_HOME = "working-copies";
|
|
302
|
+
export const WORKING_COPY_ROOM_HOMES = ["capture", "library", "collaboration"];
|
|
303
|
+
export const WORKING_COPY_PRIVATE_HOMES = ["library", "collaboration", "rooms", "agents", "house"];
|
|
304
|
+
export const WORKING_COPY_RECEIVED_REF = "refs/house/received";
|
|
305
|
+
export const WORKING_COPY_BRANCH = "main";
|
|
306
|
+
export const ROOM_REFERENCE = /^r_[0-9a-z]+\.[0-9a-z]$/;
|
|
307
|
+
export const WORKING_COPY_PUSH_METHOD = "house/git/push";
|
|
308
|
+
export const WORKING_COPY_INDEX_METHOD = "house/git/index";
|
|
309
|
+
export const WORKING_COPY_PUSH_HELP =
|
|
310
|
+
"Submit a Room commit to House; default HEAD. Use --owner only outside an Agent Session. Native git push is not a House remote.";
|
|
311
|
+
|
|
294
312
|
export const MIGRATOR_CONTRACT = "kit-migrator/1";
|
|
295
313
|
export const MIGRATOR_CATEGORY = "migrator";
|
|
296
314
|
export const MIGRATOR_MANIFEST = "migrator.json";
|
package/src/git-main.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { constants as system } from "node:os";
|
|
4
|
+
import { delimiter, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
BRIDGE_NONCE_ENVIRONMENT,
|
|
8
|
+
BRIDGE_SOCKET_ENVIRONMENT,
|
|
9
|
+
WORKING_COPY_INDEX_METHOD,
|
|
10
|
+
WORKING_COPY_RECEIVED_REF,
|
|
11
|
+
} from "./contract.js";
|
|
12
|
+
import { bridgeCall } from "./mcp.js";
|
|
13
|
+
import { formatCommandError, KitRefusal } from "./refusal.js";
|
|
14
|
+
|
|
15
|
+
const SHIM = realpathSync(fileURLToPath(new URL("session-bin/git", import.meta.url)));
|
|
16
|
+
const VALUED = new Set(["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--config-env", "--attr-source", "--super-prefix"]);
|
|
17
|
+
const RELAYED = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
|
|
18
|
+
|
|
19
|
+
function nativeGit() {
|
|
20
|
+
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
21
|
+
if (directory === "") continue;
|
|
22
|
+
const candidate = join(directory, "git");
|
|
23
|
+
try {
|
|
24
|
+
if (!statSync(candidate).isFile() || realpathSync(candidate) === SHIM) continue;
|
|
25
|
+
accessSync(candidate, constants.X_OK);
|
|
26
|
+
return candidate;
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function command(args) {
|
|
33
|
+
let index = 0;
|
|
34
|
+
while (index < args.length && args[index].startsWith("-")) index += VALUED.has(args[index]) ? 2 : 1;
|
|
35
|
+
return { globals: args.slice(0, index), name: args[index], rest: args.slice(index + 1) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function delegated(git, args) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const child = spawn(git, args, { stdio: "inherit" });
|
|
41
|
+
const relay = (signal) => child.kill(signal);
|
|
42
|
+
for (const signal of RELAYED) process.on(signal, relay);
|
|
43
|
+
const settle = (outcome) => {
|
|
44
|
+
for (const signal of RELAYED) process.off(signal, relay);
|
|
45
|
+
resolve(outcome);
|
|
46
|
+
};
|
|
47
|
+
child.once("error", (error) => {
|
|
48
|
+
process.stderr.write(`kit_git_unavailable: ${error.message}\n`);
|
|
49
|
+
settle({ status: 127, signal: null });
|
|
50
|
+
});
|
|
51
|
+
child.once("close", (status, signal) => settle({ status, signal }));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function captured(git, args, env = process.env) {
|
|
56
|
+
const run = spawnSync(git, args, { stdio: ["ignore", "pipe", "ignore"], maxBuffer: Infinity, env });
|
|
57
|
+
return run.status === 0 ? run.stdout.toString("utf8") : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function indexed(listing) {
|
|
61
|
+
const entries = new Map();
|
|
62
|
+
for (const record of listing.split("\0")) {
|
|
63
|
+
const tab = record.indexOf("\t");
|
|
64
|
+
if (tab < 0) continue;
|
|
65
|
+
const [mode, object, stage] = record.slice(0, tab).split(" ");
|
|
66
|
+
entries.set(record.slice(tab + 1), stage === "0" ? `${mode} ${object}` : null);
|
|
67
|
+
}
|
|
68
|
+
return entries;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renamings(output) {
|
|
72
|
+
return (output ?? "").split("\n").filter((line) => line.startsWith("Renaming ")).map((line) => line.slice(9));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function movedPaths(renaming, before, after) {
|
|
76
|
+
for (let at = renaming.indexOf(" to "); at >= 0; at = renaming.indexOf(" to ", at + 1)) {
|
|
77
|
+
const from = renaming.slice(0, at);
|
|
78
|
+
const to = renaming.slice(at + 4);
|
|
79
|
+
if (before.has(from) && !after.has(from) && after.get(to) === before.get(from)) return [{ from, to }];
|
|
80
|
+
}
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function recorded(cwd, change) {
|
|
85
|
+
const socket = process.env[BRIDGE_SOCKET_ENVIRONMENT];
|
|
86
|
+
const nonce = process.env[BRIDGE_NONCE_ENVIRONMENT];
|
|
87
|
+
if (socket === undefined || nonce === undefined) return;
|
|
88
|
+
try {
|
|
89
|
+
const frame = await bridgeCall({ socket, nonce }, { method: WORKING_COPY_INDEX_METHOD, params: { cwd, ...change } });
|
|
90
|
+
if (frame.error !== undefined) throw new KitRefusal(frame.error.message ?? "kit_house_refused");
|
|
91
|
+
} catch (refusal) {
|
|
92
|
+
process.stderr.write(`${formatCommandError(refusal)}: house git push will not see this index change\n`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function observed(git, { globals, name, rest }, args) {
|
|
97
|
+
const located = captured(git, [...globals, "rev-parse", "--show-toplevel", "--show-cdup", WORKING_COPY_RECEIVED_REF, "HEAD"]);
|
|
98
|
+
if (located === null) return delegated(git, args);
|
|
99
|
+
const [top, cdup, , head] = located.split("\n");
|
|
100
|
+
const listing = [...globals, "ls-files", "--stage", "-z", "--full-name", "--", cdup === "" ? "." : cdup];
|
|
101
|
+
const before = captured(git, listing);
|
|
102
|
+
const rehearsal = [...globals, "mv", "--dry-run", ...rest];
|
|
103
|
+
const planned = name === "mv" ? renamings(captured(git, rehearsal, { ...process.env, LC_ALL: "C" })) : [];
|
|
104
|
+
const outcome = await delegated(git, args);
|
|
105
|
+
const after = before === null ? null : captured(git, listing);
|
|
106
|
+
if (after === null) return outcome;
|
|
107
|
+
const [prior, next] = [indexed(before), indexed(after)];
|
|
108
|
+
const moves = outcome.status === 0 ? planned.flatMap((renaming) => movedPaths(renaming, prior, next)) : [];
|
|
109
|
+
const sources = new Set(moves.map((move) => move.from));
|
|
110
|
+
const destinations = new Set(moves.map((move) => move.to));
|
|
111
|
+
const gone = [...prior.keys()].filter((path) => !next.has(path) && !sources.has(path));
|
|
112
|
+
const arrived = [...next.keys()].filter((path) => !prior.has(path) && !destinations.has(path));
|
|
113
|
+
const heads = captured(git, [...globals, "rev-parse", "HEAD", ...(name === "rebase" ? ["ORIG_HEAD"] : [])]);
|
|
114
|
+
const [current, tip = head] = heads?.trim().split("\n") ?? [];
|
|
115
|
+
const commit = current === undefined || current === head ? [] : [tip, current];
|
|
116
|
+
if (moves.length > 0 || gone.length > 0 || arrived.length > 0 || commit.length > 0) {
|
|
117
|
+
await recorded(top, { moves, gone, arrived, commit, command: name });
|
|
118
|
+
}
|
|
119
|
+
return outcome;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function run(args) {
|
|
123
|
+
const git = nativeGit();
|
|
124
|
+
if (git === null) {
|
|
125
|
+
process.stderr.write("kit_git_unavailable\n");
|
|
126
|
+
return { status: 127, signal: null };
|
|
127
|
+
}
|
|
128
|
+
return observed(git, command(args), args);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
run(process.argv.slice(2)).then(({ status, signal }) => {
|
|
132
|
+
if (signal === null) {
|
|
133
|
+
process.exitCode = status;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
process.exitCode = 128 + system.signals[signal];
|
|
137
|
+
process.kill(process.pid, signal);
|
|
138
|
+
});
|
package/src/git.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { KitRefusal } from "./refusal.js";
|
|
3
|
+
|
|
4
|
+
function cancelledOr(signal, error, code) {
|
|
5
|
+
return signal?.aborted === true || error?.name === "AbortError"
|
|
6
|
+
? new KitRefusal("kit_command_cancelled")
|
|
7
|
+
: new KitRefusal(code);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function stdio(capture, input) {
|
|
11
|
+
if (input !== null) return ["pipe", capture ? "pipe" : "inherit", "inherit"];
|
|
12
|
+
return capture ? ["ignore", "pipe", "inherit"] : "inherit";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function runGit(cwd, args, options = {}) {
|
|
16
|
+
const { dependencies = {}, capture = false, input = null, signal, failure } = options;
|
|
17
|
+
if (signal?.aborted === true) return Promise.reject(new KitRefusal("kit_command_cancelled"));
|
|
18
|
+
const launcher = dependencies.spawn ?? spawn;
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let child;
|
|
21
|
+
try {
|
|
22
|
+
child = launcher("git", args, {
|
|
23
|
+
cwd,
|
|
24
|
+
env: process.env,
|
|
25
|
+
stdio: stdio(capture, input),
|
|
26
|
+
signal,
|
|
27
|
+
killSignal: "SIGKILL",
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
reject(cancelledOr(signal, error, "kit_git_unavailable"));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const output = [];
|
|
34
|
+
let settled = false;
|
|
35
|
+
const finish = (operation) => {
|
|
36
|
+
if (settled) return;
|
|
37
|
+
settled = true;
|
|
38
|
+
operation();
|
|
39
|
+
};
|
|
40
|
+
child.stdout?.on("data", (chunk) => output.push(Buffer.from(chunk)));
|
|
41
|
+
child.once("error", (error) =>
|
|
42
|
+
finish(() => reject(cancelledOr(signal, error, "kit_git_unavailable"))),
|
|
43
|
+
);
|
|
44
|
+
child.once("close", (status) => {
|
|
45
|
+
finish(() => {
|
|
46
|
+
if (signal?.aborted === true) {
|
|
47
|
+
reject(new KitRefusal("kit_command_cancelled"));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (status !== 0) {
|
|
51
|
+
reject(new KitRefusal(failure));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
resolve(Buffer.concat(output));
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
if (input !== null) {
|
|
58
|
+
child.stdin.on("error", () => undefined);
|
|
59
|
+
child.stdin.end(input);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
package/src/host.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, statfs } from "node:fs/promises";
|
|
2
2
|
import { readConfiguration } from "./configuration.js";
|
|
3
3
|
import { HOST_CPU_SAMPLE_MS } from "./contract.js";
|
|
4
|
-
import { openSession, reportHostCapacity } from "./house.js";
|
|
4
|
+
import { maintainControl, openSession, reportHostCapacity } from "./house.js";
|
|
5
5
|
import { KitRefusal } from "./refusal.js";
|
|
6
6
|
|
|
7
7
|
const PROC_STAT = "/proc/stat";
|
|
@@ -116,6 +116,10 @@ export async function reportHost(options, dependencies = {}) {
|
|
|
116
116
|
? observed
|
|
117
117
|
: { ...observed, maxima: declaredMaxima(configuration.host.maxima) };
|
|
118
118
|
const session = await openSession(options, dependencies);
|
|
119
|
-
await
|
|
119
|
+
await maintainControl(
|
|
120
|
+
session,
|
|
121
|
+
(signal) => reportHostCapacity(session, declaration, signal),
|
|
122
|
+
options.signal,
|
|
123
|
+
);
|
|
120
124
|
return declaration;
|
|
121
125
|
}
|
package/src/house-main.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
BRIDGE_NONCE_ENVIRONMENT,
|
|
5
|
+
BRIDGE_SOCKET_ENVIRONMENT,
|
|
6
|
+
WORKING_COPY_PUSH_HELP,
|
|
7
|
+
WORKING_COPY_PUSH_METHOD,
|
|
8
|
+
} from "./contract.js";
|
|
9
|
+
import { storedHouse } from "./login.js";
|
|
10
|
+
import { answerText, bridgeCall } from "./mcp.js";
|
|
11
|
+
import { formatCommandError, KitRefusal } from "./refusal.js";
|
|
12
|
+
import { ownerPush } from "./working-copy.js";
|
|
5
13
|
|
|
6
14
|
const HELP = ["--help", "-h"];
|
|
15
|
+
const OWNER = "--owner";
|
|
16
|
+
const GIT_PUSH_USAGE = `usage: house git push [commit] [${OWNER}]`;
|
|
7
17
|
const usage = (verb) => `usage: house ${verb} ['<arguments as one JSON object>' | - for standard input]`;
|
|
8
18
|
|
|
9
19
|
async function answered(method, params) {
|
|
@@ -49,10 +59,59 @@ function located(verb, args) {
|
|
|
49
59
|
return args;
|
|
50
60
|
}
|
|
51
61
|
|
|
62
|
+
function printed(result) {
|
|
63
|
+
const text = answerText(result);
|
|
64
|
+
return result?.isError === true ? { err: text, status: 1 } : { out: text };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function ownerPushed(request) {
|
|
68
|
+
const directory = process.env.HOUSE_KIT_HOME ?? join(homedir(), ".house-kit");
|
|
69
|
+
const cancellation = new AbortController();
|
|
70
|
+
const cancel = () => cancellation.abort();
|
|
71
|
+
process.once("SIGINT", cancel);
|
|
72
|
+
process.once("SIGTERM", cancel);
|
|
73
|
+
try {
|
|
74
|
+
const pushed = await ownerPush(
|
|
75
|
+
{ house: await storedHouse(directory), directory, signal: cancellation.signal },
|
|
76
|
+
request,
|
|
77
|
+
);
|
|
78
|
+
return pushed.refused ? { err: pushed.text, status: 1 } : { out: pushed.text };
|
|
79
|
+
} catch (refusal) {
|
|
80
|
+
return { err: formatCommandError(refusal), status: 1 };
|
|
81
|
+
} finally {
|
|
82
|
+
process.removeListener("SIGINT", cancel);
|
|
83
|
+
process.removeListener("SIGTERM", cancel);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function gitPush([action, ...rest]) {
|
|
88
|
+
if (action !== "push") throw new KitRefusal("kit_house_arguments_invalid", GIT_PUSH_USAGE);
|
|
89
|
+
if (rest.some((argument) => HELP.includes(argument))) return { out: `${GIT_PUSH_USAGE}\n\n${WORKING_COPY_PUSH_HELP}` };
|
|
90
|
+
const owner = rest.includes(OWNER);
|
|
91
|
+
const named = rest.filter((argument) => argument !== OWNER);
|
|
92
|
+
if (named.length > 1 || named.some((argument) => argument.startsWith("-"))) {
|
|
93
|
+
throw new KitRefusal("kit_house_arguments_invalid", GIT_PUSH_USAGE);
|
|
94
|
+
}
|
|
95
|
+
const request = { cwd: process.cwd(), commit: named[0] ?? "HEAD" };
|
|
96
|
+
if (process.env[BRIDGE_SOCKET_ENVIRONMENT] === undefined) {
|
|
97
|
+
if (!owner) throw new KitRefusal("kit_bridge_unavailable");
|
|
98
|
+
return ownerPushed(request);
|
|
99
|
+
}
|
|
100
|
+
if (owner) throw new KitRefusal("kit_house_arguments_invalid", `${OWNER} is only for use outside an Agent Session`);
|
|
101
|
+
return printed(await answered(WORKING_COPY_PUSH_METHOD, request));
|
|
102
|
+
}
|
|
103
|
+
|
|
52
104
|
async function house([verb, argument, ...rest]) {
|
|
105
|
+
if (verb === "git") return gitPush([argument, ...rest].filter((part) => part !== undefined));
|
|
53
106
|
if (verb === undefined || HELP.includes(verb)) {
|
|
54
107
|
const tools = await listed();
|
|
55
|
-
return {
|
|
108
|
+
return {
|
|
109
|
+
out: [
|
|
110
|
+
usage("<verb>"),
|
|
111
|
+
...tools.map((tool) => `${tool.name}\n${tool.description}`),
|
|
112
|
+
`git push [commit]\n${WORKING_COPY_PUSH_HELP}`,
|
|
113
|
+
].join("\n\n"),
|
|
114
|
+
};
|
|
56
115
|
}
|
|
57
116
|
if (HELP.includes(argument)) {
|
|
58
117
|
const tool = (await listed()).find((one) => one.name === verb);
|
|
@@ -61,9 +120,7 @@ async function house([verb, argument, ...rest]) {
|
|
|
61
120
|
}
|
|
62
121
|
if (rest.length > 0) throw new KitRefusal("kit_house_arguments_invalid", usage(verb));
|
|
63
122
|
const args = located(verb, await argumentsOf(verb, argument));
|
|
64
|
-
|
|
65
|
-
const text = (result?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
66
|
-
return result?.isError === true ? { err: text, status: 1 } : { out: text };
|
|
123
|
+
return printed(await answered("tools/call", { name: verb, arguments: args }));
|
|
67
124
|
}
|
|
68
125
|
|
|
69
126
|
house(process.argv.slice(2)).then(
|