@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.
Files changed (41) hide show
  1. package/README.md +3 -4
  2. package/dist/{__build_info__-D9lSlMFy.mjs → __build_info__-DFF18K8y.mjs} +72 -3
  3. package/dist/bin.mjs +579 -379
  4. package/dist/{clean-CA_Oi2Wl.mjs → clean-DVaEH9e7.mjs} +5 -2
  5. package/dist/{completion-DBGfL-gN.mjs → completion-Dv5kf598.mjs} +6 -6
  6. package/dist/context-CEaKOVmG.mjs +683 -0
  7. package/dist/{decide-CoIJh34e.mjs → decide-3l9q8UtX.mjs} +3 -3
  8. package/dist/{find-rBBk7Jui.mjs → find-DEjjOlTG.mjs} +1 -1
  9. package/dist/format-Cb5_m-Qd.mjs +191 -0
  10. package/dist/{in-a-run-DdAakpRR.mjs → in-a-run-BS9Nc-JW.mjs} +1 -1
  11. package/dist/{limits-BPM9wUlV.mjs → limits-BSwEjQST.mjs} +1 -1
  12. package/dist/{list-Bv5Lxu7z.mjs → list-Qxr6rwsY.mjs} +11 -4
  13. package/dist/{mcp-DM30MFBw.mjs → mcp-CslnG5wf.mjs} +6 -6
  14. package/dist/output-Bqljtmp_.mjs +40 -0
  15. package/dist/{pause-C7lJkSma.mjs → pause-D7OVB5Ac.mjs} +4 -4
  16. package/dist/{relay-BipJYszF.mjs → relay-EceRIebO.mjs} +2 -2
  17. package/dist/{remove-BDs9isB6.mjs → remove-0IQZVWmI.mjs} +14 -7
  18. package/dist/{send-nbkjf3sc.mjs → send-C0uwfUYi.mjs} +3 -3
  19. package/dist/{src-hrj2h0IZ.mjs → src-Df6KOIDC.mjs} +5678 -3245
  20. package/dist/{src-BxUJPGa5.mjs → src-cmmXKHn1.mjs} +265 -203
  21. package/dist/{src-B0TT7DBi.mjs → src-i5k2Y-kf.mjs} +35 -86
  22. package/dist/src-oaJYIddp.mjs +2255 -0
  23. package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
  24. package/dist/ui/assets/app-C8uhHVYp.css +1 -0
  25. package/dist/ui/assets/app-qs8Lofv6.js +75 -0
  26. package/dist/ui/assets/{index-CPx74lFf.js → index-DV1ZaOk-.js} +3 -3
  27. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  28. package/dist/ui/index.html +2 -2
  29. package/dist/{ui-B98FSboa.mjs → ui-CBkHJdqk.mjs} +4 -4
  30. package/dist/{usage-EW7DI6Z7.mjs → usage-Cd8pEYyK.mjs} +5 -5
  31. package/dist/{watch-BAkSGaOp.mjs → watch-DW5R2OPO.mjs} +6 -6
  32. package/dist/{workflows-vsEohqa7.mjs → workflows-BT1gDRIV.mjs} +4 -3
  33. package/dist/{worktrees-Ahj9rdWK.mjs → worktrees-g1MQMGie.mjs} +78 -19
  34. package/package.json +2 -4
  35. package/dist/context-6x3kXREO.mjs +0 -274
  36. package/dist/format-zMmhqyLE.mjs +0 -427
  37. package/dist/output-TV_j7Ni0.mjs +0 -950
  38. package/dist/src-B9v0lDc6.mjs +0 -1204
  39. package/dist/ui/assets/app-BNT0SuIz.css +0 -1
  40. package/dist/ui/assets/app-CmkynZyW.js +0 -67
  41. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
@@ -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 };