@agentshouse/kit 0.0.1 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1696 -2
  3. package/bin/connect-linux.sh +127 -0
  4. package/bin/kit.js +1023 -0
  5. package/package.json +33 -3
  6. package/src/acp.js +99 -0
  7. package/src/adapter-mcp.js +44 -0
  8. package/src/adapter.js +252 -0
  9. package/src/agent-logins.js +65 -0
  10. package/src/agents.js +449 -0
  11. package/src/apply.js +29 -0
  12. package/src/attach.js +52 -0
  13. package/src/binding.js +90 -0
  14. package/src/blocks.js +156 -0
  15. package/src/bridge.js +449 -0
  16. package/src/bytes.js +89 -0
  17. package/src/channel.js +37 -0
  18. package/src/command.js +109 -0
  19. package/src/configuration.js +465 -0
  20. package/src/connector-login.js +91 -0
  21. package/src/connector.js +107 -0
  22. package/src/continuation.js +61 -0
  23. package/src/contract.js +363 -0
  24. package/src/conversation-originals.js +143 -0
  25. package/src/custom/authorization.js +63 -0
  26. package/src/declaration.js +35 -0
  27. package/src/delivery.js +191 -0
  28. package/src/directory.js +102 -0
  29. package/src/draft.js +89 -0
  30. package/src/google/calendar.js +177 -0
  31. package/src/google/declared.js +146 -0
  32. package/src/google/drive.js +435 -0
  33. package/src/google/gateway.js +166 -0
  34. package/src/google/gmail.js +249 -0
  35. package/src/google/id.js +1 -0
  36. package/src/google/oauth.js +273 -0
  37. package/src/google/source.js +25 -0
  38. package/src/hook.js +376 -0
  39. package/src/host.js +121 -0
  40. package/src/house-main.js +79 -0
  41. package/src/house.js +1085 -0
  42. package/src/inbound.js +175 -0
  43. package/src/instance/google.js +1 -0
  44. package/src/launcher-main.js +19 -0
  45. package/src/launcher.js +276 -0
  46. package/src/local-files.js +113 -0
  47. package/src/login.js +274 -0
  48. package/src/mcp.js +71 -0
  49. package/src/migrator.js +248 -0
  50. package/src/mount.js +582 -0
  51. package/src/operation.js +20 -0
  52. package/src/original.js +631 -0
  53. package/src/peer.js +124 -0
  54. package/src/plan.js +57 -0
  55. package/src/question.js +316 -0
  56. package/src/refusal.js +79 -0
  57. package/src/repository.js +521 -0
  58. package/src/session-bin/house +2 -0
  59. package/src/settlement.js +57 -0
  60. package/src/setup.js +185 -0
  61. package/src/source-originals.js +116 -0
  62. package/src/source.js +286 -0
  63. package/src/starter-set/grilling/LICENSE +21 -0
  64. package/src/starter-set/grilling/SKILL.md +30 -0
  65. package/src/starter-set/grilling/agents/openai.yaml +3 -0
  66. package/src/starter-set.js +17 -0
  67. package/src/state.js +157 -0
  68. package/src/sync-library.js +293 -0
  69. package/src/sync-state.js +116 -0
  70. package/src/sync.js +477 -0
  71. package/src/telegram.js +1045 -0
  72. package/src/terminal.js +57 -0
  73. package/src/tmux.js +119 -0
  74. package/src/worker.js +756 -0
  75. package/src/workspace.js +500 -0
package/src/inbound.js ADDED
@@ -0,0 +1,175 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import {
6
+ holdConversationFile,
7
+ retainConversationFile,
8
+ retryConversationFiles,
9
+ settleConversationFile,
10
+ } from "./conversation-originals.js";
11
+ import { heldControl, houseRequest } from "./house.js";
12
+ import { KIT_CONTRACT_VERSION } from "./contract.js";
13
+ import { KitRefusal } from "./refusal.js";
14
+
15
+ const ROOT = "conversation-inbound";
16
+ const MAX_BYTES = 32 * 1024 * 1024;
17
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
18
+ const MEDIA = new Set(["document", "photo", "audio", "video", "voice", "video_note", "sticker", "animation"]);
19
+
20
+ function segment(value) {
21
+ return typeof value === "string" && value !== "" && value !== "." && value !== ".." &&
22
+ Buffer.byteLength(value) <= 255 && !/[\\/\0]/.test(value);
23
+ }
24
+
25
+ async function cacheDirectory(directory, parts, create) {
26
+ let path = await realpath(directory);
27
+ for (const part of [ROOT, ...parts]) {
28
+ path = join(path, part);
29
+ if (create) await mkdir(path, { mode: 0o700 }).catch((error) => {
30
+ if (error.code !== "EEXIST") throw error;
31
+ });
32
+ const state = await lstat(path).catch((error) => {
33
+ if (error.code === "ENOENT" && !create) return null;
34
+ throw error;
35
+ });
36
+ if (state === null) return null;
37
+ if (!state.isDirectory() || state.isSymbolicLink()) throw new KitRefusal("kit_inbound_path_invalid");
38
+ }
39
+ return path;
40
+ }
41
+
42
+ function request(session, path, body = {}) {
43
+ return heldControl(session, session.signal).then((authorization) =>
44
+ houseRequest(session.house, authorization, `/kit/telegram/${path}`, body, session.dependencies, session.signal));
45
+ }
46
+
47
+ async function download(session, file) {
48
+ const signal = AbortSignal.any([AbortSignal.timeout(30_000), ...(session.signal ? [session.signal] : [])]);
49
+ const response = await (session.dependencies.fetch ?? fetch)(`${session.house}/kit/telegram/inbound/${file.id}/stream`, {
50
+ method: "POST", redirect: "error", signal,
51
+ headers: { authorization: await heldControl(session, signal), "content-type": "application/json", "x-house-kit-version": KIT_CONTRACT_VERSION },
52
+ body: "{}",
53
+ });
54
+ if (!response.ok || response.body === null) {
55
+ await response.body?.cancel();
56
+ throw new KitRefusal("kit_inbound_unavailable", file.id);
57
+ }
58
+ const reader = response.body.getReader();
59
+ const chunks = [];
60
+ let size = 0;
61
+ try {
62
+ for (;;) {
63
+ const chunk = await reader.read();
64
+ if (chunk.done) break;
65
+ size += chunk.value.byteLength;
66
+ if (size > MAX_BYTES || (file.byte_size !== null && size > file.byte_size)) {
67
+ throw new KitRefusal("kit_inbound_size_mismatch", file.id);
68
+ }
69
+ chunks.push(chunk.value);
70
+ }
71
+ } finally {
72
+ await reader.cancel().catch(() => {});
73
+ reader.releaseLock();
74
+ }
75
+ if (file.byte_size !== null && size !== file.byte_size) throw new KitRefusal("kit_inbound_size_mismatch", file.id);
76
+ return Buffer.concat(chunks, size);
77
+ }
78
+
79
+ async function materialize(directory, session, file, outcome) {
80
+ const retention = { directory, session, signal: session.signal };
81
+ const parent = await cacheDirectory(directory, [file.run, file.id], true);
82
+ const path = join(parent, file.cache_name);
83
+ const incoming = { attachment: file.id, run: file.run, path };
84
+ let bytes;
85
+ try {
86
+ bytes = await download(session, file);
87
+ } catch (error) {
88
+ if (!session.signal?.aborted) outcome.originals.push(await retainConversationFile(retention, incoming, null));
89
+ throw error;
90
+ }
91
+ const temporary = join(parent, `${randomUUID()}.partial`);
92
+ try {
93
+ const opened = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
94
+ try { await opened.writeFile(bytes); } finally { await opened.close(); }
95
+ await rename(temporary, path);
96
+ } catch (error) {
97
+ await rm(temporary, { force: true });
98
+ outcome.originals.push(await retainConversationFile(retention, incoming, bytes));
99
+ throw error;
100
+ }
101
+ const { held, body } = await holdConversationFile(retention, incoming, bytes);
102
+ await request(session, `inbound/${file.id}/materialized`);
103
+ outcome.originals.push(await settleConversationFile(retention, held, body));
104
+ return path;
105
+ }
106
+
107
+ export async function reconcileInbound(directory, session) {
108
+ const outcome = { materialized: [], failures: [], originals: [] };
109
+ const attempted = new Set();
110
+ for (;;) {
111
+ const pending = await request(session, "inbound/acquire", { max: 10 });
112
+ if (!Array.isArray(pending?.files) || pending.files.length > 10) throw new KitRefusal("kit_inbound_response_invalid");
113
+ let progressed = false;
114
+ for (const file of pending.files) {
115
+ if (!UUID.test(file?.run) || !UUID.test(file?.id) || !segment(file?.cache_name) ||
116
+ !MEDIA.has(file?.media_kind) || (file.byte_size !== null && (!Number.isSafeInteger(file.byte_size) || file.byte_size < 0 || file.byte_size > MAX_BYTES))) {
117
+ throw new KitRefusal("kit_inbound_response_invalid");
118
+ }
119
+ if (attempted.has(file.id)) continue;
120
+ attempted.add(file.id);
121
+ try {
122
+ outcome.materialized.push(await materialize(directory, session, file, outcome));
123
+ progressed = true;
124
+ } catch (error) {
125
+ if (session.signal?.aborted) throw error;
126
+ outcome.failures.push({ id: file.id, run: file.run, code: error instanceof KitRefusal ? error.code : "kit_inbound_unavailable" });
127
+ }
128
+ }
129
+ if (pending.files.length < 10 || !progressed) break;
130
+ }
131
+ const retained = new Set(outcome.originals.map((original) => original.attachment));
132
+ outcome.originals.push(...(await retryConversationFiles({ directory, session, signal: session.signal }, retained)));
133
+ return outcome;
134
+ }
135
+
136
+ export async function localRequest(directory, run, request) {
137
+ const turn = request?.conversation?.turn ?? (request?.kind === "conversation" ? request.turn : null);
138
+ if (turn === null || turn === undefined) return request;
139
+ const result = structuredClone(request);
140
+ const localTurn = result.conversation?.turn ?? result.turn;
141
+ if (!Array.isArray(localTurn.messages)) throw new KitRefusal("kit_run_input_unreadable");
142
+ for (const message of localTurn.messages) {
143
+ if (!Array.isArray(message.files)) throw new KitRefusal("kit_run_input_unreadable");
144
+ for (const file of message.files) {
145
+ const parts = typeof file.path === "string" ? file.path.split("/") : [];
146
+ if (parts.length !== 4 || parts[0] !== ROOT || parts[1] !== run || !UUID.test(parts[1]) || !UUID.test(parts[2]) || !segment(parts[3])) {
147
+ throw new KitRefusal("kit_inbound_path_invalid");
148
+ }
149
+ const parent = await cacheDirectory(directory, [run, parts[2]], false);
150
+ if (parent === null) throw new KitRefusal("kit_inbound_unavailable");
151
+ const path = join(parent, parts[3]);
152
+ const opened = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW).catch(() => {
153
+ throw new KitRefusal("kit_inbound_unavailable");
154
+ });
155
+ try {
156
+ const state = await opened.stat();
157
+ if (!state.isFile() || state.size > MAX_BYTES || (file.bytes !== null && state.size !== file.bytes)) throw new KitRefusal("kit_inbound_size_mismatch");
158
+ } finally { await opened.close(); }
159
+ file.path = path;
160
+ }
161
+ }
162
+ return result;
163
+ }
164
+
165
+ export async function localReply(directory, run, frame, reply) {
166
+ if (frame.method !== "tools/call" || frame.params?.name !== "session_get_request" || reply.error || reply.result?.isError) return reply;
167
+ const result = structuredClone(reply);
168
+ for (const item of result.result?.content ?? []) {
169
+ if (item.type !== "text") continue;
170
+ let input;
171
+ try { input = JSON.parse(item.text); } catch { throw new KitRefusal("kit_run_input_unreadable"); }
172
+ item.text = JSON.stringify(await localRequest(directory, run, input));
173
+ }
174
+ return result;
175
+ }
@@ -0,0 +1 @@
1
+ export const GOOGLE_OAUTH_CLIENT = Object.freeze({"id":"911965338518-867gdvuddb7kdkjo3qvpv6gds369alb7.apps.googleusercontent.com","secret":"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"});
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { launchWorkerSession } from "./launcher.js";
3
+ import { BRIDGE_NONCE_ENVIRONMENT, BRIDGE_SOCKET_ENVIRONMENT } from "./contract.js";
4
+
5
+ const socket = process.env[BRIDGE_SOCKET_ENVIRONMENT];
6
+ const nonce = process.env[BRIDGE_NONCE_ENVIRONMENT];
7
+
8
+ if (socket === undefined || nonce === undefined) {
9
+ process.stderr.write("kit_bridge_unavailable\n");
10
+ process.exit(1);
11
+ }
12
+
13
+ launchWorkerSession({ socket, nonce }).then(
14
+ () => process.exit(0),
15
+ (refusal) => {
16
+ process.stderr.write(`${refusal instanceof Error ? refusal.message : "kit_launcher_failed"}\n`);
17
+ process.exit(1);
18
+ },
19
+ );
@@ -0,0 +1,276 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { commandHouseInstructions, commandHouseRequest } from "./adapter-mcp.js";
3
+ import {
4
+ compiledContract,
5
+ initializeCommandAdapter,
6
+ openedSession,
7
+ startCommandAdapter,
8
+ stopCommandAdapter,
9
+ } from "./adapter.js";
10
+ import { PERMISSION_OPTION_KINDS, SESSION_SET_CONFIG_OPTION } from "./contract.js";
11
+ import { observedAssistantDrafts } from "./draft.js";
12
+ import { observedPlans } from "./plan.js";
13
+ import { bridgePeer } from "./peer.js";
14
+ import {
15
+ elicitationResponse,
16
+ elicitedForm,
17
+ elicitedVisit,
18
+ grokForm,
19
+ grokResponse,
20
+ } from "./question.js";
21
+ import { KitRefusal } from "./refusal.js";
22
+
23
+ export function resolvedPermission(request, authority) {
24
+ const wanted = PERMISSION_OPTION_KINDS[authority];
25
+ if (wanted === undefined) return { outcome: { outcome: "cancelled" } };
26
+ const options = Array.isArray(request?.options) ? request.options : [];
27
+ const selected = options.find(
28
+ (option) =>
29
+ option?.kind === wanted &&
30
+ typeof option?.optionId === "string" &&
31
+ option.optionId.length > 0,
32
+ );
33
+ if (selected === undefined) return { outcome: { outcome: "cancelled" } };
34
+ return { outcome: { outcome: "selected", optionId: selected.optionId } };
35
+ }
36
+
37
+ function queuedOwner(channel, turn, frame, elicited, answer, cancel) {
38
+ const correlation = `${turn.providerTurn}:${frame.id}`;
39
+ if (elicited === null) return { action: "cancel" };
40
+ return new Promise((resolve) => {
41
+ turn.questions.set(correlation, { correlation, answer, cancel, resolve });
42
+ channel.send({
43
+ type: "question",
44
+ delivery: correlation,
45
+ form: elicited.form,
46
+ ...(elicited.ceremony === undefined ? {} : { ceremony: elicited.ceremony }),
47
+ });
48
+ });
49
+ }
50
+
51
+ export function askedOwner(channel, turn, frame) {
52
+ const correlation = `${turn.providerTurn}:${frame.id}`;
53
+ const bound = { correlation, providerTurn: turn.providerTurn };
54
+ const elicited = elicitedForm(frame.params, bound) ?? elicitedVisit(frame.params, bound);
55
+ return queuedOwner(
56
+ channel,
57
+ turn,
58
+ frame,
59
+ elicited,
60
+ (response) => elicitationResponse(elicited?.convention ?? {}, response),
61
+ () => ({ action: "cancel" }),
62
+ );
63
+ }
64
+
65
+ export function askedGrokOwner(channel, turn, frame) {
66
+ const correlation = `${turn.providerTurn}:${frame.id}`;
67
+ const elicited = grokForm(frame.params, {
68
+ correlation,
69
+ providerTurn: turn.providerTurn,
70
+ });
71
+ if (elicited === null) return { outcome: "cancelled" };
72
+ return queuedOwner(
73
+ channel,
74
+ turn,
75
+ frame,
76
+ elicited,
77
+ (response) => grokResponse(elicited?.convention ?? {}, response),
78
+ () => ({ outcome: "cancelled" }),
79
+ );
80
+ }
81
+
82
+ export function answeredOwner(channel, turn, frame) {
83
+ const pending = turn.questions.get(frame.delivery);
84
+ if (pending === undefined) return;
85
+ if (
86
+ frame.unsupported !== true &&
87
+ (frame.provider_turn !== turn.providerTurn || frame.correlation !== pending.correlation)
88
+ ) {
89
+ return;
90
+ }
91
+ turn.questions.delete(frame.delivery);
92
+ pending.resolve(frame.unsupported === true ? pending.cancel() : pending.answer(frame.response));
93
+ channel.send({ type: "question_applied", delivery: frame.delivery, outcome: "resolved" });
94
+ }
95
+
96
+ function abandonQuestions(channel, turn) {
97
+ for (const [delivery, pending] of turn.questions) {
98
+ turn.questions.delete(delivery);
99
+ pending.resolve(pending.cancel());
100
+ channel.send({ type: "question_applied", delivery, outcome: "expired" });
101
+ }
102
+ }
103
+
104
+ function turnOutcome(contract, stopReason) {
105
+ const named =
106
+ typeof stopReason === "string" &&
107
+ Object.hasOwn(contract.capabilities.stop_reasons, stopReason);
108
+ if (!named) {
109
+ throw new KitRefusal(
110
+ "kit_adapter_contract_mismatch",
111
+ `session/prompt stop reason ${JSON.stringify(stopReason ?? null)}`,
112
+ );
113
+ }
114
+ return contract.capabilities.stop_reasons[stopReason];
115
+ }
116
+
117
+ async function configureSession(acp, contract, sessionId, adapter) {
118
+ for (const field of ["model", "effort"]) {
119
+ const value = adapter[field];
120
+ if (value === null) continue;
121
+ const configId = contract.sessionConfig[field];
122
+ if (configId === null) {
123
+ throw new KitRefusal(
124
+ "kit_adapter_failed",
125
+ `${contract.kind} contract ${contract.version} offers no ${field} option`,
126
+ );
127
+ }
128
+ await acp.request(SESSION_SET_CONFIG_OPTION, { sessionId, configId, value });
129
+ }
130
+ }
131
+
132
+ async function runTurn(channel, start, bridge, turn) {
133
+ const contract = compiledContract(start.adapter?.kind);
134
+ const adapter = startCommandAdapter(
135
+ { ...start.adapter, workingDirectory: start.working_directory },
136
+ bridge,
137
+ );
138
+ const said = [];
139
+ const message = { text: "" };
140
+ const drafts = observedAssistantDrafts({ send: (text) => channel.send({ type: "draft", text }) });
141
+ const plans = observedPlans({
142
+ send: (snapshot) =>
143
+ channel.send({ type: "plan", provider_turn: turn.providerTurn, ...snapshot }),
144
+ });
145
+ const wholeSoFar = () => [...said, message.text].filter((text) => text !== "").join("\n\n");
146
+ let loading = false;
147
+ const flush = () => {
148
+ if (message.text === "") return;
149
+ channel.send({ type: "assistant", text: message.text });
150
+ said.push(message.text);
151
+ message.text = "";
152
+ drafts.complete(wholeSoFar());
153
+ };
154
+ const handlers = {
155
+ onPermission: (params) => resolvedPermission(params, start.execution_authority),
156
+ onElicitation: (request) => askedOwner(channel, turn, request),
157
+ onNativeQuestion: (request) => askedGrokOwner(channel, turn, request),
158
+ onUpdate: (params) => {
159
+ if (loading) return;
160
+ const update = params?.update;
161
+ if (update?.sessionUpdate === "agent_message_chunk") {
162
+ if (update.content?.type === "text" && typeof update.content.text === "string") {
163
+ message.text += update.content.text;
164
+ drafts.observe(wholeSoFar());
165
+ }
166
+ return;
167
+ }
168
+ if (update?.sessionUpdate === "plan") plans.observe(update.entries);
169
+ flush();
170
+ },
171
+ };
172
+ try {
173
+ const { acp } = await initializeCommandAdapter(adapter, contract, handlers);
174
+ const instructions = await commandHouseInstructions(bridge);
175
+ let providerSessionId = start.provider_session ?? null;
176
+ const setup = { cwd: start.working_directory, mcpServers: [] };
177
+ if (providerSessionId === null) {
178
+ providerSessionId = openedSession(await acp.request("session/new", setup));
179
+ if (providerSessionId === null) {
180
+ throw new KitRefusal("kit_adapter_contract_mismatch", "session/new");
181
+ }
182
+ } else {
183
+ loading = true;
184
+ await acp.request("session/load", { ...setup, sessionId: providerSessionId });
185
+ loading = false;
186
+ }
187
+ await configureSession(acp, contract, providerSessionId, start.adapter);
188
+ const request = start.request === null ? await commandHouseRequest(bridge) : start.request;
189
+ turn.adapterSession = true;
190
+ turn.cancel = () => acp.cancel({ sessionId: providerSessionId });
191
+ const prompted = await acp.request("session/prompt", {
192
+ sessionId: providerSessionId,
193
+ prompt: [
194
+ { type: "text", text: JSON.stringify(request) },
195
+ { type: "text", text: instructions },
196
+ ],
197
+ });
198
+ await new Promise((resolve) => setImmediate(resolve));
199
+ flush();
200
+ plans.complete();
201
+ channel.send({
202
+ type: "turn_ended",
203
+ stop_outcome: turnOutcome(contract, prompted?.stopReason),
204
+ provider_continuation: contract.sessionLoad,
205
+ provider_session_id: providerSessionId,
206
+ });
207
+ } finally {
208
+ turn.cancel = null;
209
+ plans.complete();
210
+ abandonQuestions(channel, turn);
211
+ drafts.close();
212
+ stopCommandAdapter(adapter);
213
+ }
214
+ }
215
+
216
+ export async function launchWorkerSession(bridge) {
217
+ const turn = {
218
+ adapterSession: false,
219
+ providerTurn: randomUUID(),
220
+ questions: new Map(),
221
+ cancel: null,
222
+ };
223
+ const peer = bridgePeer({
224
+ socket: bridge.socket,
225
+ nonce: bridge.nonce,
226
+ role: "launcher",
227
+ hello: () => ({ adapter_session: turn.adapterSession }),
228
+ greeted: (frame) => frame.type === "resumed" || frame.type === "start",
229
+ onFrame: (frame) => {
230
+ if (frame.type === "answered" && typeof frame.delivery === "string") {
231
+ answeredOwner(peer, turn, frame);
232
+ }
233
+ if (frame.type === "interrupt" && typeof frame.interrupt === "string") {
234
+ const cancel = turn.cancel;
235
+ if (cancel === null) {
236
+ peer.send({
237
+ type: "interrupt_applied",
238
+ interrupt: frame.interrupt,
239
+ outcome: "nothing_to_interrupt",
240
+ });
241
+ return;
242
+ }
243
+ void cancel().then(
244
+ () =>
245
+ peer.send({
246
+ type: "interrupt_applied",
247
+ interrupt: frame.interrupt,
248
+ outcome: "interrupted",
249
+ }),
250
+ () =>
251
+ peer.send({
252
+ type: "interrupt_applied",
253
+ interrupt: frame.interrupt,
254
+ outcome: "nothing_to_interrupt",
255
+ }),
256
+ );
257
+ }
258
+ },
259
+ });
260
+ const start = await peer.open();
261
+ if (start.type !== "start") {
262
+ throw new KitRefusal("kit_bridge_refused", start.code ?? null);
263
+ }
264
+ try {
265
+ await Promise.race([runTurn(peer, start, bridge, turn), peer.refused]);
266
+ } catch (refusal) {
267
+ peer.send({
268
+ type: "failed",
269
+ code: refusal instanceof KitRefusal ? refusal.code : "kit_adapter_failed",
270
+ detail: refusal instanceof Error ? refusal.message : null,
271
+ });
272
+ }
273
+ peer.send({ type: "exiting" });
274
+ await Promise.race([peer.drained(), peer.refused.catch(() => undefined)]);
275
+ await peer.end();
276
+ }
@@ -0,0 +1,113 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { parse, stringify } from "yaml";
3
+ import { AGENT_OPERATION_META, ATTACHMENT_FILE_META } from "./contract.js";
4
+ import { derivedOperationId } from "./operation.js";
5
+ import { localAttachment, sendLocalConversationFile, uploadLocalAttachment } from "./original.js";
6
+ import { KitRefusal } from "./refusal.js";
7
+
8
+ class HouseRefused extends Error {
9
+ constructor(frame) {
10
+ super("kit_house_refused");
11
+ this.frame = frame;
12
+ }
13
+ }
14
+
15
+ function parsed(text) {
16
+ try {
17
+ return parse(text);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ function answerOf(frame) {
24
+ if (frame.error !== undefined || frame.result?.isError === true) throw new HouseRefused(frame);
25
+ const text = (frame.result?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
26
+ const answer = parsed(text);
27
+ if (answer === null || typeof answer !== "object") throw new KitRefusal("kit_house_answer_unreadable");
28
+ return answer;
29
+ }
30
+
31
+ function answered(frame, value) {
32
+ const text = stringify(value, { aliasDuplicateObjects: false, lineWidth: 0 });
33
+ return { jsonrpc: "2.0", id: frame.id, result: { content: [{ type: "text", text }] } };
34
+ }
35
+
36
+ function declaring(frame, meta) {
37
+ return { ...frame, params: { ...frame.params, _meta: { ...frame.params._meta, ...meta } } };
38
+ }
39
+
40
+ function saved(answer) {
41
+ if (answer.save.status !== "saved") {
42
+ throw new KitRefusal("attachment_upload_failed", answer.upload_failure ?? answer.save.failure ?? answer.save.status);
43
+ }
44
+ return answer;
45
+ }
46
+
47
+ async function declaredFile(frame) {
48
+ const path = frame.params.arguments?.path;
49
+ if (typeof path !== "string" || !isAbsolute(path)) throw new KitRefusal("kit_house_arguments_invalid", "path");
50
+ const local = await localAttachment(path, derivedOperationId(frame.params._meta[AGENT_OPERATION_META], "version"));
51
+ return { local, declared: declaring(frame, { [ATTACHMENT_FILE_META]: local.file }) };
52
+ }
53
+
54
+ async function stored(frame, call, dependencies, signal) {
55
+ const { local, declared } = await declaredFile(frame);
56
+ const requested = async () => answerOf(await call(declared));
57
+ return saved(await uploadLocalAttachment(requested, local, dependencies, signal));
58
+ }
59
+
60
+ async function sent(frame, call, dependencies, signal) {
61
+ const { local, declared } = await declaredFile(frame);
62
+ const admitted = await call(declared);
63
+ const answer = answerOf(admitted);
64
+ if (answer.save === undefined) return admitted;
65
+ saved(await sendLocalConversationFile(async () => answer, local, dependencies, signal));
66
+ const operation = derivedOperationId(frame.params._meta[AGENT_OPERATION_META], "send");
67
+ return call(declaring(declared, { [AGENT_OPERATION_META]: operation }));
68
+ }
69
+
70
+ async function appended(frame, call, dependencies, signal) {
71
+ const args = frame.params.arguments;
72
+ const operation = frame.params._meta[AGENT_OPERATION_META];
73
+ const attachments = [];
74
+ for (const [position, path] of args.attachments.entries()) {
75
+ const upload = {
76
+ ...frame,
77
+ params: {
78
+ name: "upload_attachment",
79
+ arguments: { path, room_ref: args.room_ref },
80
+ _meta: { ...frame.params._meta, [AGENT_OPERATION_META]: derivedOperationId(operation, `attachment/${position}`) },
81
+ },
82
+ };
83
+ attachments.push((await stored(upload, call, dependencies, signal)).attachment);
84
+ }
85
+ return call({ ...frame, params: { ...frame.params, arguments: { ...args, attachments } } });
86
+ }
87
+
88
+ function operated(frame) {
89
+ return typeof frame.params?._meta?.[AGENT_OPERATION_META] === "string";
90
+ }
91
+
92
+ function attaches(frame) {
93
+ const args = frame.params?.arguments;
94
+ return frame.params?.name === "append_record" && Array.isArray(args?.attachments) && typeof args.room_ref === "string";
95
+ }
96
+
97
+ export function localFiles(call, dependencies, signal) {
98
+ return async (frame) => {
99
+ if (frame.method !== "tools/call" || !operated(frame)) return call(frame);
100
+ try {
101
+ if (frame.params.name === "upload_attachment") {
102
+ return frame.params.arguments?.conversation === true
103
+ ? await sent(frame, call, dependencies, signal)
104
+ : answered(frame, await stored(frame, call, dependencies, signal));
105
+ }
106
+ if (attaches(frame)) return await appended(frame, call, dependencies, signal);
107
+ } catch (refusal) {
108
+ if (refusal instanceof HouseRefused) return refusal.frame;
109
+ throw refusal;
110
+ }
111
+ return call(frame);
112
+ };
113
+ }