@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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Bt as mcpNotText, Ft as declaredOutputOf, Hn as beforeOf, Mt as calledWorkflows, Nt as envReadsOf, Un as programsRunBy, Vt as serversIn, ar as timeoutMs, cr as integrationFor, ct as planNodeSession, en as evaluate, er as isClaudeNode, ft as reportSchemaFor, jt as declaredProblem, kt as runScope, nn as renderTemplate, or as INTEGRATIONS, qn as markValue, s as planReason, sr as TOOL_PREFIX, tn as parseTemplate, tr as isToolNode, ut as sessionDriver, yt as heldTimeout, zt as MCP_FILE } from "./src-Df6KOIDC.mjs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { accessSync, constants,
|
|
3
|
+
import { accessSync, constants, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
6
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
@@ -35,7 +35,7 @@ const callExecutor = (context) => {
|
|
|
35
35
|
output: { status: "fail" },
|
|
36
36
|
error: "This run cannot call workflows."
|
|
37
37
|
});
|
|
38
|
-
const scope = runScope(context.inputs, context.outputs, context.runId);
|
|
38
|
+
const scope = runScope(context.inputs, context.outputs, context.runId, context.item);
|
|
39
39
|
const render = (text) => {
|
|
40
40
|
const template = parseTemplate(text);
|
|
41
41
|
return template.ok ? renderTemplate(template.parts, scope) : text;
|
|
@@ -61,6 +61,32 @@ const callExecutor = (context) => {
|
|
|
61
61
|
}
|
|
62
62
|
}, context.signal).then(limitedCall);
|
|
63
63
|
};
|
|
64
|
+
// ../nodes/src/claude-plan.ts
|
|
65
|
+
/**
|
|
66
|
+
* @file What a Claude node's session is planned with, read from the node's
|
|
67
|
+
* context.
|
|
68
|
+
*/
|
|
69
|
+
/** The plan options every session of a node starts from. */
|
|
70
|
+
const planOptionsOf = (context, inPlace) => {
|
|
71
|
+
const planOptions = {
|
|
72
|
+
cwd: context.cwd,
|
|
73
|
+
inputs: context.inputs,
|
|
74
|
+
outputs: context.outputs,
|
|
75
|
+
inPlace,
|
|
76
|
+
workflowDir: context.workflowDir,
|
|
77
|
+
projectRoot: context.projectRoot,
|
|
78
|
+
...context.orkDir === void 0 ? {} : { orkDir: context.orkDir },
|
|
79
|
+
pluginDir: path.join(context.runDir, "plugins", context.id)
|
|
80
|
+
};
|
|
81
|
+
if (context.checkout !== void 0) planOptions.checkout = context.checkout;
|
|
82
|
+
if (context.brief !== void 0) planOptions.brief = context.brief;
|
|
83
|
+
if (context.template !== void 0) planOptions.template = context.template;
|
|
84
|
+
if (context.item !== void 0) planOptions.item = context.item;
|
|
85
|
+
if (context.ownership !== void 0) planOptions.ownership = context.ownership;
|
|
86
|
+
if (context.mailbox !== void 0) planOptions.mailbox = context.mailbox;
|
|
87
|
+
if (context.claims !== void 0) planOptions.claims = context.claims;
|
|
88
|
+
return planOptions;
|
|
89
|
+
};
|
|
64
90
|
// ../nodes/src/claude-result.ts
|
|
65
91
|
/**
|
|
66
92
|
* @file A finished session as a node's result: its report is the output, held
|
|
@@ -123,6 +149,156 @@ const resultOf = (outcome, declared) => {
|
|
|
123
149
|
if (!result.ok) result.error = report.summary;
|
|
124
150
|
return result;
|
|
125
151
|
};
|
|
152
|
+
// ../nodes/src/environ.ts
|
|
153
|
+
/** A variable of the user's. */
|
|
154
|
+
const readEnv = (environ, name) => Object.hasOwn(environ, name) ? environ[name] : void 0;
|
|
155
|
+
// ../nodes/src/mcp-servers.ts
|
|
156
|
+
/**
|
|
157
|
+
* @file Reading an MCP server's definition from the project's `.mcp.json`, the
|
|
158
|
+
* file Claude Code reads too, so a server is defined in one place (RFC-006).
|
|
159
|
+
* `${VAR}` and `${VAR:-default}` read the user's environment, which is where
|
|
160
|
+
* secrets stay: Ork stores none, and a run masks each value read this way. A
|
|
161
|
+
* server that needs an OAuth sign-in holds Claude Code's tokens, not Ork's,
|
|
162
|
+
* so it is refused.
|
|
163
|
+
*/
|
|
164
|
+
const VARIABLE = /\$\{([A-Za-z_]\w*)(?::-([^}]*))?\}/gu;
|
|
165
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
166
|
+
const stringsOf = (value) => isRecord(value) ? Object.fromEntries(Object.entries(value).flatMap(([key, item]) => typeof item === "string" ? [[key, item]] : [])) : {};
|
|
167
|
+
const listOf = (value) => Array.isArray(value) ? value.flatMap((item) => typeof item === "string" ? [item] : []) : [];
|
|
168
|
+
/** The server as written, or why Ork can't call it. */
|
|
169
|
+
const readMcpServer = (root, name) => {
|
|
170
|
+
const servers = serversIn(root);
|
|
171
|
+
if (!servers.ok) return servers;
|
|
172
|
+
const known = Object.keys(servers.value);
|
|
173
|
+
const server = Object.hasOwn(servers.value, name) ? servers.value[name] : void 0;
|
|
174
|
+
if (!isRecord(server)) return {
|
|
175
|
+
ok: false,
|
|
176
|
+
error: `no MCP server \`${name}\` in ${MCP_FILE}${known.length === 0 ? "" : `, which has ${known.join(", ")}`}`
|
|
177
|
+
};
|
|
178
|
+
if ("oauth" in server) return {
|
|
179
|
+
ok: false,
|
|
180
|
+
error: `MCP server \`${name}\` signs in with OAuth, whose tokens are Claude Code's: give it a token through headers and \`env:\`, or call it from a session`
|
|
181
|
+
};
|
|
182
|
+
const notText = mcpNotText(server);
|
|
183
|
+
if (notText !== void 0) return {
|
|
184
|
+
ok: false,
|
|
185
|
+
error: `MCP server \`${name}\` has a value in ${notText} that isn't a string: write it in quotes`
|
|
186
|
+
};
|
|
187
|
+
const { type, url, command } = server;
|
|
188
|
+
if (type === "http" && typeof url === "string") return {
|
|
189
|
+
ok: true,
|
|
190
|
+
value: {
|
|
191
|
+
kind: "http",
|
|
192
|
+
url,
|
|
193
|
+
headers: stringsOf(server["headers"])
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
if ((type === void 0 || type === "stdio") && typeof command === "string") return {
|
|
197
|
+
ok: true,
|
|
198
|
+
value: {
|
|
199
|
+
kind: "stdio",
|
|
200
|
+
command,
|
|
201
|
+
args: listOf(server["args"]),
|
|
202
|
+
env: stringsOf(server["env"])
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
error: `MCP server \`${name}\` is not one Ork can call: it takes stdio and http servers`
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* The named servers' entries as written, for a session: `${VAR}` stays text for
|
|
212
|
+
* the `claude` that loads them to expand. The compiler has checked the names.
|
|
213
|
+
*/
|
|
214
|
+
const namedMcpEntries = (root, names) => {
|
|
215
|
+
if (names.length === 0) return {
|
|
216
|
+
ok: true,
|
|
217
|
+
value: {}
|
|
218
|
+
};
|
|
219
|
+
const servers = serversIn(root);
|
|
220
|
+
if (!servers.ok) return servers;
|
|
221
|
+
const mcpServers = {};
|
|
222
|
+
for (const name of names) {
|
|
223
|
+
const server = Object.hasOwn(servers.value, name) ? servers.value[name] : void 0;
|
|
224
|
+
if (!isRecord(server)) return {
|
|
225
|
+
ok: false,
|
|
226
|
+
error: `no MCP server \`${name}\` in ${MCP_FILE}`
|
|
227
|
+
};
|
|
228
|
+
mcpServers[name] = server;
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
ok: true,
|
|
232
|
+
value: { mcpServers }
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
/** Each variable a server reads, and whether it has a default. */
|
|
236
|
+
const readsOf = (server) => {
|
|
237
|
+
return (server.kind === "stdio" ? [
|
|
238
|
+
server.command,
|
|
239
|
+
...server.args,
|
|
240
|
+
...Object.values(server.env)
|
|
241
|
+
] : [server.url, ...Object.values(server.headers)]).flatMap((text) => [...text.matchAll(VARIABLE)].map((match) => ({
|
|
242
|
+
name: String(match[1]),
|
|
243
|
+
fallback: match[2] !== void 0
|
|
244
|
+
})));
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* The server with its variables filled in, and the values to mask: all it read
|
|
248
|
+
* but those `plain` names, which aren't secret.
|
|
249
|
+
*/
|
|
250
|
+
const expandServer = (server, environ, plain = []) => {
|
|
251
|
+
const unset = readsOf(server).find(({ name, fallback }) => !fallback && readEnv(environ, name) === void 0);
|
|
252
|
+
if (unset !== void 0) return {
|
|
253
|
+
ok: false,
|
|
254
|
+
error: `${MCP_FILE} reads ${unset.name}, which is not set`
|
|
255
|
+
};
|
|
256
|
+
const values = [];
|
|
257
|
+
const fill = (text) => text.replaceAll(VARIABLE, (_all, name, fallback) => {
|
|
258
|
+
const value = readEnv(environ, name);
|
|
259
|
+
if (value !== void 0 && !plain.includes(name)) values.push(value);
|
|
260
|
+
return value ?? fallback ?? "";
|
|
261
|
+
});
|
|
262
|
+
const fillAll = (record) => Object.fromEntries(Object.entries(record).map(([key, text]) => [key, fill(text)]));
|
|
263
|
+
return {
|
|
264
|
+
ok: true,
|
|
265
|
+
value: {
|
|
266
|
+
server: server.kind === "stdio" ? {
|
|
267
|
+
kind: "stdio",
|
|
268
|
+
command: fill(server.command),
|
|
269
|
+
args: server.args.map((arg) => fill(arg)),
|
|
270
|
+
env: fillAll(server.env)
|
|
271
|
+
} : {
|
|
272
|
+
kind: "http",
|
|
273
|
+
url: fill(server.url),
|
|
274
|
+
headers: fillAll(server.headers)
|
|
275
|
+
},
|
|
276
|
+
values
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
/** What a workflow's MCP servers read of the user's, unless called not secret. */
|
|
281
|
+
const mcpVariablesOf = (workflow, root) => Object.values(workflow.nodes).flatMap((node) => {
|
|
282
|
+
if (node.type !== "mcp.call") return [];
|
|
283
|
+
const read = readMcpServer(root, node.server);
|
|
284
|
+
return read.ok ? readsOf(read.value).map(({ name }) => name).filter((name) => node.plainEnv?.includes(name) !== true) : [];
|
|
285
|
+
});
|
|
286
|
+
/** A stdio server's program, when its name can be known before it is filled in. */
|
|
287
|
+
const programOf = (server) => server.kind === "stdio" && !server.command.includes("$") ? server.command : void 0;
|
|
288
|
+
/**
|
|
289
|
+
* What stops a workflow's `mcp.call` nodes, found before a run spends anything:
|
|
290
|
+
* a server that isn't there or can't be called, a program that isn't installed,
|
|
291
|
+
* or a variable that is not set.
|
|
292
|
+
*/
|
|
293
|
+
const mcpProblems = (workflow, root, environ = process.env, finished = /* @__PURE__ */ new Set(), installed = () => true) => Object.entries(workflow.nodes).flatMap(([id, node]) => {
|
|
294
|
+
if (node.type !== "mcp.call" || finished.has(id)) return [];
|
|
295
|
+
const read = readMcpServer(root, node.server);
|
|
296
|
+
if (!read.ok) return [`${id}: ${read.error}`];
|
|
297
|
+
const given = new Set(Object.keys(node.env ?? {}));
|
|
298
|
+
const unset = readsOf(read.value).flatMap(({ name, fallback }) => fallback || given.has(name) || readEnv(environ, name) !== void 0 ? [] : [`${id}: ${MCP_FILE} reads ${name}, which is not set`]);
|
|
299
|
+
const program = programOf(read.value);
|
|
300
|
+
return program === void 0 || installed(program) ? unset : [`\`${id}\`: MCP server \`${node.server}\` runs \`${program}\`, which is not installed`].concat(unset);
|
|
301
|
+
});
|
|
126
302
|
/** The only built-in tools a fork has: it reads, and reports. */
|
|
127
303
|
const FORK_TOOLS = [
|
|
128
304
|
"Read",
|
|
@@ -277,25 +453,39 @@ const askFork = async (context, owner, question, deps) => {
|
|
|
277
453
|
/** What the person sees of a question its asker's owner passed on. */
|
|
278
454
|
const passedOnContext = (context, owner, note) => [...context === void 0 ? [] : [context, ""], note === void 0 ? `${owner} passed this on.` : `${owner} passed this on: ${note}`].join("\n");
|
|
279
455
|
/**
|
|
456
|
+
* The node's channel to the person: what it asked is withdrawn when it stops,
|
|
457
|
+
* so a stopped node leaves no decision open.
|
|
458
|
+
*/
|
|
459
|
+
const withdrawnOnStop = (context) => {
|
|
460
|
+
const { human } = context;
|
|
461
|
+
const signalOf = (asked) => asked === void 0 ? context.signal : AbortSignal.any([asked, context.signal]);
|
|
462
|
+
return {
|
|
463
|
+
permit: (request) => human.permit({
|
|
464
|
+
...request,
|
|
465
|
+
signal: signalOf(request.signal)
|
|
466
|
+
}),
|
|
467
|
+
ask: (question) => human.ask({
|
|
468
|
+
...question,
|
|
469
|
+
signal: signalOf(question.signal)
|
|
470
|
+
}),
|
|
471
|
+
...human.settled === void 0 ? {} : { settled: human.settled }
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
/**
|
|
280
475
|
* The child's channel when its owner takes its questions first: a question goes
|
|
281
476
|
* to a fork of the owner's session, then to the person if the fork passes it on
|
|
282
477
|
* or can't answer. Permission prompts go to the person as before.
|
|
283
478
|
*/
|
|
284
479
|
const ownerFirst = (context, owner, deps) => {
|
|
285
|
-
const
|
|
480
|
+
const human = withdrawnOnStop(context);
|
|
286
481
|
return {
|
|
287
482
|
permit: (request) => human.permit(request),
|
|
288
483
|
ask: async (question) => {
|
|
289
484
|
const answered = await askFork(context, owner, question, deps);
|
|
290
485
|
if (context.signal.aborted) throw new Error(`${context.id} was stopped`);
|
|
291
|
-
|
|
292
|
-
if (answered === null) return human.ask({
|
|
293
|
-
...question,
|
|
294
|
-
signal
|
|
295
|
-
});
|
|
486
|
+
if (answered === null) return human.ask(question);
|
|
296
487
|
if (answered.choice === passOnId(question.options)) return human.ask({
|
|
297
488
|
...question,
|
|
298
|
-
signal,
|
|
299
489
|
owner: owner.id,
|
|
300
490
|
context: passedOnContext(question.context, owner.id, answered.note)
|
|
301
491
|
});
|
|
@@ -379,6 +569,20 @@ const budgetNow = (context) => {
|
|
|
379
569
|
return known.length === 0 ? void 0 : Math.min(...known);
|
|
380
570
|
};
|
|
381
571
|
/**
|
|
572
|
+
* What a node's first session may spend, set aside from the run's budget in the
|
|
573
|
+
* same step that reads it (ADR-077), so a node starting beside it is never
|
|
574
|
+
* offered the same part: the least of what is left and the node's own
|
|
575
|
+
* `budgetUsd`. With nothing left, it waits for a worker running beside it to
|
|
576
|
+
* finish, as long as one may.
|
|
577
|
+
*/
|
|
578
|
+
const budgetToStart = async (context) => {
|
|
579
|
+
const left = budgetNow(context);
|
|
580
|
+
if (left !== void 0 && left <= 0) return await context.budgetFreed?.(context.signal) === true ? budgetToStart(context) : left;
|
|
581
|
+
const own = isClaudeNode(context.node) ? context.node.budgetUsd ?? context.workflow.defaults?.budgetUsd : void 0;
|
|
582
|
+
context.reserveBudget?.(own === void 0 || left === void 0 ? left : Math.min(own, left));
|
|
583
|
+
return left;
|
|
584
|
+
};
|
|
585
|
+
/**
|
|
382
586
|
* Whether a limit across runs, rather than the node's work, ended a node: a
|
|
383
587
|
* session whose cap was a quota's and that ran into it, or one its plan refused
|
|
384
588
|
* and that gave no report. A node that still succeeded is left alone.
|
|
@@ -660,163 +864,6 @@ const tokenize = (command) => {
|
|
|
660
864
|
args
|
|
661
865
|
};
|
|
662
866
|
};
|
|
663
|
-
// ../nodes/src/environ.ts
|
|
664
|
-
/** A variable of the user's. */
|
|
665
|
-
const readEnv = (environ, name) => Object.hasOwn(environ, name) ? environ[name] : void 0;
|
|
666
|
-
// ../nodes/src/mcp-servers.ts
|
|
667
|
-
/**
|
|
668
|
-
* @file Reading an MCP server's definition from the project's `.mcp.json`, the
|
|
669
|
-
* file Claude Code reads too, so a server is defined in one place (RFC-006).
|
|
670
|
-
* `${VAR}` and `${VAR:-default}` read the user's environment, which is where
|
|
671
|
-
* secrets stay: Ork stores none, and a run masks each value read this way. A
|
|
672
|
-
* server that needs an OAuth sign-in holds Claude Code's tokens, not Ork's,
|
|
673
|
-
* so it is refused.
|
|
674
|
-
*/
|
|
675
|
-
const MCP_FILE = ".mcp.json";
|
|
676
|
-
const VARIABLE = /\$\{([A-Za-z_]\w*)(?::-([^}]*))?\}/gu;
|
|
677
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
678
|
-
/** The file's `mcpServers`; `{}` when there is no file. */
|
|
679
|
-
const serversIn = (root) => {
|
|
680
|
-
const file = path.join(root, MCP_FILE);
|
|
681
|
-
if (!existsSync(file)) return {
|
|
682
|
-
ok: true,
|
|
683
|
-
value: {}
|
|
684
|
-
};
|
|
685
|
-
try {
|
|
686
|
-
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
687
|
-
const servers = isRecord(parsed) ? parsed["mcpServers"] : void 0;
|
|
688
|
-
return {
|
|
689
|
-
ok: true,
|
|
690
|
-
value: isRecord(servers) ? servers : {}
|
|
691
|
-
};
|
|
692
|
-
} catch (error) {
|
|
693
|
-
const reason = error instanceof SyntaxError ? "it is not valid JSON" : error instanceof Error ? error.message : String(error);
|
|
694
|
-
return {
|
|
695
|
-
ok: false,
|
|
696
|
-
error: `${MCP_FILE} can't be read: ${reason}`
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
const stringsOf = (value) => isRecord(value) ? Object.fromEntries(Object.entries(value).flatMap(([key, item]) => typeof item === "string" ? [[key, item]] : [])) : {};
|
|
701
|
-
const listOf = (value) => Array.isArray(value) ? value.flatMap((item) => typeof item === "string" ? [item] : []) : [];
|
|
702
|
-
/** A server's field that holds something other than text: it would be dropped. */
|
|
703
|
-
const notTextIn = (server) => [
|
|
704
|
-
"args",
|
|
705
|
-
"env",
|
|
706
|
-
"headers"
|
|
707
|
-
].find((field) => {
|
|
708
|
-
const value = server[field];
|
|
709
|
-
return (Array.isArray(value) ? value : isRecord(value) ? Object.values(value) : []).some((item) => typeof item !== "string");
|
|
710
|
-
});
|
|
711
|
-
/** The server as written, or why Ork can't call it. */
|
|
712
|
-
const readMcpServer = (root, name) => {
|
|
713
|
-
const servers = serversIn(root);
|
|
714
|
-
if (!servers.ok) return servers;
|
|
715
|
-
const known = Object.keys(servers.value);
|
|
716
|
-
const server = Object.hasOwn(servers.value, name) ? servers.value[name] : void 0;
|
|
717
|
-
if (!isRecord(server)) return {
|
|
718
|
-
ok: false,
|
|
719
|
-
error: `no MCP server \`${name}\` in ${MCP_FILE}${known.length === 0 ? "" : `, which has ${known.join(", ")}`}`
|
|
720
|
-
};
|
|
721
|
-
if ("oauth" in server) return {
|
|
722
|
-
ok: false,
|
|
723
|
-
error: `MCP server \`${name}\` signs in with OAuth, whose tokens are Claude Code's: give it a token through headers and \`env:\`, or call it from a session`
|
|
724
|
-
};
|
|
725
|
-
const notText = notTextIn(server);
|
|
726
|
-
if (notText !== void 0) return {
|
|
727
|
-
ok: false,
|
|
728
|
-
error: `MCP server \`${name}\` has a value in ${notText} that isn't a string: write it in quotes`
|
|
729
|
-
};
|
|
730
|
-
const { type, url, command } = server;
|
|
731
|
-
if (type === "http" && typeof url === "string") return {
|
|
732
|
-
ok: true,
|
|
733
|
-
value: {
|
|
734
|
-
kind: "http",
|
|
735
|
-
url,
|
|
736
|
-
headers: stringsOf(server["headers"])
|
|
737
|
-
}
|
|
738
|
-
};
|
|
739
|
-
if ((type === void 0 || type === "stdio") && typeof command === "string") return {
|
|
740
|
-
ok: true,
|
|
741
|
-
value: {
|
|
742
|
-
kind: "stdio",
|
|
743
|
-
command,
|
|
744
|
-
args: listOf(server["args"]),
|
|
745
|
-
env: stringsOf(server["env"])
|
|
746
|
-
}
|
|
747
|
-
};
|
|
748
|
-
return {
|
|
749
|
-
ok: false,
|
|
750
|
-
error: `MCP server \`${name}\` is not one Ork can call: it takes stdio and http servers`
|
|
751
|
-
};
|
|
752
|
-
};
|
|
753
|
-
/** Each variable a server reads, and whether it has a default. */
|
|
754
|
-
const readsOf = (server) => {
|
|
755
|
-
return (server.kind === "stdio" ? [
|
|
756
|
-
server.command,
|
|
757
|
-
...server.args,
|
|
758
|
-
...Object.values(server.env)
|
|
759
|
-
] : [server.url, ...Object.values(server.headers)]).flatMap((text) => [...text.matchAll(VARIABLE)].map((match) => ({
|
|
760
|
-
name: String(match[1]),
|
|
761
|
-
fallback: match[2] !== void 0
|
|
762
|
-
})));
|
|
763
|
-
};
|
|
764
|
-
/**
|
|
765
|
-
* The server with its variables filled in, and the values to mask: all it read
|
|
766
|
-
* but those `plain` names, which aren't secret.
|
|
767
|
-
*/
|
|
768
|
-
const expandServer = (server, environ, plain = []) => {
|
|
769
|
-
const unset = readsOf(server).find(({ name, fallback }) => !fallback && readEnv(environ, name) === void 0);
|
|
770
|
-
if (unset !== void 0) return {
|
|
771
|
-
ok: false,
|
|
772
|
-
error: `${MCP_FILE} reads ${unset.name}, which is not set`
|
|
773
|
-
};
|
|
774
|
-
const values = [];
|
|
775
|
-
const fill = (text) => text.replaceAll(VARIABLE, (_all, name, fallback) => {
|
|
776
|
-
const value = readEnv(environ, name);
|
|
777
|
-
if (value !== void 0 && !plain.includes(name)) values.push(value);
|
|
778
|
-
return value ?? fallback ?? "";
|
|
779
|
-
});
|
|
780
|
-
const fillAll = (record) => Object.fromEntries(Object.entries(record).map(([key, text]) => [key, fill(text)]));
|
|
781
|
-
return {
|
|
782
|
-
ok: true,
|
|
783
|
-
value: {
|
|
784
|
-
server: server.kind === "stdio" ? {
|
|
785
|
-
kind: "stdio",
|
|
786
|
-
command: fill(server.command),
|
|
787
|
-
args: server.args.map((arg) => fill(arg)),
|
|
788
|
-
env: fillAll(server.env)
|
|
789
|
-
} : {
|
|
790
|
-
kind: "http",
|
|
791
|
-
url: fill(server.url),
|
|
792
|
-
headers: fillAll(server.headers)
|
|
793
|
-
},
|
|
794
|
-
values
|
|
795
|
-
}
|
|
796
|
-
};
|
|
797
|
-
};
|
|
798
|
-
/** What a workflow's MCP servers read of the user's, unless called not secret. */
|
|
799
|
-
const mcpVariablesOf = (workflow, root) => Object.values(workflow.nodes).flatMap((node) => {
|
|
800
|
-
if (node.type !== "mcp.call") return [];
|
|
801
|
-
const read = readMcpServer(root, node.server);
|
|
802
|
-
return read.ok ? readsOf(read.value).map(({ name }) => name).filter((name) => node.plainEnv?.includes(name) !== true) : [];
|
|
803
|
-
});
|
|
804
|
-
/** A stdio server's program, when its name can be known before it is filled in. */
|
|
805
|
-
const programOf = (server) => server.kind === "stdio" && !server.command.includes("$") ? server.command : void 0;
|
|
806
|
-
/**
|
|
807
|
-
* What stops a workflow's `mcp.call` nodes, found before a run spends anything:
|
|
808
|
-
* a server that isn't there or can't be called, a program that isn't installed,
|
|
809
|
-
* or a variable that is not set.
|
|
810
|
-
*/
|
|
811
|
-
const mcpProblems = (workflow, root, environ = process.env, finished = /* @__PURE__ */ new Set(), installed = () => true) => Object.entries(workflow.nodes).flatMap(([id, node]) => {
|
|
812
|
-
if (node.type !== "mcp.call" || finished.has(id)) return [];
|
|
813
|
-
const read = readMcpServer(root, node.server);
|
|
814
|
-
if (!read.ok) return [`${id}: ${read.error}`];
|
|
815
|
-
const given = new Set(Object.keys(node.env ?? {}));
|
|
816
|
-
const unset = readsOf(read.value).flatMap(({ name, fallback }) => fallback || given.has(name) || readEnv(environ, name) !== void 0 ? [] : [`${id}: ${MCP_FILE} reads ${name}, which is not set`]);
|
|
817
|
-
const program = programOf(read.value);
|
|
818
|
-
return program === void 0 || installed(program) ? unset : [`\`${id}\`: MCP server \`${node.server}\` runs \`${program}\`, which is not installed`].concat(unset);
|
|
819
|
-
});
|
|
820
867
|
// ../nodes/src/tool-scrub.ts
|
|
821
868
|
/**
|
|
822
869
|
* @file Masking a secret in what a program prints. A plain search, so a value
|
|
@@ -998,7 +1045,9 @@ const IDENTITY_KEYS = /* @__PURE__ */ new Set([
|
|
|
998
1045
|
"ORK_WORKFLOW",
|
|
999
1046
|
"ORK_BIN",
|
|
1000
1047
|
OUTPUT_ENV,
|
|
1001
|
-
STATE_ENV
|
|
1048
|
+
STATE_ENV,
|
|
1049
|
+
"ORK_ITEM",
|
|
1050
|
+
"ORK_INDEX"
|
|
1002
1051
|
]);
|
|
1003
1052
|
/**
|
|
1004
1053
|
* `env` without Ork's identity variables. A run started by a program in another
|
|
@@ -1026,7 +1075,11 @@ const identityOf = (context, orkBin) => ({
|
|
|
1026
1075
|
ORK_RUN_DIR: context.runDir,
|
|
1027
1076
|
ORK_PROJECT: context.projectRoot,
|
|
1028
1077
|
ORK_WORKFLOW: context.workflow.name,
|
|
1029
|
-
...orkBin === void 0 ? {} : { ORK_BIN: orkBin }
|
|
1078
|
+
...orkBin === void 0 ? {} : { ORK_BIN: orkBin },
|
|
1079
|
+
...context.item === void 0 ? {} : {
|
|
1080
|
+
ORK_ITEM: typeof context.item.value === "string" ? context.item.value : JSON.stringify(context.item.value),
|
|
1081
|
+
ORK_INDEX: String(context.item.index)
|
|
1082
|
+
}
|
|
1030
1083
|
});
|
|
1031
1084
|
/** The variables a workflow's nodes read that no `plainEnv` excuses. */
|
|
1032
1085
|
const secretNamesOf = (workflow) => Object.values(workflow.nodes).flatMap((node) => programsRunBy(node, workflow.defaults)).flatMap(({ program }) => {
|
|
@@ -1068,7 +1121,7 @@ const secretsOf = (workflow, environ, also = []) => valuesOf(namesToMask(workflo
|
|
|
1068
1121
|
const renderEnv = (node, context, environ) => {
|
|
1069
1122
|
const values = {};
|
|
1070
1123
|
const scope = {
|
|
1071
|
-
...runScope(context.inputs, context.outputs, context.runId),
|
|
1124
|
+
...runScope(context.inputs, context.outputs, context.runId, context.item),
|
|
1072
1125
|
output: { env: environ }
|
|
1073
1126
|
};
|
|
1074
1127
|
for (const [key, text] of Object.entries(node.env ?? {})) {
|
|
@@ -1158,7 +1211,7 @@ const render = (text, context) => {
|
|
|
1158
1211
|
};
|
|
1159
1212
|
return {
|
|
1160
1213
|
ok: true,
|
|
1161
|
-
value: renderTemplate(template.parts, runScope(context.inputs, context.outputs, context.runId))
|
|
1214
|
+
value: renderTemplate(template.parts, runScope(context.inputs, context.outputs, context.runId, context.item))
|
|
1162
1215
|
};
|
|
1163
1216
|
};
|
|
1164
1217
|
/** The program and arguments a tool node runs, before the allow check. */
|
|
@@ -1515,11 +1568,19 @@ const checkedResult = (result, { check, resumes }) => {
|
|
|
1515
1568
|
* (ADR-019).
|
|
1516
1569
|
*/
|
|
1517
1570
|
const MAX_INPUT = 8192;
|
|
1518
|
-
|
|
1571
|
+
const inputJson = (input) => JSON.stringify(input ?? {}, void 0, 2);
|
|
1572
|
+
/** What a sent-back input is whole, for `read_input`, when its note cuts it. */
|
|
1573
|
+
const wholeInput = (input) => {
|
|
1574
|
+
const json = inputJson(input);
|
|
1575
|
+
return json.length > MAX_INPUT ? json : void 0;
|
|
1576
|
+
};
|
|
1577
|
+
/** A re-entry's note of what was sent back; `read_input` has a cut rest. */
|
|
1519
1578
|
const reentryNote = (round, input) => {
|
|
1520
|
-
const json =
|
|
1521
|
-
const
|
|
1522
|
-
|
|
1579
|
+
const json = inputJson(input);
|
|
1580
|
+
const cut = json.length > MAX_INPUT;
|
|
1581
|
+
const body = cut ? `${json.slice(0, MAX_INPUT)}\n…` : json;
|
|
1582
|
+
const rest = cut ? `\n\nThat is the first ${String(MAX_INPUT)} of its ${String(json.length)} characters. Call read_input for all of it before you act on it.` : "";
|
|
1583
|
+
return `\n\n## Round ${String(round)}\n\nThe workflow sent this task back to you with:\n\n\`\`\`json\n${body}\n\`\`\`${rest}`;
|
|
1523
1584
|
};
|
|
1524
1585
|
/**
|
|
1525
1586
|
* What a resumed leader is told: it already has its brief and its earlier work,
|
|
@@ -1545,22 +1606,7 @@ const specFor = (spec, context) => {
|
|
|
1545
1606
|
};
|
|
1546
1607
|
/** Runs a `claude.leader` or `claude.worker` as one session. */
|
|
1547
1608
|
const claudeExecutor = (options) => async (context) => {
|
|
1548
|
-
const planOptions =
|
|
1549
|
-
cwd: context.cwd,
|
|
1550
|
-
inputs: context.inputs,
|
|
1551
|
-
outputs: context.outputs,
|
|
1552
|
-
inPlace: options.inPlace,
|
|
1553
|
-
workflowDir: context.workflowDir,
|
|
1554
|
-
projectRoot: context.projectRoot,
|
|
1555
|
-
...context.orkDir === void 0 ? {} : { orkDir: context.orkDir },
|
|
1556
|
-
pluginDir: path.join(context.runDir, "plugins", context.id)
|
|
1557
|
-
};
|
|
1558
|
-
if (context.checkout !== void 0) planOptions.checkout = context.checkout;
|
|
1559
|
-
if (context.brief !== void 0) planOptions.brief = context.brief;
|
|
1560
|
-
if (context.template !== void 0) planOptions.template = context.template;
|
|
1561
|
-
if (context.ownership !== void 0) planOptions.ownership = context.ownership;
|
|
1562
|
-
if (context.mailbox !== void 0) planOptions.mailbox = context.mailbox;
|
|
1563
|
-
if (context.claims !== void 0) planOptions.claims = context.claims;
|
|
1609
|
+
const planOptions = planOptionsOf(context, options.inPlace);
|
|
1564
1610
|
const before = beforeOf(context.node, context.workflow.defaults);
|
|
1565
1611
|
if (before !== void 0) {
|
|
1566
1612
|
const ran = await runStep(options.steps ?? noSteps, context, before, {
|
|
@@ -1577,7 +1623,7 @@ const claudeExecutor = (options) => async (context) => {
|
|
|
1577
1623
|
}
|
|
1578
1624
|
const blocked = context.quota?.().blocked;
|
|
1579
1625
|
if (blocked !== void 0) return notStarted(blocked, blocked);
|
|
1580
|
-
const budget =
|
|
1626
|
+
const budget = await budgetToStart(context);
|
|
1581
1627
|
if (budget !== void 0 && budget <= 0) return notStarted("nothing is left of the run's budget");
|
|
1582
1628
|
if (budget !== void 0) planOptions.budgetLeftUsd = budget;
|
|
1583
1629
|
const plan = planNodeSession(context.workflow, context.id, planOptions);
|
|
@@ -1589,16 +1635,21 @@ const claudeExecutor = (options) => async (context) => {
|
|
|
1589
1635
|
},
|
|
1590
1636
|
error: plan.error
|
|
1591
1637
|
};
|
|
1592
|
-
const
|
|
1638
|
+
const servers = namedMcpEntries(context.projectRoot, plan.mcp);
|
|
1639
|
+
if (!servers.ok) return notStarted(servers.error);
|
|
1640
|
+
const spec = {
|
|
1641
|
+
...specFor(plan.spec, context),
|
|
1642
|
+
...servers.value
|
|
1643
|
+
};
|
|
1593
1644
|
if (options.machineClaude === true) spec.machineClaude = true;
|
|
1594
1645
|
const driver = sessionDriver({
|
|
1595
1646
|
override: options.driver,
|
|
1596
1647
|
named: plan.driver,
|
|
1597
1648
|
ready: options.readyDriver
|
|
1598
1649
|
});
|
|
1599
|
-
if (driver === void 0) return notStarted("no
|
|
1650
|
+
if (driver === void 0) return notStarted("no agent driver is ready; see `ork doctor`");
|
|
1600
1651
|
const runtime = await options.runtimeFor(driver);
|
|
1601
|
-
const ports = { human: context.owner === void 0 ? context
|
|
1652
|
+
const ports = { human: context.owner === void 0 ? withdrawnOnStop(context) : ownerFirst(context, context.owner, {
|
|
1602
1653
|
runtimeFor: options.runtimeFor,
|
|
1603
1654
|
driver: options.driver,
|
|
1604
1655
|
readyDriver: options.readyDriver,
|
|
@@ -1609,9 +1660,12 @@ const claudeExecutor = (options) => async (context) => {
|
|
|
1609
1660
|
if (context.mailbox !== void 0) ports.mailbox = context.mailbox;
|
|
1610
1661
|
if (context.claims !== void 0) ports.claims = context.claims;
|
|
1611
1662
|
if (context.feedback !== void 0) ports.feedback = context.feedback;
|
|
1663
|
+
const whole = context.round > 1 ? wholeInput(context.input) : void 0;
|
|
1664
|
+
if (whole !== void 0) ports.input = whole;
|
|
1612
1665
|
const watch = limitWatch(context);
|
|
1613
1666
|
const run = async (asked) => {
|
|
1614
1667
|
const watching = watch.session(asked);
|
|
1668
|
+
context.reserveBudget?.(asked.budgetUsd);
|
|
1615
1669
|
const session = runtime.start(asked, ports);
|
|
1616
1670
|
const stop = () => {
|
|
1617
1671
|
session.interrupt();
|
|
@@ -1629,6 +1683,7 @@ const claudeExecutor = (options) => async (context) => {
|
|
|
1629
1683
|
context.emit(event);
|
|
1630
1684
|
}
|
|
1631
1685
|
} finally {
|
|
1686
|
+
context.reserveBudget?.(0);
|
|
1632
1687
|
disconnect?.();
|
|
1633
1688
|
context.signal.removeEventListener("abort", stop);
|
|
1634
1689
|
}
|
|
@@ -1691,10 +1746,11 @@ const claudeExecutor = (options) => async (context) => {
|
|
|
1691
1746
|
};
|
|
1692
1747
|
// ../nodes/src/gate.ts
|
|
1693
1748
|
/**
|
|
1694
|
-
* @file The `human.gate` node type: asks the human the rendered question,
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1749
|
+
* @file The `human.gate` node type: asks the human the rendered question, with
|
|
1750
|
+
* its document if it has one, both in readable text (no raw JSON), outputs
|
|
1751
|
+
* their choice with the note they gave, and answers with the `default` when
|
|
1752
|
+
* the `timeout` passes. The timeout counts only while the run and the gate
|
|
1753
|
+
* are not paused (ADR-041).
|
|
1698
1754
|
*/
|
|
1699
1755
|
/** A promise that never settles: the losing side of a race. */
|
|
1700
1756
|
const NEVER = new Promise(() => {});
|
|
@@ -1716,8 +1772,13 @@ const gateExecutor = async (context) => {
|
|
|
1716
1772
|
output: {},
|
|
1717
1773
|
error: `${context.id} is not a gate`
|
|
1718
1774
|
};
|
|
1719
|
-
const
|
|
1720
|
-
const
|
|
1775
|
+
const scope = runScope(context.inputs, context.outputs, context.runId);
|
|
1776
|
+
const render = (text) => {
|
|
1777
|
+
const template = parseTemplate(text);
|
|
1778
|
+
return template.ok ? renderTemplate(template.parts, scope, { readable: true }) : text;
|
|
1779
|
+
};
|
|
1780
|
+
const question = render(node.question);
|
|
1781
|
+
const document = render(node.document ?? "");
|
|
1721
1782
|
const options = node.options.map((o) => choiceOf(o));
|
|
1722
1783
|
const withdraw = new AbortController();
|
|
1723
1784
|
context.signal.addEventListener("abort", () => {
|
|
@@ -1734,6 +1795,7 @@ const gateExecutor = async (context) => {
|
|
|
1734
1795
|
}
|
|
1735
1796
|
};
|
|
1736
1797
|
if (node.recommended !== void 0) asked.recommended = node.recommended;
|
|
1798
|
+
if (document.trim() !== "") asked.document = document;
|
|
1737
1799
|
const answer = context.human.ask(asked).then((choice) => ({
|
|
1738
1800
|
choice,
|
|
1739
1801
|
answeredBy: "user",
|
|
@@ -1848,7 +1910,7 @@ const mcpCallExecutor = (options = {}) => async (context) => {
|
|
|
1848
1910
|
}, node.plainEnv);
|
|
1849
1911
|
if (!expanded.ok) return failed(expanded.error);
|
|
1850
1912
|
const scrub = scrubberOf([...secretsOf(context.workflow, environ, options.maskedNames), ...expanded.value.values]);
|
|
1851
|
-
const scope = runScope(context.inputs, context.outputs, context.runId);
|
|
1913
|
+
const scope = runScope(context.inputs, context.outputs, context.runId, context.item);
|
|
1852
1914
|
const args = Object.fromEntries(Object.entries(node.arguments ?? {}).map(([key, value]) => [key, renderValue(value, scope)]));
|
|
1853
1915
|
const name = `${node.server}.${node.tool}`;
|
|
1854
1916
|
context.emit({
|
|
@@ -2130,4 +2192,4 @@ const missingPrograms = (workflow, installed = (program) => onPath(program)) =>
|
|
|
2130
2192
|
}
|
|
2131
2193
|
return [...missing.values()];
|
|
2132
2194
|
};
|
|
2133
|
-
export {
|
|
2195
|
+
export { namedMcpEntries as _, builtinExecutors as a, toolExecutor as c, maskedEnv as d, maskedNamesOfRun as f, clearOutputFiles as g, sessionEnv as h, onPath as i, clearStateFiles as l, runScrubber as m, missingProgramMessage as n, spawnTool as o, runMarkerIn as p, missingPrograms as r, mcpCallExecutor as s, envProblems as t, inheritedScrubber as u, resultOf as v };
|