@krapht/ork 1.0.0-alpha.1 → 1.0.0-alpha.10
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 +3 -4
- package/dist/{__build_info__-D9lSlMFy.mjs → __build_info__-DFF18K8y.mjs} +72 -3
- package/dist/bin.mjs +579 -379
- package/dist/{clean-CA_Oi2Wl.mjs → clean-DVaEH9e7.mjs} +5 -2
- package/dist/{completion-DBGfL-gN.mjs → completion-Dv5kf598.mjs} +6 -6
- package/dist/context-CEaKOVmG.mjs +683 -0
- package/dist/{decide-CoIJh34e.mjs → decide-3l9q8UtX.mjs} +3 -3
- package/dist/{find-rBBk7Jui.mjs → find-DEjjOlTG.mjs} +1 -1
- package/dist/format-Cb5_m-Qd.mjs +191 -0
- package/dist/{in-a-run-DdAakpRR.mjs → in-a-run-BS9Nc-JW.mjs} +1 -1
- package/dist/{limits-BPM9wUlV.mjs → limits-BSwEjQST.mjs} +1 -1
- package/dist/{list-Bv5Lxu7z.mjs → list-Qxr6rwsY.mjs} +11 -4
- package/dist/{mcp-DM30MFBw.mjs → mcp-CslnG5wf.mjs} +6 -6
- package/dist/output-Bqljtmp_.mjs +40 -0
- package/dist/{pause-C7lJkSma.mjs → pause-D7OVB5Ac.mjs} +4 -4
- package/dist/{relay-BipJYszF.mjs → relay-EceRIebO.mjs} +2 -2
- package/dist/{remove-BDs9isB6.mjs → remove-0IQZVWmI.mjs} +14 -7
- package/dist/{send-nbkjf3sc.mjs → send-C0uwfUYi.mjs} +3 -3
- package/dist/{src-hrj2h0IZ.mjs → src-Df6KOIDC.mjs} +5678 -3245
- package/dist/{src-BxUJPGa5.mjs → src-cmmXKHn1.mjs} +265 -203
- package/dist/{src-B0TT7DBi.mjs → src-i5k2Y-kf.mjs} +35 -86
- package/dist/src-oaJYIddp.mjs +2255 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
- package/dist/ui/assets/app-C8uhHVYp.css +1 -0
- package/dist/ui/assets/app-qs8Lofv6.js +75 -0
- package/dist/ui/assets/{index-CPx74lFf.js → index-DV1ZaOk-.js} +3 -3
- package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/{ui-B98FSboa.mjs → ui-CBkHJdqk.mjs} +4 -4
- package/dist/{usage-EW7DI6Z7.mjs → usage-Cd8pEYyK.mjs} +5 -5
- package/dist/{watch-BAkSGaOp.mjs → watch-DW5R2OPO.mjs} +6 -6
- package/dist/{workflows-vsEohqa7.mjs → workflows-BT1gDRIV.mjs} +4 -3
- package/dist/{worktrees-Ahj9rdWK.mjs → worktrees-g1MQMGie.mjs} +78 -19
- package/package.json +2 -4
- package/dist/context-6x3kXREO.mjs +0 -274
- package/dist/format-zMmhqyLE.mjs +0 -427
- package/dist/output-TV_j7Ni0.mjs +0 -950
- package/dist/src-B9v0lDc6.mjs +0 -1204
- package/dist/ui/assets/app-BNT0SuIz.css +0 -1
- package/dist/ui/assets/app-CmkynZyW.js +0 -67
- package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
package/dist/src-B9v0lDc6.mjs
DELETED
|
@@ -1,1204 +0,0 @@
|
|
|
1
|
-
import { Bt as FEEDBACK_TARGETS, Rt as FEEDBACK_KINDS, pt as parseReport, zt as FEEDBACK_SEVERITIES } from "./src-hrj2h0IZ.mjs";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
|
-
import { createInterface } from "node:readline";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import { createServer } from "node:http";
|
|
7
|
-
import { createSdkMcpServer, query, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
8
|
-
// ../runtime-claude/src/cache-env.ts
|
|
9
|
-
/** Claude Code's switch for the one-hour prompt cache. */
|
|
10
|
-
const PROMPT_CACHE_1H = "ENABLE_PROMPT_CACHING_1H";
|
|
11
|
-
/** The variables a session adds to its driver's environment. */
|
|
12
|
-
const cacheEnv = (spec) => spec.promptCache === "1h" ? { [PROMPT_CACHE_1H]: "1" } : {};
|
|
13
|
-
// ../runtime-claude/src/clean.ts
|
|
14
|
-
/** Claude Code's switch that turns its auto-memory off. */
|
|
15
|
-
const NO_AUTO_MEMORY = "CLAUDE_CODE_DISABLE_AUTO_MEMORY";
|
|
16
|
-
/**
|
|
17
|
-
* The CLI flag that moves the per-machine sections (working directory, memory
|
|
18
|
-
* path, git status) out of the system prompt; the SDK's
|
|
19
|
-
* `excludeDynamicSections`.
|
|
20
|
-
*/
|
|
21
|
-
const EXCLUDE_DYNAMIC_SECTIONS = "--exclude-dynamic-system-prompt-sections";
|
|
22
|
-
/** Whether the session loads no MCP server but Ork's. */
|
|
23
|
-
const strictMcpOf = (spec) => spec.tools.strictMcp === true || spec.machineClaude !== true;
|
|
24
|
-
/** The variables that keep the machine's auto-memory out of a session. */
|
|
25
|
-
const memoryEnv = (spec) => spec.machineClaude === true ? {} : { [NO_AUTO_MEMORY]: "1" };
|
|
26
|
-
/** Everything a session adds to its driver's environment. */
|
|
27
|
-
const driverEnv = (spec) => ({
|
|
28
|
-
...cacheEnv(spec),
|
|
29
|
-
...memoryEnv(spec)
|
|
30
|
-
});
|
|
31
|
-
// ../runtime-claude/src/normalize.ts
|
|
32
|
-
/**
|
|
33
|
-
* @file Both drivers emit the same message JSON (the Agent SDK's messages are
|
|
34
|
-
* the CLI's `stream-json` lines), so one normalizer serves both.
|
|
35
|
-
*/
|
|
36
|
-
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
37
|
-
const get = (value, key) => isObject(value) ? value[key] : void 0;
|
|
38
|
-
const num = (value) => typeof value === "number" ? value : 0;
|
|
39
|
-
const str = (value) => typeof value === "string" ? value : "";
|
|
40
|
-
const blocks = (message) => {
|
|
41
|
-
const content = get(get(message, "message"), "content");
|
|
42
|
-
return Array.isArray(content) ? content.filter((item) => isObject(item)) : [];
|
|
43
|
-
};
|
|
44
|
-
const usageOf = (result) => {
|
|
45
|
-
const usage = get(result, "usage");
|
|
46
|
-
return {
|
|
47
|
-
costUsd: num(result["total_cost_usd"]),
|
|
48
|
-
inputTokens: num(get(usage, "input_tokens")),
|
|
49
|
-
outputTokens: num(get(usage, "output_tokens")),
|
|
50
|
-
cacheReadTokens: num(get(usage, "cache_read_input_tokens")),
|
|
51
|
-
cacheWriteTokens: num(get(usage, "cache_creation_input_tokens")),
|
|
52
|
-
turns: num(result["num_turns"]),
|
|
53
|
-
durationMs: num(result["duration_ms"])
|
|
54
|
-
};
|
|
55
|
-
};
|
|
56
|
-
/** The outcome a `result` message describes. */
|
|
57
|
-
const outcomeOf = (result) => {
|
|
58
|
-
const errors = result["errors"];
|
|
59
|
-
return {
|
|
60
|
-
subtype: str(result["subtype"]) || "unknown",
|
|
61
|
-
sessionId: str(result["session_id"]),
|
|
62
|
-
report: parseReport(result["structured_output"]),
|
|
63
|
-
text: str(result["result"]),
|
|
64
|
-
usage: usageOf(result),
|
|
65
|
-
errors: Array.isArray(errors) ? errors.filter((e) => typeof e === "string") : []
|
|
66
|
-
};
|
|
67
|
-
};
|
|
68
|
-
/** The year 2286: a reset beyond this is not in seconds. */
|
|
69
|
-
const MAX_RESET_SECONDS = 1e10;
|
|
70
|
-
/**
|
|
71
|
-
* What a subscription's limit said (RFC-007). Only the fields Claude Code
|
|
72
|
-
* documents are read, so `utilization`, often absent, plays no part.
|
|
73
|
-
*/
|
|
74
|
-
const planLimitOf = (info) => {
|
|
75
|
-
const status = get(info, "status");
|
|
76
|
-
if (status !== "allowed" && status !== "allowed_warning" && status !== "rejected") return [];
|
|
77
|
-
const window = get(info, "rateLimitType");
|
|
78
|
-
const resets = get(info, "resetsAt");
|
|
79
|
-
const overage = get(info, "overageStatus");
|
|
80
|
-
return [{
|
|
81
|
-
type: "plan.limit",
|
|
82
|
-
status: status === "rejected" && !(status === "rejected" && overage !== "allowed" && overage !== "allowed_warning") ? "allowed_warning" : status,
|
|
83
|
-
window: typeof window === "string" ? window : "plan",
|
|
84
|
-
...typeof resets === "number" && resets > 0 && resets < MAX_RESET_SECONDS ? { resetsAt: (/* @__PURE__ */ new Date(resets * 1e3)).toISOString() } : {}
|
|
85
|
-
}];
|
|
86
|
-
};
|
|
87
|
-
/** Maps one driver message to zero or more normalized events. */
|
|
88
|
-
const normalize = (message) => {
|
|
89
|
-
if (!isObject(message)) return [];
|
|
90
|
-
switch (message["type"]) {
|
|
91
|
-
case "system": {
|
|
92
|
-
if (message["subtype"] !== "init") return [];
|
|
93
|
-
const started = {
|
|
94
|
-
type: "session.started",
|
|
95
|
-
sessionId: str(message["session_id"]),
|
|
96
|
-
model: str(message["model"])
|
|
97
|
-
};
|
|
98
|
-
if (str(message["cwd"]) !== "") started.cwd = str(message["cwd"]);
|
|
99
|
-
return [started];
|
|
100
|
-
}
|
|
101
|
-
case "assistant": return blocks(message).flatMap((block) => {
|
|
102
|
-
if (block["type"] === "text" && str(block["text"]).trim() !== "") return [{
|
|
103
|
-
type: "assistant.text",
|
|
104
|
-
text: str(block["text"])
|
|
105
|
-
}];
|
|
106
|
-
if (block["type"] === "tool_use") {
|
|
107
|
-
const input = block["input"];
|
|
108
|
-
return [{
|
|
109
|
-
type: "tool.called",
|
|
110
|
-
id: str(block["id"]),
|
|
111
|
-
name: str(block["name"]),
|
|
112
|
-
input: isObject(input) ? input : {}
|
|
113
|
-
}];
|
|
114
|
-
}
|
|
115
|
-
return [];
|
|
116
|
-
});
|
|
117
|
-
case "user": return blocks(message).filter((block) => block["type"] === "tool_result").map((block) => ({
|
|
118
|
-
type: "tool.result",
|
|
119
|
-
id: str(block["tool_use_id"]),
|
|
120
|
-
isError: block["is_error"] === true
|
|
121
|
-
}));
|
|
122
|
-
case "rate_limit_event": return planLimitOf(message["rate_limit_info"]);
|
|
123
|
-
case "result": return [{
|
|
124
|
-
type: "session.finished",
|
|
125
|
-
outcome: outcomeOf(message)
|
|
126
|
-
}];
|
|
127
|
-
default: return [];
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
/** Marks a session's start with the driver that runs it. */
|
|
131
|
-
const withDriver = (event, driver) => event.type === "session.started" ? {
|
|
132
|
-
...event,
|
|
133
|
-
driver
|
|
134
|
-
} : event;
|
|
135
|
-
/** A finished event for a session the driver could not run. */
|
|
136
|
-
const driverFailure = (message) => ({
|
|
137
|
-
type: "session.finished",
|
|
138
|
-
outcome: {
|
|
139
|
-
subtype: "error_driver",
|
|
140
|
-
sessionId: "",
|
|
141
|
-
report: null,
|
|
142
|
-
text: "",
|
|
143
|
-
usage: {
|
|
144
|
-
costUsd: 0,
|
|
145
|
-
inputTokens: 0,
|
|
146
|
-
outputTokens: 0,
|
|
147
|
-
cacheReadTokens: 0,
|
|
148
|
-
cacheWriteTokens: 0,
|
|
149
|
-
turns: 0,
|
|
150
|
-
durationMs: 0
|
|
151
|
-
},
|
|
152
|
-
errors: [message]
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
/** How long a hook holds a call before it defers it instead. */
|
|
156
|
-
const HOLD_WINDOW_MS = 54e4;
|
|
157
|
-
/**
|
|
158
|
-
* Waits at a tool call while `hold` is held: "go" once it is released within
|
|
159
|
-
* the window (or at once when it is not held), "defer" after the window.
|
|
160
|
-
*/
|
|
161
|
-
const holdAtTool = async (hold, signal, windowMs = HOLD_WINDOW_MS) => {
|
|
162
|
-
if (!hold.held()) return "go";
|
|
163
|
-
const window = new AbortController();
|
|
164
|
-
const timer = setTimeout(() => {
|
|
165
|
-
window.abort();
|
|
166
|
-
}, windowMs);
|
|
167
|
-
try {
|
|
168
|
-
await hold.wait("tool", AbortSignal.any([signal, window.signal]));
|
|
169
|
-
} finally {
|
|
170
|
-
clearTimeout(timer);
|
|
171
|
-
}
|
|
172
|
-
return hold.held() && !signal.aborted ? "defer" : "go";
|
|
173
|
-
};
|
|
174
|
-
/** The hook's answer: nothing to say for "go", the deferral otherwise. */
|
|
175
|
-
const hookOutput = (answer) => answer === "go" ? {} : { hookSpecificOutput: {
|
|
176
|
-
hookEventName: "PreToolUse",
|
|
177
|
-
permissionDecision: "defer",
|
|
178
|
-
permissionDecisionReason: "Ork paused this node; it goes on from this call when it continues."
|
|
179
|
-
} };
|
|
180
|
-
/** Whether a stream-json message is the result of a deferred tool call. */
|
|
181
|
-
const isDeferredResult = (message) => typeof message === "object" && message !== null && "type" in message && message.type === "result" && "stop_reason" in message && message.stop_reason === "tool_deferred";
|
|
182
|
-
/**
|
|
183
|
-
* Two legs of one session, as one: the tokens of both, and the later leg's
|
|
184
|
-
* cost, which is already the session's total (ADR-046).
|
|
185
|
-
*/
|
|
186
|
-
const addUsage = (a, b) => ({
|
|
187
|
-
costUsd: Math.max(a.costUsd, b.costUsd),
|
|
188
|
-
inputTokens: a.inputTokens + b.inputTokens,
|
|
189
|
-
outputTokens: a.outputTokens + b.outputTokens,
|
|
190
|
-
cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
|
|
191
|
-
cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
|
|
192
|
-
turns: a.turns + b.turns,
|
|
193
|
-
durationMs: a.durationMs + b.durationMs
|
|
194
|
-
});
|
|
195
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
196
|
-
/**
|
|
197
|
-
* The node's `--settings` with Ork's `PreToolUse` HTTP hook added: before every
|
|
198
|
-
* tool call, pre-approved ones included, the session asks whether its node may
|
|
199
|
-
* go on.
|
|
200
|
-
*/
|
|
201
|
-
const withHoldHook = (settings, holdUrl, token) => {
|
|
202
|
-
const hooks = isRecord(settings?.["hooks"]) ? settings["hooks"] : {};
|
|
203
|
-
const listed = hooks["PreToolUse"];
|
|
204
|
-
const before = Array.isArray(listed) ? [...listed] : [];
|
|
205
|
-
const hook = {
|
|
206
|
-
type: "http",
|
|
207
|
-
url: holdUrl,
|
|
208
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
209
|
-
timeout: 600
|
|
210
|
-
};
|
|
211
|
-
return {
|
|
212
|
-
...settings,
|
|
213
|
-
hooks: {
|
|
214
|
-
...hooks,
|
|
215
|
-
PreToolUse: [...before, {
|
|
216
|
-
matcher: "*",
|
|
217
|
-
hooks: [hook]
|
|
218
|
-
}]
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
};
|
|
222
|
-
/**
|
|
223
|
-
* What one stream-json message of a leg yields, and how the leg ends if it
|
|
224
|
-
* does: a result is the session's end, unless the hold hook deferred a call in
|
|
225
|
-
* this leg; a resumed leg's start is not news, and its end carries what the
|
|
226
|
-
* legs before it spent.
|
|
227
|
-
*/
|
|
228
|
-
const legStep = (message, input) => {
|
|
229
|
-
const deferred = input.deferred && isDeferredResult(message);
|
|
230
|
-
const events = [];
|
|
231
|
-
let ended;
|
|
232
|
-
for (const event of normalize(message)) {
|
|
233
|
-
if (event.type === "session.started" && input.carried !== void 0) continue;
|
|
234
|
-
if (event.type !== "session.finished") events.push(withDriver(event, input.driver));
|
|
235
|
-
else if (deferred) ended = {
|
|
236
|
-
kind: "deferred",
|
|
237
|
-
sessionId: event.outcome.sessionId,
|
|
238
|
-
usage: event.outcome.usage,
|
|
239
|
-
last: withDriver(event, input.driver)
|
|
240
|
-
};
|
|
241
|
-
else {
|
|
242
|
-
const usage = input.carried === void 0 ? event.outcome.usage : addUsage(input.carried, event.outcome.usage);
|
|
243
|
-
events.push(withDriver({
|
|
244
|
-
...event,
|
|
245
|
-
outcome: {
|
|
246
|
-
...event.outcome,
|
|
247
|
-
usage
|
|
248
|
-
}
|
|
249
|
-
}, input.driver));
|
|
250
|
-
ended = { kind: "finished" };
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return ended === void 0 ? { events } : {
|
|
254
|
-
events,
|
|
255
|
-
ended
|
|
256
|
-
};
|
|
257
|
-
};
|
|
258
|
-
// ../runtime-claude/src/cli-leg.ts
|
|
259
|
-
/**
|
|
260
|
-
* @file One `claude -p` process of a CLI-driven session: it writes the queued
|
|
261
|
-
* stream-json lines, yields the normalized events, and says how it ended. A
|
|
262
|
-
* session is one leg, or several when a paused node's tool call was deferred
|
|
263
|
-
* and the session resumed (ADR-041); a resumed leg carries what the legs
|
|
264
|
-
* before it spent, and its start is not news.
|
|
265
|
-
*/
|
|
266
|
-
const STDERR_LINES = 20;
|
|
267
|
-
const parseLine = (line) => {
|
|
268
|
-
try {
|
|
269
|
-
return JSON.parse(line);
|
|
270
|
-
} catch {
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
/** Runs one `claude` process, yielding its events; returns how it ended. */
|
|
275
|
-
const runLeg = async function* (input) {
|
|
276
|
-
const child = spawn(input.bin, input.args, {
|
|
277
|
-
cwd: input.cwd,
|
|
278
|
-
env: { ...input.env },
|
|
279
|
-
stdio: [
|
|
280
|
-
"pipe",
|
|
281
|
-
"pipe",
|
|
282
|
-
"pipe"
|
|
283
|
-
]
|
|
284
|
-
});
|
|
285
|
-
const exited = new Promise((resolve) => {
|
|
286
|
-
child.on("close", (code) => {
|
|
287
|
-
resolve(code);
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
const spawnError = new Promise((resolve) => {
|
|
291
|
-
child.on("error", resolve);
|
|
292
|
-
});
|
|
293
|
-
const stderr = [];
|
|
294
|
-
child.stderr.setEncoding("utf8");
|
|
295
|
-
child.stderr.on("data", (chunk) => {
|
|
296
|
-
stderr.push(...chunk.split("\n").filter((l) => l.trim() !== ""));
|
|
297
|
-
stderr.splice(0, Math.max(0, stderr.length - STDERR_LINES));
|
|
298
|
-
});
|
|
299
|
-
const write = (line) => {
|
|
300
|
-
if (child.stdin.writable) child.stdin.write(line);
|
|
301
|
-
};
|
|
302
|
-
input.attach({
|
|
303
|
-
write,
|
|
304
|
-
stop: () => {
|
|
305
|
-
child.kill("SIGINT");
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
for (const line of input.pending.splice(0)) write(line);
|
|
309
|
-
let outcome;
|
|
310
|
-
try {
|
|
311
|
-
for await (const line of createInterface({ input: child.stdout })) {
|
|
312
|
-
const { events, ended } = legStep(parseLine(line), {
|
|
313
|
-
carried: input.carried,
|
|
314
|
-
deferred: input.deferred(),
|
|
315
|
-
driver: "cli"
|
|
316
|
-
});
|
|
317
|
-
yield* events;
|
|
318
|
-
if (ended !== void 0) {
|
|
319
|
-
child.stdin.end();
|
|
320
|
-
outcome = ended;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
const code = await Promise.race([exited, spawnError.then((error) => error)]);
|
|
324
|
-
return outcome ?? {
|
|
325
|
-
kind: "failed",
|
|
326
|
-
reason: code instanceof Error ? `could not start ${input.bin}: ${code.message}` : stderr.at(-1) ?? `claude exited with code ${String(code)}`
|
|
327
|
-
};
|
|
328
|
-
} finally {
|
|
329
|
-
input.attach();
|
|
330
|
-
if (child.exitCode === null) child.kill();
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
/** The name Claude Code gives an Ork tool: `mcp__ork__<name>`. */
|
|
334
|
-
const toolId = (name) => `mcp__ork__${name}`;
|
|
335
|
-
const text = (value) => ({ content: [{
|
|
336
|
-
type: "text",
|
|
337
|
-
text: value
|
|
338
|
-
}] });
|
|
339
|
-
const defineTool = (definition) => {
|
|
340
|
-
const schema = z.object(definition.shape);
|
|
341
|
-
return {
|
|
342
|
-
name: definition.name,
|
|
343
|
-
bind: (context) => definition.available?.(context) ?? true ? {
|
|
344
|
-
name: definition.name,
|
|
345
|
-
description: definition.description,
|
|
346
|
-
shape: definition.shape,
|
|
347
|
-
schema,
|
|
348
|
-
preapproved: definition.preapproved ?? true,
|
|
349
|
-
call: (args) => definition.run(context, schema.parse(args))
|
|
350
|
-
} : null
|
|
351
|
-
};
|
|
352
|
-
};
|
|
353
|
-
// ../runtime-claude/src/mcp-http.ts
|
|
354
|
-
/**
|
|
355
|
-
* @file A minimal MCP server over the streamable HTTP transport, for the
|
|
356
|
-
* `claude` CLI driver. It answers with plain JSON (no SSE stream), which the
|
|
357
|
-
* transport allows, and implements only what Ork needs: `initialize`, `ping`,
|
|
358
|
-
* `tools/list` and `tools/call`. Beside it, `/hold` answers the session's
|
|
359
|
-
* `PreToolUse` HTTP hook, which waits while the node is paused (ADR-041).
|
|
360
|
-
*/
|
|
361
|
-
const PATH = "/mcp";
|
|
362
|
-
const HOLD_PATH = "/hold";
|
|
363
|
-
const MAX_BODY = 1048576;
|
|
364
|
-
const MCP_PROTOCOL_FALLBACK = "2025-06-18";
|
|
365
|
-
const JsonRpcSchema = z.object({
|
|
366
|
-
jsonrpc: z.literal("2.0"),
|
|
367
|
-
id: z.union([z.string(), z.number()]).optional(),
|
|
368
|
-
method: z.string(),
|
|
369
|
-
params: z.record(z.string(), z.unknown()).optional()
|
|
370
|
-
});
|
|
371
|
-
/** Handles one JSON-RPC request; `null` for a notification (no reply). */
|
|
372
|
-
const handleRpc = async (tools, message) => {
|
|
373
|
-
const parsed = JsonRpcSchema.safeParse(message);
|
|
374
|
-
if (!parsed.success) return { error: {
|
|
375
|
-
code: -32600,
|
|
376
|
-
message: "invalid request"
|
|
377
|
-
} };
|
|
378
|
-
const { id, method, params } = parsed.data;
|
|
379
|
-
if (id === void 0) return null;
|
|
380
|
-
switch (method) {
|
|
381
|
-
case "initialize": {
|
|
382
|
-
const requested = params?.["protocolVersion"];
|
|
383
|
-
return { result: {
|
|
384
|
-
protocolVersion: typeof requested === "string" ? requested : MCP_PROTOCOL_FALLBACK,
|
|
385
|
-
capabilities: { tools: {} },
|
|
386
|
-
serverInfo: {
|
|
387
|
-
name: "ork",
|
|
388
|
-
version: "0.0.0"
|
|
389
|
-
}
|
|
390
|
-
} };
|
|
391
|
-
}
|
|
392
|
-
case "ping": return { result: {} };
|
|
393
|
-
case "tools/list": return { result: { tools: tools.map((t) => ({
|
|
394
|
-
name: t.name,
|
|
395
|
-
description: t.description,
|
|
396
|
-
inputSchema: z.toJSONSchema(t.schema)
|
|
397
|
-
})) } };
|
|
398
|
-
case "tools/call": {
|
|
399
|
-
const tool = tools.find((t) => t.name === params?.["name"]);
|
|
400
|
-
if (tool === void 0) return { error: {
|
|
401
|
-
code: -32602,
|
|
402
|
-
message: "unknown tool"
|
|
403
|
-
} };
|
|
404
|
-
try {
|
|
405
|
-
return { result: await tool.call(params?.["arguments"] ?? {}) };
|
|
406
|
-
} catch (error) {
|
|
407
|
-
return { result: {
|
|
408
|
-
content: [{
|
|
409
|
-
type: "text",
|
|
410
|
-
text: error instanceof Error ? error.message : "tool failed"
|
|
411
|
-
}],
|
|
412
|
-
isError: true
|
|
413
|
-
} };
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
default: return { error: {
|
|
417
|
-
code: -32601,
|
|
418
|
-
message: `unknown method ${method}`
|
|
419
|
-
} };
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
const authorized = (req, token) => {
|
|
423
|
-
const expected = Buffer.from(`Bearer ${token}`);
|
|
424
|
-
const given = Buffer.from(req.headers.authorization ?? "");
|
|
425
|
-
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
426
|
-
};
|
|
427
|
-
const readBody = async (req) => {
|
|
428
|
-
const chunks = [];
|
|
429
|
-
let size = 0;
|
|
430
|
-
for await (const chunk of req) {
|
|
431
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
432
|
-
size += buffer.length;
|
|
433
|
-
if (size > MAX_BODY) return null;
|
|
434
|
-
chunks.push(buffer);
|
|
435
|
-
}
|
|
436
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
437
|
-
};
|
|
438
|
-
const respond = (res, status, body) => {
|
|
439
|
-
if (body === void 0) {
|
|
440
|
-
res.writeHead(status).end();
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(body));
|
|
444
|
-
};
|
|
445
|
-
/** The hook's POST: waits for `hold`; a closed connection gives up. */
|
|
446
|
-
const serveHold = async (req, res, hold) => {
|
|
447
|
-
if (req.method !== "POST") {
|
|
448
|
-
respond(res, 405);
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
if (await readBody(req) === null) {
|
|
452
|
-
respond(res, 413);
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
const gone = new AbortController();
|
|
456
|
-
res.once("close", () => {
|
|
457
|
-
gone.abort();
|
|
458
|
-
});
|
|
459
|
-
const output = await hold(gone.signal);
|
|
460
|
-
if (!gone.signal.aborted) respond(res, 200, output);
|
|
461
|
-
};
|
|
462
|
-
const serve = async (req, res, tools, token, hold) => {
|
|
463
|
-
if (!authorized(req, token)) {
|
|
464
|
-
respond(res, 401);
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
if (req.url === HOLD_PATH && hold !== void 0) {
|
|
468
|
-
await serveHold(req, res, hold);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
if (req.url !== PATH) {
|
|
472
|
-
respond(res, 404);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
if (req.method !== "POST") {
|
|
476
|
-
respond(res, 405);
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
const body = await readBody(req);
|
|
480
|
-
if (body === null) {
|
|
481
|
-
respond(res, 413);
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
let message;
|
|
485
|
-
try {
|
|
486
|
-
message = JSON.parse(body);
|
|
487
|
-
} catch {
|
|
488
|
-
respond(res, 400);
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
const id = message !== null && typeof message === "object" && "id" in message ? message.id : null;
|
|
492
|
-
const reply = await handleRpc(tools, message);
|
|
493
|
-
if (reply === null) {
|
|
494
|
-
respond(res, 202);
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
respond(res, 200, {
|
|
498
|
-
jsonrpc: "2.0",
|
|
499
|
-
id,
|
|
500
|
-
...reply
|
|
501
|
-
});
|
|
502
|
-
};
|
|
503
|
-
/**
|
|
504
|
-
* Serves Ork's tools to one `claude` CLI session: on 127.0.0.1 only, on a
|
|
505
|
-
* random port, behind a per-session bearer token.
|
|
506
|
-
*/
|
|
507
|
-
const startOrkMcpHttp = async (tools, hold) => {
|
|
508
|
-
const token = randomBytes(24).toString("hex");
|
|
509
|
-
const http = createServer((req, res) => {
|
|
510
|
-
serve(req, res, tools, token, hold).catch(() => {
|
|
511
|
-
if (!res.headersSent) respond(res, 500);
|
|
512
|
-
});
|
|
513
|
-
});
|
|
514
|
-
await new Promise((resolve) => {
|
|
515
|
-
http.listen(0, "127.0.0.1", resolve);
|
|
516
|
-
});
|
|
517
|
-
const address = http.address();
|
|
518
|
-
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
519
|
-
return {
|
|
520
|
-
url: `http://127.0.0.1:${port}${PATH}`,
|
|
521
|
-
holdUrl: `http://127.0.0.1:${port}${HOLD_PATH}`,
|
|
522
|
-
token,
|
|
523
|
-
close: () => new Promise((resolve) => {
|
|
524
|
-
http.closeAllConnections();
|
|
525
|
-
http.close(() => {
|
|
526
|
-
resolve();
|
|
527
|
-
});
|
|
528
|
-
})
|
|
529
|
-
};
|
|
530
|
-
};
|
|
531
|
-
// ../runtime-claude/src/tools/ask-user.ts
|
|
532
|
-
const askUserTool = defineTool({
|
|
533
|
-
name: "ask_user",
|
|
534
|
-
description: "Ask the user to decide something only they can decide. Give two to four options and the one you recommend. The run waits for the answer.",
|
|
535
|
-
shape: {
|
|
536
|
-
question: z.string().describe("One clear question."),
|
|
537
|
-
options: z.array(z.object({
|
|
538
|
-
id: z.string().describe("A short id, e.g. 'keep-api'."),
|
|
539
|
-
label: z.string().optional(),
|
|
540
|
-
description: z.string().optional().describe("What choosing it means.")
|
|
541
|
-
})).min(2).max(4),
|
|
542
|
-
recommended: z.string().optional().describe("The id you recommend."),
|
|
543
|
-
context: z.string().optional().describe("What the user needs to know to decide, in a few sentences.")
|
|
544
|
-
},
|
|
545
|
-
run: async ({ nodeId, ports }, args) => {
|
|
546
|
-
let note;
|
|
547
|
-
let owner;
|
|
548
|
-
const question = {
|
|
549
|
-
nodeId,
|
|
550
|
-
onNote: (given) => {
|
|
551
|
-
note = given;
|
|
552
|
-
},
|
|
553
|
-
onOwner: (given) => {
|
|
554
|
-
owner = given;
|
|
555
|
-
},
|
|
556
|
-
question: args.question,
|
|
557
|
-
options: args.options.map((o) => {
|
|
558
|
-
const choice = { id: o.id };
|
|
559
|
-
if (o.label !== void 0) choice.label = o.label;
|
|
560
|
-
if (o.description !== void 0) choice.description = o.description;
|
|
561
|
-
return choice;
|
|
562
|
-
})
|
|
563
|
-
};
|
|
564
|
-
if (args.recommended !== void 0) question.recommended = args.recommended;
|
|
565
|
-
if (args.context !== void 0) question.context = args.context;
|
|
566
|
-
const answer = await ports.human.ask(question);
|
|
567
|
-
const who = owner === void 0 ? "The user" : `Your owner, \`${owner}\`,`;
|
|
568
|
-
return text(note === void 0 ? `${who} chose: ${answer}` : `${who} chose: ${answer}\nThey said: ${note}`);
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
// ../runtime-claude/src/tools/claims.ts
|
|
572
|
-
/**
|
|
573
|
-
* @file `claim` and `release`: a session's side of the run's claims. A node
|
|
574
|
-
* claims a file, folder or other resource before changing it, so nodes
|
|
575
|
-
* running at the same time do not change the same thing; a refused claim says
|
|
576
|
-
* who holds it. Only sessions that may write while another does get them.
|
|
577
|
-
*/
|
|
578
|
-
const contended = ({ ports }) => ports.claims?.contended() === true;
|
|
579
|
-
const resource = z.string().min(1).describe("A path from the project root (a folder covers everything in it), or a name for something else shared, e.g. 'db:migrations'.");
|
|
580
|
-
const claimTool = defineTool({
|
|
581
|
-
name: "claim",
|
|
582
|
-
description: "Claim a file, folder or other shared resource before you change it, so other nodes running in this run leave it alone. If another node holds it (or something overlapping), you are told who; Ork tells you when it is released.",
|
|
583
|
-
shape: { resource },
|
|
584
|
-
available: contended,
|
|
585
|
-
run: ({ ports }, args) => {
|
|
586
|
-
const claimed = ports.claims?.claim(args.resource);
|
|
587
|
-
if (claimed === void 0) return Promise.resolve(text("No claims."));
|
|
588
|
-
if (!claimed.ok) return Promise.resolve(text(`Not claimed: ${claimed.error}.`));
|
|
589
|
-
return Promise.resolve(text(claimed.already ? `You already hold ${claimed.resource}.` : `Claimed ${claimed.resource}.`));
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
const releaseTool = defineTool({
|
|
593
|
-
name: "release",
|
|
594
|
-
description: "Release a claim you are done with, so other nodes can take it. Ork releases your remaining claims when you finish.",
|
|
595
|
-
shape: { resource },
|
|
596
|
-
available: contended,
|
|
597
|
-
run: ({ ports }, args) => {
|
|
598
|
-
const released = ports.claims?.release(args.resource);
|
|
599
|
-
if (released === void 0) return Promise.resolve(text("No claims."));
|
|
600
|
-
return Promise.resolve(text(released.ok ? `Released ${released.resource}.` : `Not released: ${released.error}.`));
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
// ../runtime-claude/src/tools/delegate.ts
|
|
604
|
-
const BRIEF = z.string().min(1).describe("Self-contained: the goal, the files that matter, and what done looks like. The child does not see your conversation.");
|
|
605
|
-
/** What the leader reads back: each report without the usage details. */
|
|
606
|
-
const reportLine = (report) => {
|
|
607
|
-
const { usage, ...output } = report.output;
|
|
608
|
-
const cost = usage !== null && typeof usage === "object" && "costUsd" in usage ? usage.costUsd : void 0;
|
|
609
|
-
return {
|
|
610
|
-
child: report.child,
|
|
611
|
-
ok: report.ok,
|
|
612
|
-
...output,
|
|
613
|
-
...cost === void 0 ? {} : { costUsd: cost },
|
|
614
|
-
...report.ok || report.error === void 0 || report.error === output["summary"] ? {} : { error: report.error }
|
|
615
|
-
};
|
|
616
|
-
};
|
|
617
|
-
const delegationText = (delegation) => text(JSON.stringify({
|
|
618
|
-
reports: delegation.reports.map(reportLine),
|
|
619
|
-
...delegation.notes.length > 0 ? { notes: delegation.notes } : {}
|
|
620
|
-
}));
|
|
621
|
-
const delegateTool = defineTool({
|
|
622
|
-
name: "delegate",
|
|
623
|
-
description: "Hand tasks to nodes you own. Tasks in one call run in parallel; the call returns every child's report when all are done. Ork merges a passing child's branch into yours.",
|
|
624
|
-
shape: { tasks: z.array(z.object({
|
|
625
|
-
child: z.string().describe("The id of a node you own."),
|
|
626
|
-
brief: BRIEF
|
|
627
|
-
})).min(1) },
|
|
628
|
-
available: ({ ports }) => (ports.ownership?.children.length ?? 0) > 0,
|
|
629
|
-
run: async ({ ports }, args) => {
|
|
630
|
-
if (ports.ownership === void 0) return text("You own no nodes.");
|
|
631
|
-
return delegationText(await ports.ownership.delegate(args.tasks));
|
|
632
|
-
}
|
|
633
|
-
});
|
|
634
|
-
const spawnWorkerTool = defineTool({
|
|
635
|
-
name: "spawn_worker",
|
|
636
|
-
description: "Start new instances of a child worker for work that splits into independent parts. Each runs in its own worktree; the call returns their reports when all are done.",
|
|
637
|
-
shape: { tasks: z.array(z.object({
|
|
638
|
-
template: z.string().describe("The id of a child worker you own."),
|
|
639
|
-
brief: BRIEF
|
|
640
|
-
})).min(1) },
|
|
641
|
-
available: ({ ports }) => ports.ownership !== void 0 && ports.ownership.templates.length > 0 && ports.ownership.workersLeft() > 0,
|
|
642
|
-
run: async ({ ports }, args) => {
|
|
643
|
-
if (ports.ownership === void 0) return text("You own no workers.");
|
|
644
|
-
return delegationText(await ports.ownership.spawn(args.tasks));
|
|
645
|
-
}
|
|
646
|
-
});
|
|
647
|
-
// ../runtime-claude/src/tools/feedback.ts
|
|
648
|
-
/**
|
|
649
|
-
* @file `feedback`: a session's way to report what got in its way, kept on the
|
|
650
|
-
* user's machine (specs/feedback.md). Every session inside a run gets it; Ork
|
|
651
|
-
* caps reports per session and drops a run's duplicates.
|
|
652
|
-
*/
|
|
653
|
-
const feedbackTool = defineTool({
|
|
654
|
-
name: "feedback",
|
|
655
|
-
description: "Report something that materially got in the way of your task: a tool that failed or misled you, context or instructions that were missing or wrong, a skill that did not fit, or a flaw in the workflow. One line plus a short body; at most a few per session. Do not report routine work or your own mistakes.",
|
|
656
|
-
shape: {
|
|
657
|
-
kind: z.enum(FEEDBACK_KINDS),
|
|
658
|
-
target: z.enum(FEEDBACK_TARGETS).optional().describe("What it is about: ork (the harness and its tools), workflow (this workflow's briefs, models, budgets, loops), or project (AGENTS.md, CLAUDE.md, skills, docs). Default: from the kind."),
|
|
659
|
-
severity: z.enum(FEEDBACK_SEVERITIES).optional(),
|
|
660
|
-
title: z.string().min(1).describe("One line."),
|
|
661
|
-
body: z.string().optional().describe("A few sentences: what happened and what would have helped."),
|
|
662
|
-
evidence: z.array(z.string()).optional().describe("File paths, tool names; never pasted file contents.")
|
|
663
|
-
},
|
|
664
|
-
available: ({ ports }) => ports.feedback !== void 0,
|
|
665
|
-
run: ({ ports }, args) => {
|
|
666
|
-
const draft = {
|
|
667
|
-
kind: args.kind,
|
|
668
|
-
title: args.title
|
|
669
|
-
};
|
|
670
|
-
if (args.target !== void 0) draft.target = args.target;
|
|
671
|
-
if (args.severity !== void 0) draft.severity = args.severity;
|
|
672
|
-
if (args.body !== void 0) draft.body = args.body;
|
|
673
|
-
if (args.evidence !== void 0) draft.evidence = args.evidence;
|
|
674
|
-
const sent = ports.feedback?.report(draft);
|
|
675
|
-
if (sent === void 0) return Promise.resolve(text("No feedback here."));
|
|
676
|
-
return Promise.resolve(text(sent.ok ? "Recorded. Thank you; carry on with your task." : `Not recorded: ${sent.error}.`));
|
|
677
|
-
}
|
|
678
|
-
});
|
|
679
|
-
// ../runtime-claude/src/tools/messages.ts
|
|
680
|
-
/**
|
|
681
|
-
* @file `send_message`, `read_inbox` and `answer`: a session's side of the
|
|
682
|
-
* run's message bus. A message lands in the recipient's inbox and a live
|
|
683
|
-
* session gets a one-line notice; `answer` replies to a question Ork asked an
|
|
684
|
-
* owner about a node it owns. Only sessions inside a run get them.
|
|
685
|
-
*/
|
|
686
|
-
/**
|
|
687
|
-
* Peers are mutual: a node no one may write to has no inbox to read, unless it
|
|
688
|
-
* may be linked later, since a session's tools are set when it starts.
|
|
689
|
-
*/
|
|
690
|
-
const hasPeers = ({ ports }) => (ports.mailbox?.peers().length ?? 0) > 0 || ports.mailbox?.linkable() === true;
|
|
691
|
-
/** A program or a rule may write to a node with no one to write to itself. */
|
|
692
|
-
const hasInbox = (context) => hasPeers(context) || context.ports.mailbox?.hears() === true;
|
|
693
|
-
const MAX_WAIT_SECONDS = 3600;
|
|
694
|
-
const DEFAULT_WAIT_SECONDS = 300;
|
|
695
|
-
const inboxText = (unread) => JSON.stringify({ messages: unread.map((m) => ({
|
|
696
|
-
from: m.from,
|
|
697
|
-
...m.via === void 0 ? {} : { via: m.via },
|
|
698
|
-
body: m.body
|
|
699
|
-
})) });
|
|
700
|
-
const sendMessageTool = defineTool({
|
|
701
|
-
name: "send_message",
|
|
702
|
-
description: "Send a short message to a node you can reach: your owner, a node you own, one you share a declared channel with, or a node of another run the user linked you to. It sees a notice and reads the message with read_inbox. Use it for what they need to know now; your report still goes to your owner at the end.",
|
|
703
|
-
shape: {
|
|
704
|
-
to: z.string().describe("The node's id, or <run>/<node> for a linked node."),
|
|
705
|
-
body: z.string().min(1).describe("Self-contained: the recipient does not see your work.")
|
|
706
|
-
},
|
|
707
|
-
available: hasPeers,
|
|
708
|
-
run: ({ ports }, args) => {
|
|
709
|
-
const sent = ports.mailbox?.send(args.to, args.body);
|
|
710
|
-
if (sent === void 0) return Promise.resolve(text("No message bus."));
|
|
711
|
-
if (!sent.ok) return Promise.resolve(text(`Not sent: ${sent.error}.`));
|
|
712
|
-
return Promise.resolve(text(sent.delivered === "now" ? `Sent to ${args.to}; it sees a notice at its next step.` : `Sent to ${args.to}; it is not running now and sees the message if it starts again.`));
|
|
713
|
-
}
|
|
714
|
-
});
|
|
715
|
-
const readInboxTool = defineTool({
|
|
716
|
-
name: "read_inbox",
|
|
717
|
-
description: "Read your unread messages from other nodes, oldest first. Ork tells you when one arrives.",
|
|
718
|
-
shape: {},
|
|
719
|
-
available: hasInbox,
|
|
720
|
-
run: ({ ports }) => {
|
|
721
|
-
const unread = ports.mailbox?.read() ?? [];
|
|
722
|
-
return Promise.resolve(text(unread.length === 0 ? "No unread messages." : inboxText(unread)));
|
|
723
|
-
}
|
|
724
|
-
});
|
|
725
|
-
const waitForMessagesTool = defineTool({
|
|
726
|
-
name: "wait_for_messages",
|
|
727
|
-
description: "Wait for a message from another node when you cannot go on without its reply, instead of ending your session. Returns what arrived, or says none did in time. You hold no session slot while you wait.",
|
|
728
|
-
shape: { timeoutSeconds: z.number().int().positive().max(MAX_WAIT_SECONDS).optional().describe(`How long to wait. Default: ${String(DEFAULT_WAIT_SECONDS)}.`) },
|
|
729
|
-
available: hasInbox,
|
|
730
|
-
run: async ({ ports }, args) => {
|
|
731
|
-
const seconds = args.timeoutSeconds ?? DEFAULT_WAIT_SECONDS;
|
|
732
|
-
await ports.mailbox?.wait(seconds * 1e3);
|
|
733
|
-
const unread = ports.mailbox?.read() ?? [];
|
|
734
|
-
return text(unread.length === 0 ? `No message yet, after waiting up to ${String(seconds)} seconds. Ork may have told you something; if not, wait again or go on.` : inboxText(unread));
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
const answerTool = defineTool({
|
|
738
|
-
name: "answer",
|
|
739
|
-
description: "Answer a question Ork asked you as the owner of a node, such as a loop of yours that ran out of rounds. The question's id and options are in Ork's message.",
|
|
740
|
-
shape: {
|
|
741
|
-
question: z.string().describe("The question's id, e.g. 'q1'."),
|
|
742
|
-
choice: z.string().describe("One of its options."),
|
|
743
|
-
note: z.string().optional().describe("Why, in a sentence or two; it is recorded with your answer.")
|
|
744
|
-
},
|
|
745
|
-
available: ({ ports }) => ports.mailbox !== void 0 && ports.ownership !== void 0,
|
|
746
|
-
run: ({ ports }, args) => {
|
|
747
|
-
const answered = ports.mailbox?.answer(args.question, args.choice, args.note);
|
|
748
|
-
if (answered === void 0) return Promise.resolve(text("No questions."));
|
|
749
|
-
return Promise.resolve(text(answered.ok ? `Answered ${args.question}: ${args.choice}.` : `Not answered: ${answered.error}.`));
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
// ../runtime-claude/src/tools/registry.ts
|
|
753
|
-
/**
|
|
754
|
-
* @file The registry of Ork's MCP tools. Both drivers serve what it binds for a
|
|
755
|
-
* session; a new tool is one file here and one line in `ORK_TOOLS`.
|
|
756
|
-
*/
|
|
757
|
-
const ORK_TOOLS = [
|
|
758
|
-
askUserTool,
|
|
759
|
-
defineTool({
|
|
760
|
-
name: "permission",
|
|
761
|
-
description: "Ork's permission prompt for the claude CLI.",
|
|
762
|
-
shape: {
|
|
763
|
-
tool_name: z.string(),
|
|
764
|
-
input: z.record(z.string(), z.unknown()),
|
|
765
|
-
tool_use_id: z.string().optional()
|
|
766
|
-
},
|
|
767
|
-
preapproved: false,
|
|
768
|
-
available: ({ driver }) => driver === "cli",
|
|
769
|
-
run: async ({ nodeId, ports }, args) => {
|
|
770
|
-
const decision = await ports.human.permit({
|
|
771
|
-
nodeId,
|
|
772
|
-
toolName: args.tool_name,
|
|
773
|
-
input: args.input
|
|
774
|
-
});
|
|
775
|
-
return text(JSON.stringify(decision.allow ? {
|
|
776
|
-
behavior: "allow",
|
|
777
|
-
updatedInput: args.input
|
|
778
|
-
} : {
|
|
779
|
-
behavior: "deny",
|
|
780
|
-
message: decision.message
|
|
781
|
-
}));
|
|
782
|
-
}
|
|
783
|
-
}),
|
|
784
|
-
delegateTool,
|
|
785
|
-
spawnWorkerTool,
|
|
786
|
-
sendMessageTool,
|
|
787
|
-
readInboxTool,
|
|
788
|
-
waitForMessagesTool,
|
|
789
|
-
answerTool,
|
|
790
|
-
claimTool,
|
|
791
|
-
releaseTool,
|
|
792
|
-
feedbackTool
|
|
793
|
-
];
|
|
794
|
-
/** The tools one session gets. */
|
|
795
|
-
const orkTools = (context) => ORK_TOOLS.flatMap((tool) => tool.bind(context) ?? []);
|
|
796
|
-
/** Their Claude Code names, for the session's allowed tools. */
|
|
797
|
-
const preapprovedTools = (tools) => tools.filter((t) => t.preapproved).map((t) => toolId(t.name));
|
|
798
|
-
// ../runtime-claude/src/cli.ts
|
|
799
|
-
/**
|
|
800
|
-
* Per-request timeout for Ork's MCP server, in ms. The CLI otherwise gives an
|
|
801
|
-
* HTTP server's request 60 seconds and drops a call silent for 5 minutes, and
|
|
802
|
-
* `delegate` waits for whole child sessions (the per-server `timeout` raises
|
|
803
|
-
* both; `MCP_TOOL_TIMEOUT` still caps it).
|
|
804
|
-
*/
|
|
805
|
-
const ORK_TOOL_TIMEOUT_MS = 864e5;
|
|
806
|
-
/** A later message goes in at the next step, even mid-turn. */
|
|
807
|
-
const userLine = (text, later = false) => `${JSON.stringify({
|
|
808
|
-
type: "user",
|
|
809
|
-
message: {
|
|
810
|
-
role: "user",
|
|
811
|
-
content: text
|
|
812
|
-
},
|
|
813
|
-
parent_tool_use_id: null,
|
|
814
|
-
...later ? { priority: "next" } : {}
|
|
815
|
-
})}\n`;
|
|
816
|
-
/**
|
|
817
|
-
* The `claude -p` arguments for a session spec. The brief goes on stdin;
|
|
818
|
-
* `allowed` are the Ork tools that need no permission prompt.
|
|
819
|
-
*/
|
|
820
|
-
const buildCliArgs = (spec, mcp, allowed) => {
|
|
821
|
-
const mcpConfig = { mcpServers: { ["ork"]: {
|
|
822
|
-
type: "http",
|
|
823
|
-
url: mcp.url,
|
|
824
|
-
headers: { Authorization: `Bearer ${mcp.token}` },
|
|
825
|
-
timeout: ORK_TOOL_TIMEOUT_MS
|
|
826
|
-
} } };
|
|
827
|
-
const args = [
|
|
828
|
-
"-p",
|
|
829
|
-
"--input-format",
|
|
830
|
-
"stream-json",
|
|
831
|
-
"--output-format",
|
|
832
|
-
"stream-json",
|
|
833
|
-
"--verbose",
|
|
834
|
-
"--setting-sources",
|
|
835
|
-
spec.settingSources.join(","),
|
|
836
|
-
"--append-system-prompt",
|
|
837
|
-
spec.systemAppend,
|
|
838
|
-
EXCLUDE_DYNAMIC_SECTIONS,
|
|
839
|
-
"--permission-mode",
|
|
840
|
-
spec.permissionMode,
|
|
841
|
-
"--json-schema",
|
|
842
|
-
JSON.stringify(spec.reportSchema),
|
|
843
|
-
"--mcp-config",
|
|
844
|
-
JSON.stringify(mcpConfig),
|
|
845
|
-
"--permission-prompt-tool",
|
|
846
|
-
toolId("permission"),
|
|
847
|
-
"--allowedTools",
|
|
848
|
-
[...spec.tools.allow, ...allowed].join(",")
|
|
849
|
-
];
|
|
850
|
-
if (spec.tools.only !== void 0) args.push("--tools", spec.tools.only.join(","));
|
|
851
|
-
if (strictMcpOf(spec)) args.push("--strict-mcp-config");
|
|
852
|
-
if (spec.agents !== void 0) args.push("--agents", JSON.stringify(spec.agents));
|
|
853
|
-
if (spec.tools.deny.length > 0) args.push("--disallowedTools", spec.tools.deny.join(","));
|
|
854
|
-
if (spec.model !== void 0) args.push("--model", spec.model);
|
|
855
|
-
if (spec.effort !== void 0) args.push("--effort", spec.effort);
|
|
856
|
-
if (spec.maxTurns !== void 0) args.push("--max-turns", String(spec.maxTurns));
|
|
857
|
-
if (spec.budgetUsd !== void 0) args.push("--max-budget-usd", String(spec.budgetUsd));
|
|
858
|
-
if (spec.resume !== void 0) args.push("--resume", spec.resume);
|
|
859
|
-
if (spec.resume !== void 0 && spec.fork === true) args.push("--fork-session");
|
|
860
|
-
for (const dir of spec.plugins ?? []) args.push("--plugin-dir", dir);
|
|
861
|
-
const settings = mcp.holdUrl === void 0 ? spec.settings : withHoldHook(spec.settings, mcp.holdUrl, mcp.token);
|
|
862
|
-
if (settings !== void 0) args.push("--settings", JSON.stringify(settings));
|
|
863
|
-
return args;
|
|
864
|
-
};
|
|
865
|
-
/**
|
|
866
|
-
* Drives a session with the user's own `claude` CLI in print mode, logged in by
|
|
867
|
-
* the user. Ork never reads or passes its credentials.
|
|
868
|
-
*/
|
|
869
|
-
const createCliRuntime = (options = {}) => ({
|
|
870
|
-
driver: "cli",
|
|
871
|
-
start: (spec, ports) => {
|
|
872
|
-
const pending = [userLine(spec.prompt)];
|
|
873
|
-
let io;
|
|
874
|
-
const ended = new AbortController();
|
|
875
|
-
/** The hold hook deferred a call in the running process. */
|
|
876
|
-
let deferring = false;
|
|
877
|
-
const { hold } = ports;
|
|
878
|
-
const events = async function* () {
|
|
879
|
-
const tools = orkTools({
|
|
880
|
-
nodeId: spec.nodeId,
|
|
881
|
-
driver: "cli",
|
|
882
|
-
ports
|
|
883
|
-
});
|
|
884
|
-
const mcp = await startOrkMcpHttp(tools, hold === void 0 ? void 0 : async (gone) => {
|
|
885
|
-
const answer = await holdAtTool(hold, AbortSignal.any([gone, ended.signal]), options.holdWindowMs);
|
|
886
|
-
if (answer === "defer") deferring = true;
|
|
887
|
-
return hookOutput(answer);
|
|
888
|
-
});
|
|
889
|
-
const allowed = preapprovedTools(tools);
|
|
890
|
-
const endpoint = hold === void 0 ? {
|
|
891
|
-
url: mcp.url,
|
|
892
|
-
token: mcp.token
|
|
893
|
-
} : mcp;
|
|
894
|
-
const legOf = (args, carried) => runLeg({
|
|
895
|
-
bin: options.bin ?? "claude",
|
|
896
|
-
env: {
|
|
897
|
-
...options.env ?? process.env,
|
|
898
|
-
...driverEnv(spec)
|
|
899
|
-
},
|
|
900
|
-
cwd: spec.cwd,
|
|
901
|
-
args,
|
|
902
|
-
carried,
|
|
903
|
-
pending,
|
|
904
|
-
deferred: () => deferring,
|
|
905
|
-
attach: (attached) => {
|
|
906
|
-
io = attached;
|
|
907
|
-
}
|
|
908
|
-
});
|
|
909
|
-
let args = buildCliArgs(spec, endpoint, allowed);
|
|
910
|
-
let spent;
|
|
911
|
-
try {
|
|
912
|
-
for (;;) {
|
|
913
|
-
const leg = yield* legOf(args, spent);
|
|
914
|
-
if (leg.kind === "failed") {
|
|
915
|
-
yield driverFailure(leg.reason);
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
if (leg.kind === "finished") return;
|
|
919
|
-
deferring = false;
|
|
920
|
-
spent = spent === void 0 ? leg.usage : addUsage(spent, leg.usage);
|
|
921
|
-
await hold?.wait("tool", ended.signal);
|
|
922
|
-
if (ended.signal.aborted) {
|
|
923
|
-
yield leg.last;
|
|
924
|
-
return;
|
|
925
|
-
}
|
|
926
|
-
args = buildCliArgs({
|
|
927
|
-
...spec,
|
|
928
|
-
resume: leg.sessionId,
|
|
929
|
-
fork: false
|
|
930
|
-
}, endpoint, allowed);
|
|
931
|
-
}
|
|
932
|
-
} finally {
|
|
933
|
-
await mcp.close();
|
|
934
|
-
}
|
|
935
|
-
};
|
|
936
|
-
return {
|
|
937
|
-
events: events(),
|
|
938
|
-
send: (text) => {
|
|
939
|
-
const line = userLine(text, true);
|
|
940
|
-
if (io === void 0) pending.push(line);
|
|
941
|
-
else io.write(line);
|
|
942
|
-
},
|
|
943
|
-
interrupt: () => {
|
|
944
|
-
ended.abort();
|
|
945
|
-
io?.stop();
|
|
946
|
-
return Promise.resolve();
|
|
947
|
-
}
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
});
|
|
951
|
-
// ../runtime-claude/src/queue.ts
|
|
952
|
-
const createQueue = () => {
|
|
953
|
-
const items = [];
|
|
954
|
-
const waiting = [];
|
|
955
|
-
let closed = false;
|
|
956
|
-
const push = (item) => {
|
|
957
|
-
if (closed) return;
|
|
958
|
-
const next = waiting.shift();
|
|
959
|
-
if (next === void 0) items.push({ value: item });
|
|
960
|
-
else next({
|
|
961
|
-
value: item,
|
|
962
|
-
done: false
|
|
963
|
-
});
|
|
964
|
-
};
|
|
965
|
-
const close = () => {
|
|
966
|
-
closed = true;
|
|
967
|
-
for (const next of waiting.splice(0)) next({
|
|
968
|
-
value: void 0,
|
|
969
|
-
done: true
|
|
970
|
-
});
|
|
971
|
-
};
|
|
972
|
-
const iterator = { next: () => {
|
|
973
|
-
const box = items.shift();
|
|
974
|
-
if (box !== void 0) return Promise.resolve({
|
|
975
|
-
value: box.value,
|
|
976
|
-
done: false
|
|
977
|
-
});
|
|
978
|
-
if (closed) return Promise.resolve({
|
|
979
|
-
value: void 0,
|
|
980
|
-
done: true
|
|
981
|
-
});
|
|
982
|
-
return new Promise((resolve) => {
|
|
983
|
-
waiting.push(resolve);
|
|
984
|
-
});
|
|
985
|
-
} };
|
|
986
|
-
return {
|
|
987
|
-
push,
|
|
988
|
-
close,
|
|
989
|
-
get closed() {
|
|
990
|
-
return closed;
|
|
991
|
-
},
|
|
992
|
-
[Symbol.asyncIterator]: () => iterator
|
|
993
|
-
};
|
|
994
|
-
};
|
|
995
|
-
// ../runtime-claude/src/sdk.ts
|
|
996
|
-
/**
|
|
997
|
-
* @file The SDK driver: runs a session in-process with the Claude Agent SDK,
|
|
998
|
-
* only with an API key or a cloud provider, never a claude.ai login, clean of
|
|
999
|
-
* the machine's Claude setup unless the run asks for it (ADR-065). A node
|
|
1000
|
-
* that can be paused gets a `PreToolUse` callback hook: a short pause holds
|
|
1001
|
-
* the call, a long one defers it and resumes the session when the node
|
|
1002
|
-
* continues (ADR-041).
|
|
1003
|
-
*/
|
|
1004
|
-
/**
|
|
1005
|
-
* The SDK driver runs only with an API key or a cloud provider. It must never
|
|
1006
|
-
* fall back to a claude.ai login: Anthropic does not allow third-party tools to
|
|
1007
|
-
* route requests through subscription credentials.
|
|
1008
|
-
*/
|
|
1009
|
-
const sdkCredentials = (env) => {
|
|
1010
|
-
if (env["CLAUDE_CODE_USE_BEDROCK"] === "1") return "Amazon Bedrock";
|
|
1011
|
-
if (env["CLAUDE_CODE_USE_VERTEX"] === "1") return "Google Vertex AI";
|
|
1012
|
-
if (env["CLAUDE_CODE_USE_FOUNDRY"] === "1") return "Microsoft Foundry";
|
|
1013
|
-
if ((env["ANTHROPIC_API_KEY"] ?? "") !== "") return "ANTHROPIC_API_KEY";
|
|
1014
|
-
return null;
|
|
1015
|
-
};
|
|
1016
|
-
const NO_SDK_CREDENTIALS = "the sdk driver needs ANTHROPIC_API_KEY (or CLAUDE_CODE_USE_BEDROCK=1 / CLAUDE_CODE_USE_VERTEX=1); use --driver cli to run with your own claude login";
|
|
1017
|
-
/** A later message goes in at the next step, even mid-turn. */
|
|
1018
|
-
const userMessage = (text, later = false) => ({
|
|
1019
|
-
type: "user",
|
|
1020
|
-
message: {
|
|
1021
|
-
role: "user",
|
|
1022
|
-
content: text
|
|
1023
|
-
},
|
|
1024
|
-
parent_tool_use_id: null,
|
|
1025
|
-
...later ? { priority: "next" } : {}
|
|
1026
|
-
});
|
|
1027
|
-
const definedEnv = (env) => Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== void 0));
|
|
1028
|
-
/**
|
|
1029
|
-
* The hold hook: waits before each tool call while the node is paused, and
|
|
1030
|
-
* defers the call (`onDefer`) when the pause outlasts the window.
|
|
1031
|
-
*/
|
|
1032
|
-
const holdHook = (hold, onDefer, windowMs) => ({
|
|
1033
|
-
timeout: 600,
|
|
1034
|
-
hooks: [async (_input, _toolUseId, { signal }) => {
|
|
1035
|
-
if (await holdAtTool(hold, signal, windowMs) === "go") return {};
|
|
1036
|
-
onDefer();
|
|
1037
|
-
return { hookSpecificOutput: {
|
|
1038
|
-
hookEventName: "PreToolUse",
|
|
1039
|
-
permissionDecision: "defer",
|
|
1040
|
-
permissionDecisionReason: "Ork paused this node; it goes on from this call when it continues."
|
|
1041
|
-
} };
|
|
1042
|
-
}]
|
|
1043
|
-
});
|
|
1044
|
-
/** The Agent SDK options for a session spec. */
|
|
1045
|
-
const buildSdkOptions = (spec, ports, env, held) => {
|
|
1046
|
-
const { human } = ports;
|
|
1047
|
-
const tools = orkTools({
|
|
1048
|
-
nodeId: spec.nodeId,
|
|
1049
|
-
driver: "sdk",
|
|
1050
|
-
ports
|
|
1051
|
-
});
|
|
1052
|
-
const ork = createSdkMcpServer({
|
|
1053
|
-
name: "ork",
|
|
1054
|
-
version: "0.0.0",
|
|
1055
|
-
tools: tools.map((t) => tool(t.name, t.description, t.shape, (args) => t.call(args)))
|
|
1056
|
-
});
|
|
1057
|
-
const options = {
|
|
1058
|
-
cwd: spec.cwd,
|
|
1059
|
-
env: definedEnv({
|
|
1060
|
-
...env,
|
|
1061
|
-
...driverEnv(spec)
|
|
1062
|
-
}),
|
|
1063
|
-
settingSources: spec.settingSources,
|
|
1064
|
-
systemPrompt: {
|
|
1065
|
-
type: "preset",
|
|
1066
|
-
preset: "claude_code",
|
|
1067
|
-
append: spec.systemAppend,
|
|
1068
|
-
excludeDynamicSections: true
|
|
1069
|
-
},
|
|
1070
|
-
permissionMode: spec.permissionMode,
|
|
1071
|
-
allowedTools: [...spec.tools.allow, ...preapprovedTools(tools)],
|
|
1072
|
-
disallowedTools: spec.tools.deny,
|
|
1073
|
-
mcpServers: { ["ork"]: ork },
|
|
1074
|
-
outputFormat: {
|
|
1075
|
-
type: "json_schema",
|
|
1076
|
-
schema: spec.reportSchema
|
|
1077
|
-
},
|
|
1078
|
-
canUseTool: async (toolName, input) => {
|
|
1079
|
-
const decision = await human.permit({
|
|
1080
|
-
nodeId: spec.nodeId,
|
|
1081
|
-
toolName,
|
|
1082
|
-
input
|
|
1083
|
-
});
|
|
1084
|
-
return decision.allow ? {
|
|
1085
|
-
behavior: "allow",
|
|
1086
|
-
updatedInput: input
|
|
1087
|
-
} : {
|
|
1088
|
-
behavior: "deny",
|
|
1089
|
-
message: decision.message
|
|
1090
|
-
};
|
|
1091
|
-
}
|
|
1092
|
-
};
|
|
1093
|
-
if (spec.tools.only !== void 0) options.tools = spec.tools.only;
|
|
1094
|
-
if (strictMcpOf(spec)) options.strictMcpConfig = true;
|
|
1095
|
-
if (spec.agents !== void 0) options.agents = spec.agents;
|
|
1096
|
-
if (spec.model !== void 0) options.model = spec.model;
|
|
1097
|
-
if (spec.effort !== void 0) options.effort = spec.effort;
|
|
1098
|
-
if (spec.maxTurns !== void 0) options.maxTurns = spec.maxTurns;
|
|
1099
|
-
if (spec.budgetUsd !== void 0) options.maxBudgetUsd = spec.budgetUsd;
|
|
1100
|
-
if (spec.resume !== void 0) options.resume = spec.resume;
|
|
1101
|
-
if (spec.resume !== void 0 && spec.fork === true) options.forkSession = true;
|
|
1102
|
-
if (spec.plugins !== void 0) options.plugins = spec.plugins.map((dir) => ({
|
|
1103
|
-
type: "local",
|
|
1104
|
-
path: dir
|
|
1105
|
-
}));
|
|
1106
|
-
if (spec.settings !== void 0) options.settings = spec.settings;
|
|
1107
|
-
if (ports.hold !== void 0 && held !== void 0) options.hooks = { PreToolUse: [holdHook(ports.hold, held.onDefer, held.windowMs)] };
|
|
1108
|
-
return options;
|
|
1109
|
-
};
|
|
1110
|
-
/** Drives a session with the Claude Agent SDK, in this process. */
|
|
1111
|
-
const createSdkRuntime = (env = process.env, holdWindowMs) => ({
|
|
1112
|
-
driver: "sdk",
|
|
1113
|
-
start: (spec, ports) => {
|
|
1114
|
-
let input = createQueue();
|
|
1115
|
-
input.push(userMessage(spec.prompt));
|
|
1116
|
-
let current;
|
|
1117
|
-
const ended = new AbortController();
|
|
1118
|
-
/** The hold hook deferred a call in the current query. */
|
|
1119
|
-
let deferring = false;
|
|
1120
|
-
const held = {
|
|
1121
|
-
onDefer: () => {
|
|
1122
|
-
deferring = true;
|
|
1123
|
-
},
|
|
1124
|
-
...holdWindowMs === void 0 ? {} : { windowMs: holdWindowMs }
|
|
1125
|
-
};
|
|
1126
|
-
/** One query: a leg of the session, until its result. */
|
|
1127
|
-
const leg = async function* (resume, carried) {
|
|
1128
|
-
deferring = false;
|
|
1129
|
-
const legSpec = resume === void 0 ? spec : {
|
|
1130
|
-
...spec,
|
|
1131
|
-
resume,
|
|
1132
|
-
fork: false
|
|
1133
|
-
};
|
|
1134
|
-
current = query({
|
|
1135
|
-
prompt: input,
|
|
1136
|
-
options: buildSdkOptions(legSpec, ports, env, held)
|
|
1137
|
-
});
|
|
1138
|
-
let outcome = {
|
|
1139
|
-
kind: "failed",
|
|
1140
|
-
reason: "the session ended early"
|
|
1141
|
-
};
|
|
1142
|
-
for await (const message of current) {
|
|
1143
|
-
const { events, ended: end } = legStep(message, {
|
|
1144
|
-
carried,
|
|
1145
|
-
deferred: deferring,
|
|
1146
|
-
driver: "sdk"
|
|
1147
|
-
});
|
|
1148
|
-
yield* events;
|
|
1149
|
-
if (end !== void 0) {
|
|
1150
|
-
input.close();
|
|
1151
|
-
outcome = end;
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
return outcome;
|
|
1155
|
-
};
|
|
1156
|
-
const events = async function* () {
|
|
1157
|
-
if (sdkCredentials(env) === null) {
|
|
1158
|
-
yield driverFailure(NO_SDK_CREDENTIALS);
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
let resume = spec.resume;
|
|
1162
|
-
let spent;
|
|
1163
|
-
try {
|
|
1164
|
-
for (;;) {
|
|
1165
|
-
const result = yield* leg(resume, spent);
|
|
1166
|
-
if (result.kind === "failed") {
|
|
1167
|
-
yield driverFailure(result.reason);
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
if (result.kind === "finished") return;
|
|
1171
|
-
input = createQueue();
|
|
1172
|
-
spent = spent === void 0 ? result.usage : addUsage(spent, result.usage);
|
|
1173
|
-
await ports.hold?.wait("tool", ended.signal);
|
|
1174
|
-
if (ended.signal.aborted) {
|
|
1175
|
-
yield result.last;
|
|
1176
|
-
return;
|
|
1177
|
-
}
|
|
1178
|
-
resume = result.sessionId;
|
|
1179
|
-
}
|
|
1180
|
-
} catch (error) {
|
|
1181
|
-
input.close();
|
|
1182
|
-
yield driverFailure(error instanceof Error ? error.message : String(error));
|
|
1183
|
-
}
|
|
1184
|
-
};
|
|
1185
|
-
return {
|
|
1186
|
-
events: events(),
|
|
1187
|
-
send: (text) => {
|
|
1188
|
-
input.push(userMessage(text, true));
|
|
1189
|
-
},
|
|
1190
|
-
interrupt: async () => {
|
|
1191
|
-
ended.abort();
|
|
1192
|
-
await current?.interrupt();
|
|
1193
|
-
}
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
1196
|
-
});
|
|
1197
|
-
// ../runtime-claude/src/registry.ts
|
|
1198
|
-
const RUNTIME_FACTORIES = {
|
|
1199
|
-
sdk: (env) => createSdkRuntime(env),
|
|
1200
|
-
cli: (env) => createCliRuntime({ env })
|
|
1201
|
-
};
|
|
1202
|
-
/** The runtime for a driver id, with the user's environment. */
|
|
1203
|
-
const createRuntime = (driver, env) => RUNTIME_FACTORIES[driver](env);
|
|
1204
|
-
export { createRuntime };
|