@krapht/ork 0.0.0-stage → 0.2.0

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.
@@ -0,0 +1,853 @@
1
+ import { S as FEEDBACK_TARGETS, b as FEEDBACK_KINDS, f as parseReport, x as FEEDBACK_SEVERITIES } from "./src-CJ_zF0Zj.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
+ /** The name Claude Code gives an Ork tool: `mcp__ork__<name>`. */
9
+ const toolId = (name) => `mcp__ork__${name}`;
10
+ const text = (value) => ({ content: [{
11
+ type: "text",
12
+ text: value
13
+ }] });
14
+ const defineTool = (definition) => {
15
+ const schema = z.object(definition.shape);
16
+ return {
17
+ name: definition.name,
18
+ bind: (context) => definition.available?.(context) ?? true ? {
19
+ name: definition.name,
20
+ description: definition.description,
21
+ shape: definition.shape,
22
+ schema,
23
+ preapproved: definition.preapproved ?? true,
24
+ call: (args) => definition.run(context, schema.parse(args))
25
+ } : null
26
+ };
27
+ };
28
+ // ../runtime-claude/src/mcp-http.ts
29
+ /**
30
+ * @file A minimal MCP server over the streamable HTTP transport, for the
31
+ * `claude` CLI driver. It answers with plain JSON (no SSE stream), which the
32
+ * transport allows, and implements only what Ork needs: `initialize`, `ping`,
33
+ * `tools/list` and `tools/call`.
34
+ */
35
+ const PATH = "/mcp";
36
+ const MAX_BODY = 1048576;
37
+ const MCP_PROTOCOL_FALLBACK = "2025-06-18";
38
+ const JsonRpcSchema = z.object({
39
+ jsonrpc: z.literal("2.0"),
40
+ id: z.union([z.string(), z.number()]).optional(),
41
+ method: z.string(),
42
+ params: z.record(z.string(), z.unknown()).optional()
43
+ });
44
+ /** Handles one JSON-RPC request; `null` for a notification (no reply). */
45
+ const handleRpc = async (tools, message) => {
46
+ const parsed = JsonRpcSchema.safeParse(message);
47
+ if (!parsed.success) return { error: {
48
+ code: -32600,
49
+ message: "invalid request"
50
+ } };
51
+ const { id, method, params } = parsed.data;
52
+ if (id === void 0) return null;
53
+ switch (method) {
54
+ case "initialize": {
55
+ const requested = params?.["protocolVersion"];
56
+ return { result: {
57
+ protocolVersion: typeof requested === "string" ? requested : MCP_PROTOCOL_FALLBACK,
58
+ capabilities: { tools: {} },
59
+ serverInfo: {
60
+ name: "ork",
61
+ version: "0.0.0"
62
+ }
63
+ } };
64
+ }
65
+ case "ping": return { result: {} };
66
+ case "tools/list": return { result: { tools: tools.map((t) => ({
67
+ name: t.name,
68
+ description: t.description,
69
+ inputSchema: z.toJSONSchema(t.schema)
70
+ })) } };
71
+ case "tools/call": {
72
+ const tool = tools.find((t) => t.name === params?.["name"]);
73
+ if (tool === void 0) return { error: {
74
+ code: -32602,
75
+ message: "unknown tool"
76
+ } };
77
+ try {
78
+ return { result: await tool.call(params?.["arguments"] ?? {}) };
79
+ } catch (error) {
80
+ return { result: {
81
+ content: [{
82
+ type: "text",
83
+ text: error instanceof Error ? error.message : "tool failed"
84
+ }],
85
+ isError: true
86
+ } };
87
+ }
88
+ }
89
+ default: return { error: {
90
+ code: -32601,
91
+ message: `unknown method ${method}`
92
+ } };
93
+ }
94
+ };
95
+ const authorized = (req, token) => {
96
+ const expected = Buffer.from(`Bearer ${token}`);
97
+ const given = Buffer.from(req.headers.authorization ?? "");
98
+ return given.length === expected.length && timingSafeEqual(given, expected);
99
+ };
100
+ const readBody = async (req) => {
101
+ const chunks = [];
102
+ let size = 0;
103
+ for await (const chunk of req) {
104
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
105
+ size += buffer.length;
106
+ if (size > MAX_BODY) return null;
107
+ chunks.push(buffer);
108
+ }
109
+ return Buffer.concat(chunks).toString("utf8");
110
+ };
111
+ const respond = (res, status, body) => {
112
+ if (body === void 0) {
113
+ res.writeHead(status).end();
114
+ return;
115
+ }
116
+ res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(body));
117
+ };
118
+ const serve = async (req, res, tools, token) => {
119
+ if (!authorized(req, token)) {
120
+ respond(res, 401);
121
+ return;
122
+ }
123
+ if (req.url !== PATH) {
124
+ respond(res, 404);
125
+ return;
126
+ }
127
+ if (req.method !== "POST") {
128
+ respond(res, 405);
129
+ return;
130
+ }
131
+ const body = await readBody(req);
132
+ if (body === null) {
133
+ respond(res, 413);
134
+ return;
135
+ }
136
+ let message;
137
+ try {
138
+ message = JSON.parse(body);
139
+ } catch {
140
+ respond(res, 400);
141
+ return;
142
+ }
143
+ const id = message !== null && typeof message === "object" && "id" in message ? message.id : null;
144
+ const reply = await handleRpc(tools, message);
145
+ if (reply === null) {
146
+ respond(res, 202);
147
+ return;
148
+ }
149
+ respond(res, 200, {
150
+ jsonrpc: "2.0",
151
+ id,
152
+ ...reply
153
+ });
154
+ };
155
+ /**
156
+ * Serves Ork's tools to one `claude` CLI session: on 127.0.0.1 only, on a
157
+ * random port, behind a per-session bearer token.
158
+ */
159
+ const startOrkMcpHttp = async (tools) => {
160
+ const token = randomBytes(24).toString("hex");
161
+ const http = createServer((req, res) => {
162
+ serve(req, res, tools, token).catch(() => {
163
+ if (!res.headersSent) respond(res, 500);
164
+ });
165
+ });
166
+ await new Promise((resolve) => {
167
+ http.listen(0, "127.0.0.1", resolve);
168
+ });
169
+ const address = http.address();
170
+ return {
171
+ url: `http://127.0.0.1:${typeof address === "object" && address !== null ? address.port : 0}${PATH}`,
172
+ token,
173
+ close: () => new Promise((resolve) => {
174
+ http.closeAllConnections();
175
+ http.close(() => {
176
+ resolve();
177
+ });
178
+ })
179
+ };
180
+ };
181
+ // ../runtime-claude/src/normalize.ts
182
+ /**
183
+ * @file Both drivers emit the same message JSON (the Agent SDK's messages are
184
+ * the CLI's `stream-json` lines), so one normalizer serves both.
185
+ */
186
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
187
+ const get = (value, key) => isObject(value) ? value[key] : void 0;
188
+ const num = (value) => typeof value === "number" ? value : 0;
189
+ const str = (value) => typeof value === "string" ? value : "";
190
+ const blocks = (message) => {
191
+ const content = get(get(message, "message"), "content");
192
+ return Array.isArray(content) ? content.filter((item) => isObject(item)) : [];
193
+ };
194
+ const usageOf = (result) => {
195
+ const usage = get(result, "usage");
196
+ return {
197
+ costUsd: num(result["total_cost_usd"]),
198
+ inputTokens: num(get(usage, "input_tokens")),
199
+ outputTokens: num(get(usage, "output_tokens")),
200
+ cacheReadTokens: num(get(usage, "cache_read_input_tokens")),
201
+ cacheWriteTokens: num(get(usage, "cache_creation_input_tokens")),
202
+ turns: num(result["num_turns"]),
203
+ durationMs: num(result["duration_ms"])
204
+ };
205
+ };
206
+ /** The outcome a `result` message describes. */
207
+ const outcomeOf = (result) => {
208
+ const errors = result["errors"];
209
+ return {
210
+ subtype: str(result["subtype"]) || "unknown",
211
+ sessionId: str(result["session_id"]),
212
+ report: parseReport(result["structured_output"]),
213
+ text: str(result["result"]),
214
+ usage: usageOf(result),
215
+ errors: Array.isArray(errors) ? errors.filter((e) => typeof e === "string") : []
216
+ };
217
+ };
218
+ /** Maps one driver message to zero or more normalized events. */
219
+ const normalize = (message) => {
220
+ if (!isObject(message)) return [];
221
+ switch (message["type"]) {
222
+ case "system": {
223
+ if (message["subtype"] !== "init") return [];
224
+ const started = {
225
+ type: "session.started",
226
+ sessionId: str(message["session_id"]),
227
+ model: str(message["model"])
228
+ };
229
+ if (str(message["cwd"]) !== "") started.cwd = str(message["cwd"]);
230
+ return [started];
231
+ }
232
+ case "assistant": return blocks(message).flatMap((block) => {
233
+ if (block["type"] === "text" && str(block["text"]).trim() !== "") return [{
234
+ type: "assistant.text",
235
+ text: str(block["text"])
236
+ }];
237
+ if (block["type"] === "tool_use") {
238
+ const input = block["input"];
239
+ return [{
240
+ type: "tool.called",
241
+ id: str(block["id"]),
242
+ name: str(block["name"]),
243
+ input: isObject(input) ? input : {}
244
+ }];
245
+ }
246
+ return [];
247
+ });
248
+ case "user": return blocks(message).filter((block) => block["type"] === "tool_result").map((block) => ({
249
+ type: "tool.result",
250
+ id: str(block["tool_use_id"]),
251
+ isError: block["is_error"] === true
252
+ }));
253
+ case "result": return [{
254
+ type: "session.finished",
255
+ outcome: outcomeOf(message)
256
+ }];
257
+ default: return [];
258
+ }
259
+ };
260
+ /** Marks a session's start with the driver that runs it. */
261
+ const withDriver = (event, driver) => event.type === "session.started" ? {
262
+ ...event,
263
+ driver
264
+ } : event;
265
+ /** A finished event for a session the driver could not run. */
266
+ const driverFailure = (message) => ({
267
+ type: "session.finished",
268
+ outcome: {
269
+ subtype: "error_driver",
270
+ sessionId: "",
271
+ report: null,
272
+ text: "",
273
+ usage: {
274
+ costUsd: 0,
275
+ inputTokens: 0,
276
+ outputTokens: 0,
277
+ cacheReadTokens: 0,
278
+ cacheWriteTokens: 0,
279
+ turns: 0,
280
+ durationMs: 0
281
+ },
282
+ errors: [message]
283
+ }
284
+ });
285
+ // ../runtime-claude/src/tools/ask-user.ts
286
+ const askUserTool = defineTool({
287
+ name: "ask_user",
288
+ 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.",
289
+ shape: {
290
+ question: z.string().describe("One clear question."),
291
+ options: z.array(z.object({
292
+ id: z.string().describe("A short id, e.g. 'keep-api'."),
293
+ label: z.string().optional(),
294
+ description: z.string().optional().describe("What choosing it means.")
295
+ })).min(2).max(4),
296
+ recommended: z.string().optional().describe("The id you recommend."),
297
+ context: z.string().optional().describe("What the user needs to know to decide, in a few sentences.")
298
+ },
299
+ run: async ({ nodeId, ports }, args) => {
300
+ const question = {
301
+ nodeId,
302
+ question: args.question,
303
+ options: args.options.map((o) => {
304
+ const choice = { id: o.id };
305
+ if (o.label !== void 0) choice.label = o.label;
306
+ if (o.description !== void 0) choice.description = o.description;
307
+ return choice;
308
+ })
309
+ };
310
+ if (args.recommended !== void 0) question.recommended = args.recommended;
311
+ if (args.context !== void 0) question.context = args.context;
312
+ const answer = await ports.human.ask(question);
313
+ return text(`The user chose: ${answer}`);
314
+ }
315
+ });
316
+ // ../runtime-claude/src/tools/claims.ts
317
+ /**
318
+ * @file `claim` and `release`: a session's side of the run's claims. A node
319
+ * claims a file, folder or other resource before changing it, so nodes
320
+ * running at the same time do not change the same thing; a refused claim says
321
+ * who holds it. Only sessions that may write while another does get them.
322
+ */
323
+ const contended = ({ ports }) => ports.claims?.contended() === true;
324
+ 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'.");
325
+ const claimTool = defineTool({
326
+ name: "claim",
327
+ 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.",
328
+ shape: { resource },
329
+ available: contended,
330
+ run: ({ ports }, args) => {
331
+ const claimed = ports.claims?.claim(args.resource);
332
+ if (claimed === void 0) return Promise.resolve(text("No claims."));
333
+ if (!claimed.ok) return Promise.resolve(text(`Not claimed: ${claimed.error}.`));
334
+ return Promise.resolve(text(claimed.already ? `You already hold ${claimed.resource}.` : `Claimed ${claimed.resource}.`));
335
+ }
336
+ });
337
+ const releaseTool = defineTool({
338
+ name: "release",
339
+ description: "Release a claim you are done with, so other nodes can take it. Ork releases your remaining claims when you finish.",
340
+ shape: { resource },
341
+ available: contended,
342
+ run: ({ ports }, args) => {
343
+ const released = ports.claims?.release(args.resource);
344
+ if (released === void 0) return Promise.resolve(text("No claims."));
345
+ return Promise.resolve(text(released.ok ? `Released ${released.resource}.` : `Not released: ${released.error}.`));
346
+ }
347
+ });
348
+ // ../runtime-claude/src/tools/delegate.ts
349
+ 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.");
350
+ /** What the leader reads back: each report without the usage details. */
351
+ const reportLine = (report) => {
352
+ const { usage, ...output } = report.output;
353
+ const cost = usage !== null && typeof usage === "object" && "costUsd" in usage ? usage.costUsd : void 0;
354
+ return {
355
+ child: report.child,
356
+ ok: report.ok,
357
+ ...output,
358
+ ...cost === void 0 ? {} : { costUsd: cost },
359
+ ...report.ok || report.error === void 0 || report.error === output["summary"] ? {} : { error: report.error }
360
+ };
361
+ };
362
+ const delegationText = (delegation) => text(JSON.stringify({
363
+ reports: delegation.reports.map(reportLine),
364
+ ...delegation.notes.length > 0 ? { notes: delegation.notes } : {}
365
+ }));
366
+ const delegateTool = defineTool({
367
+ name: "delegate",
368
+ 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.",
369
+ shape: { tasks: z.array(z.object({
370
+ child: z.string().describe("The id of a node you own."),
371
+ brief: BRIEF
372
+ })).min(1) },
373
+ available: ({ ports }) => (ports.ownership?.children.length ?? 0) > 0,
374
+ run: async ({ ports }, args) => {
375
+ if (ports.ownership === void 0) return text("You own no nodes.");
376
+ return delegationText(await ports.ownership.delegate(args.tasks));
377
+ }
378
+ });
379
+ const spawnWorkerTool = defineTool({
380
+ name: "spawn_worker",
381
+ 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.",
382
+ shape: { tasks: z.array(z.object({
383
+ template: z.string().describe("The id of a child worker you own."),
384
+ brief: BRIEF
385
+ })).min(1) },
386
+ available: ({ ports }) => ports.ownership !== void 0 && ports.ownership.templates.length > 0 && ports.ownership.workersLeft() > 0,
387
+ run: async ({ ports }, args) => {
388
+ if (ports.ownership === void 0) return text("You own no workers.");
389
+ return delegationText(await ports.ownership.spawn(args.tasks));
390
+ }
391
+ });
392
+ // ../runtime-claude/src/tools/feedback.ts
393
+ /**
394
+ * @file `feedback`: a session's way to report what got in its way, kept on the
395
+ * user's machine (specs/feedback.md). Every session inside a run gets it; Ork
396
+ * caps reports per session and drops a run's duplicates.
397
+ */
398
+ const feedbackTool = defineTool({
399
+ name: "feedback",
400
+ 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.",
401
+ shape: {
402
+ kind: z.enum(FEEDBACK_KINDS),
403
+ 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."),
404
+ severity: z.enum(FEEDBACK_SEVERITIES).optional(),
405
+ title: z.string().min(1).describe("One line."),
406
+ body: z.string().optional().describe("A few sentences: what happened and what would have helped."),
407
+ evidence: z.array(z.string()).optional().describe("File paths, tool names; never pasted file contents.")
408
+ },
409
+ available: ({ ports }) => ports.feedback !== void 0,
410
+ run: ({ ports }, args) => {
411
+ const draft = {
412
+ kind: args.kind,
413
+ title: args.title
414
+ };
415
+ if (args.target !== void 0) draft.target = args.target;
416
+ if (args.severity !== void 0) draft.severity = args.severity;
417
+ if (args.body !== void 0) draft.body = args.body;
418
+ if (args.evidence !== void 0) draft.evidence = args.evidence;
419
+ const sent = ports.feedback?.report(draft);
420
+ if (sent === void 0) return Promise.resolve(text("No feedback here."));
421
+ return Promise.resolve(text(sent.ok ? "Recorded. Thank you; carry on with your task." : `Not recorded: ${sent.error}.`));
422
+ }
423
+ });
424
+ // ../runtime-claude/src/tools/messages.ts
425
+ /**
426
+ * @file `send_message`, `read_inbox` and `answer`: a session's side of the
427
+ * run's message bus. A message lands in the recipient's inbox and a live
428
+ * session gets a one-line notice; `answer` replies to a question Ork asked an
429
+ * owner about a node it owns. Only sessions inside a run get them.
430
+ */
431
+ /** Peers are mutual: a node no one may write to has no inbox to read. */
432
+ const hasPeers = ({ ports }) => (ports.mailbox?.peers().length ?? 0) > 0;
433
+ const sendMessageTool = defineTool({
434
+ name: "send_message",
435
+ description: "Send a short message to another node in this run: your owner, a node you own, or one you share a declared channel with. 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.",
436
+ shape: {
437
+ to: z.string().describe("The node's id."),
438
+ body: z.string().min(1).describe("Self-contained: the recipient does not see your work.")
439
+ },
440
+ available: hasPeers,
441
+ run: ({ ports }, args) => {
442
+ const sent = ports.mailbox?.send(args.to, args.body);
443
+ if (sent === void 0) return Promise.resolve(text("No message bus."));
444
+ if (!sent.ok) return Promise.resolve(text(`Not sent: ${sent.error}.`));
445
+ 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 when it next starts.`));
446
+ }
447
+ });
448
+ const readInboxTool = defineTool({
449
+ name: "read_inbox",
450
+ description: "Read your unread messages from other nodes in this run, oldest first. Ork tells you when one arrives.",
451
+ shape: {},
452
+ available: hasPeers,
453
+ run: ({ ports }) => {
454
+ const unread = ports.mailbox?.read() ?? [];
455
+ return Promise.resolve(text(unread.length === 0 ? "No unread messages." : JSON.stringify({ messages: unread.map((m) => ({
456
+ from: m.from,
457
+ body: m.body
458
+ })) })));
459
+ }
460
+ });
461
+ const answerTool = defineTool({
462
+ name: "answer",
463
+ 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.",
464
+ shape: {
465
+ question: z.string().describe("The question's id, e.g. 'q1'."),
466
+ choice: z.string().describe("One of its options.")
467
+ },
468
+ available: ({ ports }) => ports.mailbox !== void 0 && ports.ownership !== void 0,
469
+ run: ({ ports }, args) => {
470
+ const answered = ports.mailbox?.answer(args.question, args.choice);
471
+ if (answered === void 0) return Promise.resolve(text("No questions."));
472
+ return Promise.resolve(text(answered.ok ? `Answered ${args.question}: ${args.choice}.` : `Not answered: ${answered.error}.`));
473
+ }
474
+ });
475
+ // ../runtime-claude/src/tools/registry.ts
476
+ /**
477
+ * @file The registry of Ork's MCP tools. Both drivers serve what it binds for a
478
+ * session; a new tool is one file here and one line in `ORK_TOOLS`.
479
+ */
480
+ const ORK_TOOLS = [
481
+ askUserTool,
482
+ defineTool({
483
+ name: "permission",
484
+ description: "Ork's permission prompt for the claude CLI.",
485
+ shape: {
486
+ tool_name: z.string(),
487
+ input: z.record(z.string(), z.unknown()),
488
+ tool_use_id: z.string().optional()
489
+ },
490
+ preapproved: false,
491
+ available: ({ driver }) => driver === "cli",
492
+ run: async ({ nodeId, ports }, args) => {
493
+ const decision = await ports.human.permit({
494
+ nodeId,
495
+ toolName: args.tool_name,
496
+ input: args.input
497
+ });
498
+ return text(JSON.stringify(decision.allow ? {
499
+ behavior: "allow",
500
+ updatedInput: args.input
501
+ } : {
502
+ behavior: "deny",
503
+ message: decision.message
504
+ }));
505
+ }
506
+ }),
507
+ delegateTool,
508
+ spawnWorkerTool,
509
+ sendMessageTool,
510
+ readInboxTool,
511
+ answerTool,
512
+ claimTool,
513
+ releaseTool,
514
+ feedbackTool
515
+ ];
516
+ /** The tools one session gets. */
517
+ const orkTools = (context) => ORK_TOOLS.flatMap((tool) => tool.bind(context) ?? []);
518
+ /** Their Claude Code names, for the session's allowed tools. */
519
+ const preapprovedTools = (tools) => tools.filter((t) => t.preapproved).map((t) => toolId(t.name));
520
+ // ../runtime-claude/src/cli.ts
521
+ /**
522
+ * @file The CLI driver: runs a session through the user's own `claude -p` in
523
+ * stream-json mode, with Ork's tools served by a local MCP server.
524
+ */
525
+ const STDERR_LINES = 20;
526
+ /**
527
+ * Per-request timeout for Ork's MCP server, in ms. The CLI otherwise gives an
528
+ * HTTP server's request 60 seconds and drops a call silent for 5 minutes, and
529
+ * `delegate` waits for whole child sessions (the per-server `timeout` raises
530
+ * both; `MCP_TOOL_TIMEOUT` still caps it).
531
+ */
532
+ const ORK_TOOL_TIMEOUT_MS = 864e5;
533
+ /** A later message goes in at the next step, even mid-turn. */
534
+ const userLine = (text, later = false) => `${JSON.stringify({
535
+ type: "user",
536
+ message: {
537
+ role: "user",
538
+ content: text
539
+ },
540
+ parent_tool_use_id: null,
541
+ ...later ? { priority: "next" } : {}
542
+ })}\n`;
543
+ /**
544
+ * The `claude -p` arguments for a session spec. The brief goes on stdin;
545
+ * `allowed` are the Ork tools that need no permission prompt.
546
+ */
547
+ const buildCliArgs = (spec, mcp, allowed) => {
548
+ const mcpConfig = { mcpServers: { ["ork"]: {
549
+ type: "http",
550
+ url: mcp.url,
551
+ headers: { Authorization: `Bearer ${mcp.token}` },
552
+ timeout: ORK_TOOL_TIMEOUT_MS
553
+ } } };
554
+ const args = [
555
+ "-p",
556
+ "--input-format",
557
+ "stream-json",
558
+ "--output-format",
559
+ "stream-json",
560
+ "--verbose",
561
+ "--setting-sources",
562
+ spec.settingSources.join(","),
563
+ "--append-system-prompt",
564
+ spec.systemAppend,
565
+ "--permission-mode",
566
+ spec.permissionMode,
567
+ "--json-schema",
568
+ JSON.stringify(spec.reportSchema),
569
+ "--mcp-config",
570
+ JSON.stringify(mcpConfig),
571
+ "--permission-prompt-tool",
572
+ toolId("permission"),
573
+ "--allowedTools",
574
+ [...spec.tools.allow, ...allowed].join(",")
575
+ ];
576
+ if (spec.tools.only !== void 0) args.push("--tools", spec.tools.only.join(","));
577
+ if (spec.agents !== void 0) args.push("--agents", JSON.stringify(spec.agents));
578
+ if (spec.tools.deny.length > 0) args.push("--disallowedTools", spec.tools.deny.join(","));
579
+ if (spec.model !== void 0) args.push("--model", spec.model);
580
+ if (spec.effort !== void 0) args.push("--effort", spec.effort);
581
+ if (spec.maxTurns !== void 0) args.push("--max-turns", String(spec.maxTurns));
582
+ if (spec.budgetUsd !== void 0) args.push("--max-budget-usd", String(spec.budgetUsd));
583
+ if (spec.resume !== void 0) args.push("--resume", spec.resume);
584
+ for (const dir of spec.plugins ?? []) args.push("--plugin-dir", dir);
585
+ if (spec.settings !== void 0) args.push("--settings", JSON.stringify(spec.settings));
586
+ return args;
587
+ };
588
+ const parseLine = (line) => {
589
+ try {
590
+ return JSON.parse(line);
591
+ } catch {
592
+ return;
593
+ }
594
+ };
595
+ /**
596
+ * Drives a session with the user's own `claude` CLI in print mode, logged in by
597
+ * the user. Ork never reads or passes its credentials.
598
+ */
599
+ const createCliRuntime = (options = {}) => ({
600
+ driver: "cli",
601
+ start: (spec, ports) => {
602
+ const pending = [userLine(spec.prompt)];
603
+ let write;
604
+ let stop;
605
+ const events = async function* () {
606
+ const tools = orkTools({
607
+ nodeId: spec.nodeId,
608
+ driver: "cli",
609
+ ports
610
+ });
611
+ const mcp = await startOrkMcpHttp(tools);
612
+ const args = buildCliArgs(spec, mcp, preapprovedTools(tools));
613
+ const child = spawn(options.bin ?? "claude", args, {
614
+ cwd: spec.cwd,
615
+ env: { ...options.env ?? process.env },
616
+ stdio: [
617
+ "pipe",
618
+ "pipe",
619
+ "pipe"
620
+ ]
621
+ });
622
+ const exited = new Promise((resolve) => {
623
+ child.on("close", (code) => {
624
+ resolve(code);
625
+ });
626
+ });
627
+ const spawnError = new Promise((resolve) => {
628
+ child.on("error", resolve);
629
+ });
630
+ const stderr = [];
631
+ child.stderr.setEncoding("utf8");
632
+ child.stderr.on("data", (chunk) => {
633
+ stderr.push(...chunk.split("\n").filter((l) => l.trim() !== ""));
634
+ stderr.splice(0, Math.max(0, stderr.length - STDERR_LINES));
635
+ });
636
+ write = (line) => {
637
+ if (child.stdin.writable) child.stdin.write(line);
638
+ };
639
+ stop = () => {
640
+ child.kill("SIGINT");
641
+ };
642
+ for (const line of pending.splice(0)) write(line);
643
+ let finished = false;
644
+ try {
645
+ const lines = createInterface({ input: child.stdout });
646
+ for await (const line of lines) for (const event of normalize(parseLine(line))) {
647
+ if (event.type === "session.finished") {
648
+ finished = true;
649
+ child.stdin.end();
650
+ }
651
+ yield withDriver(event, "cli");
652
+ }
653
+ const code = await Promise.race([exited, spawnError.then((error) => error)]);
654
+ if (!finished) {
655
+ const reason = code instanceof Error ? `could not start ${options.bin ?? "claude"}: ${code.message}` : stderr.at(-1) ?? `claude exited with code ${String(code)}`;
656
+ yield driverFailure(reason);
657
+ }
658
+ } finally {
659
+ if (child.exitCode === null) child.kill();
660
+ await mcp.close();
661
+ }
662
+ };
663
+ return {
664
+ events: events(),
665
+ send: (text) => {
666
+ const line = userLine(text, true);
667
+ if (write === void 0) pending.push(line);
668
+ else write(line);
669
+ },
670
+ interrupt: () => {
671
+ stop?.();
672
+ return Promise.resolve();
673
+ }
674
+ };
675
+ }
676
+ });
677
+ // ../runtime-claude/src/queue.ts
678
+ const createQueue = () => {
679
+ const items = [];
680
+ const waiting = [];
681
+ let closed = false;
682
+ const push = (item) => {
683
+ if (closed) return;
684
+ const next = waiting.shift();
685
+ if (next === void 0) items.push({ value: item });
686
+ else next({
687
+ value: item,
688
+ done: false
689
+ });
690
+ };
691
+ const close = () => {
692
+ closed = true;
693
+ for (const next of waiting.splice(0)) next({
694
+ value: void 0,
695
+ done: true
696
+ });
697
+ };
698
+ const iterator = { next: () => {
699
+ const box = items.shift();
700
+ if (box !== void 0) return Promise.resolve({
701
+ value: box.value,
702
+ done: false
703
+ });
704
+ if (closed) return Promise.resolve({
705
+ value: void 0,
706
+ done: true
707
+ });
708
+ return new Promise((resolve) => {
709
+ waiting.push(resolve);
710
+ });
711
+ } };
712
+ return {
713
+ push,
714
+ close,
715
+ get closed() {
716
+ return closed;
717
+ },
718
+ [Symbol.asyncIterator]: () => iterator
719
+ };
720
+ };
721
+ // ../runtime-claude/src/sdk.ts
722
+ /**
723
+ * @file The SDK driver: runs a session in-process with the Claude Agent SDK,
724
+ * only with an API key or a cloud provider, never a claude.ai login.
725
+ */
726
+ /**
727
+ * The SDK driver runs only with an API key or a cloud provider. It must never
728
+ * fall back to a claude.ai login: Anthropic does not allow third-party tools to
729
+ * route requests through subscription credentials.
730
+ */
731
+ const sdkCredentials = (env) => {
732
+ if (env["CLAUDE_CODE_USE_BEDROCK"] === "1") return "Amazon Bedrock";
733
+ if (env["CLAUDE_CODE_USE_VERTEX"] === "1") return "Google Vertex AI";
734
+ if (env["CLAUDE_CODE_USE_FOUNDRY"] === "1") return "Microsoft Foundry";
735
+ if ((env["ANTHROPIC_API_KEY"] ?? "") !== "") return "ANTHROPIC_API_KEY";
736
+ return null;
737
+ };
738
+ 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";
739
+ /** A later message goes in at the next step, even mid-turn. */
740
+ const userMessage = (text, later = false) => ({
741
+ type: "user",
742
+ message: {
743
+ role: "user",
744
+ content: text
745
+ },
746
+ parent_tool_use_id: null,
747
+ ...later ? { priority: "next" } : {}
748
+ });
749
+ const definedEnv = (env) => Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== void 0));
750
+ /** The Agent SDK options for a session spec. */
751
+ const buildSdkOptions = (spec, ports, env) => {
752
+ const { human } = ports;
753
+ const tools = orkTools({
754
+ nodeId: spec.nodeId,
755
+ driver: "sdk",
756
+ ports
757
+ });
758
+ const ork = createSdkMcpServer({
759
+ name: "ork",
760
+ version: "0.0.0",
761
+ tools: tools.map((t) => tool(t.name, t.description, t.shape, (args) => t.call(args)))
762
+ });
763
+ const options = {
764
+ cwd: spec.cwd,
765
+ env: definedEnv(env),
766
+ settingSources: spec.settingSources,
767
+ systemPrompt: {
768
+ type: "preset",
769
+ preset: "claude_code",
770
+ append: spec.systemAppend
771
+ },
772
+ permissionMode: spec.permissionMode,
773
+ allowedTools: [...spec.tools.allow, ...preapprovedTools(tools)],
774
+ disallowedTools: spec.tools.deny,
775
+ mcpServers: { ["ork"]: ork },
776
+ outputFormat: {
777
+ type: "json_schema",
778
+ schema: spec.reportSchema
779
+ },
780
+ canUseTool: async (toolName, input) => {
781
+ const decision = await human.permit({
782
+ nodeId: spec.nodeId,
783
+ toolName,
784
+ input
785
+ });
786
+ return decision.allow ? {
787
+ behavior: "allow",
788
+ updatedInput: input
789
+ } : {
790
+ behavior: "deny",
791
+ message: decision.message
792
+ };
793
+ }
794
+ };
795
+ if (spec.tools.only !== void 0) options.tools = spec.tools.only;
796
+ if (spec.agents !== void 0) options.agents = spec.agents;
797
+ if (spec.model !== void 0) options.model = spec.model;
798
+ if (spec.effort !== void 0) options.effort = spec.effort;
799
+ if (spec.maxTurns !== void 0) options.maxTurns = spec.maxTurns;
800
+ if (spec.budgetUsd !== void 0) options.maxBudgetUsd = spec.budgetUsd;
801
+ if (spec.resume !== void 0) options.resume = spec.resume;
802
+ if (spec.plugins !== void 0) options.plugins = spec.plugins.map((dir) => ({
803
+ type: "local",
804
+ path: dir
805
+ }));
806
+ if (spec.settings !== void 0) options.settings = spec.settings;
807
+ return options;
808
+ };
809
+ /** Drives a session with the Claude Agent SDK, in this process. */
810
+ const createSdkRuntime = (env = process.env) => ({
811
+ driver: "sdk",
812
+ start: (spec, ports) => {
813
+ const input = createQueue();
814
+ input.push(userMessage(spec.prompt));
815
+ let current;
816
+ const events = async function* () {
817
+ if (sdkCredentials(env) === null) {
818
+ yield driverFailure(NO_SDK_CREDENTIALS);
819
+ return;
820
+ }
821
+ try {
822
+ current = query({
823
+ prompt: input,
824
+ options: buildSdkOptions(spec, ports, env)
825
+ });
826
+ for await (const message of current) for (const event of normalize(message)) {
827
+ if (event.type === "session.finished") input.close();
828
+ yield withDriver(event, "sdk");
829
+ }
830
+ } catch (error) {
831
+ input.close();
832
+ yield driverFailure(error instanceof Error ? error.message : String(error));
833
+ }
834
+ };
835
+ return {
836
+ events: events(),
837
+ send: (text) => {
838
+ input.push(userMessage(text, true));
839
+ },
840
+ interrupt: async () => {
841
+ await current?.interrupt();
842
+ }
843
+ };
844
+ }
845
+ });
846
+ // ../runtime-claude/src/registry.ts
847
+ const RUNTIME_FACTORIES = {
848
+ sdk: (env) => createSdkRuntime(env),
849
+ cli: (env) => createCliRuntime({ env })
850
+ };
851
+ /** The runtime for a driver id, with the user's environment. */
852
+ const createRuntime = (driver, env) => RUNTIME_FACTORIES[driver](env);
853
+ export { createRuntime };