@krapht/ork 0.2.0 → 0.4.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.
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/dist/__build_info__-CtSVVuZE.mjs +691 -0
- package/dist/bin.mjs +1654 -3532
- package/dist/clean-Dcr807be.mjs +88 -0
- package/dist/context-C_bksb0C.mjs +351 -0
- package/dist/find-DptmbhGd.mjs +47 -0
- package/dist/format-J-a21Yvn.mjs +393 -0
- package/dist/in-a-run-D1Qc5Mit.mjs +22 -0
- package/dist/limits-BDZCmGEu.mjs +44 -0
- package/dist/list-ZvY2OcLq.mjs +110 -0
- package/dist/mcp-CHVKKg3B.mjs +91 -0
- package/dist/output-CXSJO00X.mjs +900 -0
- package/dist/pause-BPSmg4yz.mjs +65 -0
- package/dist/relay-Do-aNbSD.mjs +122 -0
- package/dist/remove-AwaMMaHf.mjs +194 -0
- package/dist/report-DK4LGqwF.mjs +129 -0
- package/dist/send-DhGBkowy.mjs +163 -0
- package/dist/{src-DAsOYtoR.mjs → src-BHnxpJyH.mjs} +496 -193
- package/dist/src-CGqMati3.mjs +12505 -0
- package/dist/src-CpDKIulI.mjs +3118 -0
- package/dist/src-UWkk1sl9.mjs +1899 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +631 -0
- package/dist/ui/assets/app-BgVDC_5l.js +67 -0
- package/dist/ui/assets/app-DVf68fj5.css +1 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-C51Di1Mf.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-CPJFfJgk.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-D2cXvDHF.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-DfuvdVgn.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-CkxHOdCb.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-XB2BPmDK.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-BY1KTvO5.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-C7aYOB1D.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-B2yvC1Cq.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-D-GG86Jb.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-CfzLURNc.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-DI-rJ0UV.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-Buglb9-a.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-CrWEgvU_.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-BS9BGVxz.woff2 +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-DjbiTEEA.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-CmnCPGa0.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-Q_ksOahU.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CKEaXIlx.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CZEPLOgu.woff +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Bs29RrUZ.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Dx9-epDo.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-BOaIZNA2.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-gapTbOY8.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-CN2lyvyL.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-uokXdC-Q.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-CA1yjETN.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-DFOURf8L.woff2 +0 -0
- package/dist/ui/assets/index-BiO54L90.css +1 -0
- package/dist/ui/assets/index-MQNNgsyT.js +10 -0
- package/dist/ui/assets/theme-3xH5jxKs.js +1 -0
- package/dist/ui/favicon.ico +0 -0
- package/dist/ui/favicon.svg +4 -0
- package/dist/ui/index.html +30 -0
- package/dist/ui/ork-wordmark-small-on-light.svg +4 -0
- package/dist/ui/ork-wordmark-small.svg +4 -0
- package/dist/ui-DhutUMgQ.mjs +472 -0
- package/dist/usage-CT7Z-yjr.mjs +419 -0
- package/dist/watch-0Qr2UfCQ.mjs +394 -0
- package/dist/worktrees-DHl80k96.mjs +212 -0
- package/package.json +6 -4
- package/dist/src-CJ_zF0Zj.mjs +0 -5433
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { lt as liveStatus } from "./src-CGqMati3.mjs";
|
|
2
|
+
import { i as style, n as cost } from "./output-CXSJO00X.mjs";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
// src/human.ts
|
|
5
|
+
/**
|
|
6
|
+
* @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
|
|
7
|
+
* permission prompts on the terminal, one at a time, and denies or defers
|
|
8
|
+
* when no one is there. In a run it is the decision queue's local answerer,
|
|
9
|
+
* racing `ork decide`.
|
|
10
|
+
*/
|
|
11
|
+
const PREVIEW = 160;
|
|
12
|
+
const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
|
|
13
|
+
const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
|
|
14
|
+
/** One line that shows what a tool is about to do. */
|
|
15
|
+
const previewInput = (toolName, input) => {
|
|
16
|
+
const key = [
|
|
17
|
+
"command",
|
|
18
|
+
"file_path",
|
|
19
|
+
"path",
|
|
20
|
+
"url",
|
|
21
|
+
"pattern"
|
|
22
|
+
].find((k) => typeof input[k] === "string");
|
|
23
|
+
const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
|
|
24
|
+
return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
|
|
25
|
+
};
|
|
26
|
+
/** The option an answer names: its number, or its id. */
|
|
27
|
+
const pickOption = (answer, options, recommended) => {
|
|
28
|
+
const value = answer.trim();
|
|
29
|
+
if (value === "") return recommended;
|
|
30
|
+
const index = Number(value);
|
|
31
|
+
if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
|
|
32
|
+
return options.find((o) => o.id === value)?.id;
|
|
33
|
+
};
|
|
34
|
+
const optionLine = (option, index, recommended) => {
|
|
35
|
+
const label = option.label === void 0 ? "" : ` ${option.label}`;
|
|
36
|
+
const description = option.description === void 0 ? "" : style("dim", ` - ${option.description}`);
|
|
37
|
+
const mark = option.id === recommended ? style("green", " (recommended)") : "";
|
|
38
|
+
return ` ${String(index + 1)}) ${style("bold", option.id)}${label}${description}${mark}`;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Answers permission prompts and `ask_user` questions on the terminal, one at a
|
|
42
|
+
* time. Without a terminal it denies tools and leaves questions to the node. A
|
|
43
|
+
* withdrawn prompt (answered elsewhere) rejects, which frees the turn.
|
|
44
|
+
*/
|
|
45
|
+
const createTerminalHuman = (io) => {
|
|
46
|
+
let rl;
|
|
47
|
+
let turn = Promise.resolve();
|
|
48
|
+
const say = (text) => {
|
|
49
|
+
io.output.write(`${text}\n`);
|
|
50
|
+
};
|
|
51
|
+
const prompt = (text, signal) => {
|
|
52
|
+
rl ??= createInterface({
|
|
53
|
+
input: io.input,
|
|
54
|
+
output: io.output
|
|
55
|
+
});
|
|
56
|
+
return signal === void 0 ? rl.question(text) : rl.question(text, { signal });
|
|
57
|
+
};
|
|
58
|
+
const choose = async (hint, options, recommended, signal) => {
|
|
59
|
+
const picked = pickOption(await prompt(` Choose${hint}: `, signal), options, recommended);
|
|
60
|
+
if (picked !== void 0) return picked;
|
|
61
|
+
say(style("dim", " Type an option's number or id."));
|
|
62
|
+
return choose(hint, options, recommended, signal);
|
|
63
|
+
};
|
|
64
|
+
const oneAtATime = (work) => {
|
|
65
|
+
const result = turn.then(work);
|
|
66
|
+
turn = result.catch(() => null);
|
|
67
|
+
return result;
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
permit: ({ nodeId, toolName, input, signal }) => oneAtATime(async () => {
|
|
71
|
+
if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
|
|
72
|
+
say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} wants to run ${previewInput(toolName, input)}`);
|
|
73
|
+
if (!io.interactive) {
|
|
74
|
+
say(style("dim", ` denied: ${NO_ONE_TO_PERMIT}`));
|
|
75
|
+
return {
|
|
76
|
+
allow: false,
|
|
77
|
+
message: NO_ONE_TO_PERMIT
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const answer = (await prompt(" Allow? [y/N] ", signal)).trim().toLowerCase();
|
|
81
|
+
return answer === "y" || answer === "yes" ? { allow: true } : {
|
|
82
|
+
allow: false,
|
|
83
|
+
message: "The user did not allow this tool."
|
|
84
|
+
};
|
|
85
|
+
}),
|
|
86
|
+
ask: ({ nodeId, question, options, recommended, context, signal }) => oneAtATime(() => {
|
|
87
|
+
if (signal?.aborted === true) return Promise.reject(/* @__PURE__ */ new Error("the question was withdrawn"));
|
|
88
|
+
say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} asks: ${question}`);
|
|
89
|
+
if (context !== void 0) say(style("dim", ` ${context}`));
|
|
90
|
+
for (const [index, option] of options.entries()) say(optionLine(option, index, recommended));
|
|
91
|
+
if (!io.interactive) {
|
|
92
|
+
const answer = recommended ?? NO_ONE_TO_ASK;
|
|
93
|
+
say(style("dim", ` no terminal; answered ${answer}`));
|
|
94
|
+
return Promise.resolve(answer);
|
|
95
|
+
}
|
|
96
|
+
const fallback = options.findIndex((o) => o.id === recommended);
|
|
97
|
+
const hint = fallback === -1 ? "" : ` [${String(fallback + 1)}]`;
|
|
98
|
+
return choose(hint, options, recommended, signal);
|
|
99
|
+
}),
|
|
100
|
+
close: () => {
|
|
101
|
+
rl?.close();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
// src/run/format-bus.ts
|
|
106
|
+
/** The first line of a message body, cut to fit one terminal line. */
|
|
107
|
+
const firstLine = (body) => {
|
|
108
|
+
const [line = ""] = body.trim().split("\n");
|
|
109
|
+
return line.length > 100 ? `${line.slice(0, 99)}…` : line;
|
|
110
|
+
};
|
|
111
|
+
/** Messages, owner questions and claims, without the node prefix. */
|
|
112
|
+
const formatBusEvent = (event) => {
|
|
113
|
+
if (event.type === "claim.taken") return [style("dim", `⚑ claimed ${event.resource}`)];
|
|
114
|
+
if (event.type === "claim.refused") {
|
|
115
|
+
const held = event.held === event.resource ? "" : ` (it holds ${event.held})`;
|
|
116
|
+
return [style("yellow", `⚑ ${event.resource} is ${event.holder}'s${held}`)];
|
|
117
|
+
}
|
|
118
|
+
if (event.type === "claim.released") return [style("dim", `⚑ released ${event.resource}`)];
|
|
119
|
+
if (event.type === "message.sent") {
|
|
120
|
+
const how = event.via === void 0 ? "" : ` (${event.via})`;
|
|
121
|
+
return [style("dim", `✉ to ${event.to}${how}: ${firstLine(event.body)}`)];
|
|
122
|
+
}
|
|
123
|
+
if (event.type === "message.received") {
|
|
124
|
+
const how = event.via === void 0 ? "" : ` (${event.via})`;
|
|
125
|
+
return [style("dim", `✉ from ${event.from}${how}: ${firstLine(event.body)}`)];
|
|
126
|
+
}
|
|
127
|
+
if (event.type === "call.started") return [style("dim", `↳ started ${event.workflow} as run ${event.run}`)];
|
|
128
|
+
if (event.type === "link.notice") return [style("dim", `↔ ${event.text}`)];
|
|
129
|
+
if (event.type === "message.read") return [];
|
|
130
|
+
if (event.type === "owner.asked") return [`${style(["yellow", "bold"], "?")} asked as the owner of ${event.about}: ${event.text} ${style("dim", `(${event.question})`)}`];
|
|
131
|
+
if (event.type === "owner.answered") return [style("dim", `→ ${event.answer} (owner)`)];
|
|
132
|
+
return [style("dim", "→ its session ended first; asking you")];
|
|
133
|
+
};
|
|
134
|
+
// src/run/format.ts
|
|
135
|
+
/** Who answered a decision, as the log says it. */
|
|
136
|
+
const ANSWERED_BY = {
|
|
137
|
+
terminal: "terminal",
|
|
138
|
+
decide: "ork decide",
|
|
139
|
+
ui: "ork ui"
|
|
140
|
+
};
|
|
141
|
+
const STATUS_MARK = {
|
|
142
|
+
pass: style("green", "✓"),
|
|
143
|
+
done: style("green", "✓"),
|
|
144
|
+
changes_requested: style("yellow", "!"),
|
|
145
|
+
fail: style("red", "✗")
|
|
146
|
+
};
|
|
147
|
+
const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
|
|
148
|
+
const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${line}`);
|
|
149
|
+
/** The usage line: cost, tokens, turns and time. */
|
|
150
|
+
const formatUsage = ({ usage }) => style("dim", [
|
|
151
|
+
cost(usage.costUsd),
|
|
152
|
+
`${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
|
|
153
|
+
`${String(usage.turns)} turns`,
|
|
154
|
+
seconds(usage.durationMs)
|
|
155
|
+
].join(" · "));
|
|
156
|
+
/** How a finished session reads on the terminal. */
|
|
157
|
+
const formatOutcome = (nodeId, outcome) => {
|
|
158
|
+
const { report } = outcome;
|
|
159
|
+
if (report === null) return [
|
|
160
|
+
`${style("red", "✗")} ${style("bold", nodeId)} ended without a report (${outcome.subtype})`,
|
|
161
|
+
...outcome.errors.map((e) => ` ${style("red", e)}`),
|
|
162
|
+
` ${formatUsage(outcome)}`
|
|
163
|
+
];
|
|
164
|
+
const lines = [`${STATUS_MARK[report.status] ?? "-"} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
|
|
165
|
+
if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
|
|
166
|
+
for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
|
|
167
|
+
for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
|
|
168
|
+
lines.push(` ${formatUsage(outcome)}`);
|
|
169
|
+
return lines;
|
|
170
|
+
};
|
|
171
|
+
/** The transcript lines for one event; tool names come from `tools`. */
|
|
172
|
+
const formatEvent = (nodeId, event, tools) => {
|
|
173
|
+
if (event.type === "session.started") return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId})`)];
|
|
174
|
+
if (event.type === "assistant.text") return indent(event.text);
|
|
175
|
+
if (event.type === "tool.called") {
|
|
176
|
+
tools.set(event.id, event.name);
|
|
177
|
+
return [style("dim", ` · ${previewInput(event.name, event.input)}`)];
|
|
178
|
+
}
|
|
179
|
+
if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
|
|
180
|
+
if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
|
|
181
|
+
return formatOutcome(nodeId, event.outcome);
|
|
182
|
+
};
|
|
183
|
+
/** A one-line verdict for a finished node, by what its type outputs. */
|
|
184
|
+
const nodeVerdict = (output) => {
|
|
185
|
+
const { status, summary, exitCode, choice, answeredBy, count } = output;
|
|
186
|
+
if (typeof status === "string") return typeof summary === "string" && summary !== "" ? `${status}: ${summary}` : status;
|
|
187
|
+
if (typeof exitCode === "number") return `exit ${String(exitCode)}`;
|
|
188
|
+
if (typeof choice === "string") return `${choice} (${String(answeredBy)})`;
|
|
189
|
+
if (typeof count === "number") return `${String(count)} ran`;
|
|
190
|
+
return "done";
|
|
191
|
+
};
|
|
192
|
+
/** Transcript lines for one run event, prefixed with the node they are from. */
|
|
193
|
+
const formatRunEvent = (event, width, tools) => {
|
|
194
|
+
const tag = (id) => style("cyan", `${id.padEnd(width)} │`);
|
|
195
|
+
if (event.type === "run.started") return [style("dim", `▸ run ${event.runId} of ${event.workflow} (starts at ${event.entries.join(", ")})`), ...event.settings?.trigger === void 0 ? [] : [style("dim", ` started by trigger ${event.settings.trigger}; untrusted inputs: ${(event.settings.untrusted ?? []).join(", ") || "none"}`)]];
|
|
196
|
+
if (event.type === "node.started") {
|
|
197
|
+
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
198
|
+
const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
|
|
199
|
+
return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}`)}`];
|
|
200
|
+
}
|
|
201
|
+
if (event.type === "node.event") {
|
|
202
|
+
const inner = event.event;
|
|
203
|
+
if (inner.type === "session.finished") return [];
|
|
204
|
+
if (inner.type === "session.started") return [`${tag(event.id)} ${style("dim", `${inner.model}, session ${inner.sessionId}`)}`];
|
|
205
|
+
return formatEvent(event.id, event.event, tools).map((line) => `${tag(event.id)} ${line.replace(/^ {2}/u, "")}`);
|
|
206
|
+
}
|
|
207
|
+
if (event.type === "node.finished") {
|
|
208
|
+
const mark = event.ok ? style("green", "✓") : style("red", "✗");
|
|
209
|
+
const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
|
|
210
|
+
return [`${tag(event.id)} ${mark} ${nodeVerdict(event.output)}${spent}`];
|
|
211
|
+
}
|
|
212
|
+
if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
|
|
213
|
+
if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
|
|
214
|
+
if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
|
|
215
|
+
if (event.type === "watch.changed") return [`${tag(event.id)} ${style("dim", `look ${String(event.looks)}:`)} ${event.said}`];
|
|
216
|
+
if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
|
|
217
|
+
if (event.type === "edge.taken") return [];
|
|
218
|
+
if (event.type === "worktree") {
|
|
219
|
+
const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
|
|
220
|
+
const line = `${event.action} ${event.branch ?? event.path}${detail}`;
|
|
221
|
+
return [`${tag(event.id)} ${event.action === "conflict" ? style("yellow", `! ${line}`) : style("dim", `⎇ ${line}`)}`];
|
|
222
|
+
}
|
|
223
|
+
if (event.type === "decision.opened") {
|
|
224
|
+
const what = event.kind === "permission" ? "asks to" : "asks:";
|
|
225
|
+
const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
|
|
226
|
+
return [`${tag(event.node)} ${style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(decision ${event.decision}; ork decide)`)}`];
|
|
227
|
+
}
|
|
228
|
+
if (event.type === "decision.answered") {
|
|
229
|
+
const by = ANSWERED_BY[event.by];
|
|
230
|
+
return [`${tag(event.node)} ${style("dim", `→ ${event.answer} (${by})`)}`];
|
|
231
|
+
}
|
|
232
|
+
if (event.type === "decision.withdrawn") return [`${tag(event.node)} ${style("dim", "→ question withdrawn")}`];
|
|
233
|
+
if (event.type === "run.command") {
|
|
234
|
+
const node = event.node === void 0 ? "" : ` --node ${event.node}`;
|
|
235
|
+
return [style(["yellow", "bold"], `! ork ${event.command}${node} received`)];
|
|
236
|
+
}
|
|
237
|
+
if (event.type === "node.held") {
|
|
238
|
+
const at = event.at === "tool" ? "its tool call waits for ork continue" : "it starts after ork continue";
|
|
239
|
+
return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
|
|
240
|
+
}
|
|
241
|
+
if (event.type === "node.released") return [`${tag(event.id)} ${style("dim", "▸ continues")}`];
|
|
242
|
+
if (event.type === "run.finished") return formatRunResult(event.result);
|
|
243
|
+
if (event.type === "run.resumed") {
|
|
244
|
+
const [one] = event.kept;
|
|
245
|
+
const kept = one === void 0 ? "every node runs again" : event.kept.length === 1 ? `${one} keeps its result` : `${event.kept.join(", ")} keep their results`;
|
|
246
|
+
return ["", style("dim", `▸ resumed: ${kept}`)];
|
|
247
|
+
}
|
|
248
|
+
if (event.type === "feedback.recorded") {
|
|
249
|
+
const { target, kind, title } = event.feedback;
|
|
250
|
+
const line = style("dim", `✎ feedback (${target}, ${kind}): ${title}`);
|
|
251
|
+
return [event.node === void 0 ? line : `${tag(event.node)} ${line}`];
|
|
252
|
+
}
|
|
253
|
+
if (event.type === "user.message") {
|
|
254
|
+
const note = event.delivered === "queued" ? " (for its next session)" : "";
|
|
255
|
+
return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
|
|
256
|
+
}
|
|
257
|
+
return formatBusEvent(event).map((line) => `${tag(event.node)} ${line}`);
|
|
258
|
+
};
|
|
259
|
+
const RUN_MARK = {
|
|
260
|
+
succeeded: style("green", "✓"),
|
|
261
|
+
failed: style("red", "✗"),
|
|
262
|
+
budget_exhausted: style("yellow", "!"),
|
|
263
|
+
quota_exhausted: style("yellow", "!"),
|
|
264
|
+
timed_out: style("yellow", "!"),
|
|
265
|
+
interrupted: style("yellow", "!")
|
|
266
|
+
};
|
|
267
|
+
/** The run summary: status, cost, what ran, branches to look at, and why. */
|
|
268
|
+
const formatRunResult = (result) => {
|
|
269
|
+
const ran = result.nodes.filter((n) => n.runs > 0).length;
|
|
270
|
+
const skipped = result.nodes.length - ran;
|
|
271
|
+
const lines = ["", `${RUN_MARK[result.status]} run ${result.runId} ${result.status.replace("_", " ")} ${style("dim", `· ${cost(result.costUsd)} · ${String(ran)} ran, ${String(skipped)} skipped`)}`];
|
|
272
|
+
for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
|
|
273
|
+
for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
|
|
274
|
+
return lines;
|
|
275
|
+
};
|
|
276
|
+
// src/runs/format.ts
|
|
277
|
+
/**
|
|
278
|
+
* @file How stored runs read on the terminal: the `ork ps` table and the `ork
|
|
279
|
+
* tree` ownership tree with each node's state, tokens and cost, and what is
|
|
280
|
+
* paused (ADR-041).
|
|
281
|
+
*/
|
|
282
|
+
const STATUS_STYLE = {
|
|
283
|
+
running: "cyan",
|
|
284
|
+
paused: "yellow",
|
|
285
|
+
succeeded: "green",
|
|
286
|
+
failed: "red",
|
|
287
|
+
crashed: "red",
|
|
288
|
+
budget_exhausted: "yellow",
|
|
289
|
+
quota_exhausted: "yellow",
|
|
290
|
+
timed_out: "yellow",
|
|
291
|
+
interrupted: "yellow"
|
|
292
|
+
};
|
|
293
|
+
/** `just now`, `5m ago`, `3h ago`, `2d ago`, then the date. */
|
|
294
|
+
const ago = (iso, now = Date.now()) => {
|
|
295
|
+
const minutes = Math.floor((now - Date.parse(iso)) / 6e4);
|
|
296
|
+
if (minutes < 1) return "just now";
|
|
297
|
+
if (minutes < 60) return `${String(minutes)}m ago`;
|
|
298
|
+
if (minutes < 1440) return `${String(Math.floor(minutes / 60))}h ago`;
|
|
299
|
+
if (minutes < 10080) return `${String(Math.floor(minutes / 1440))}d ago`;
|
|
300
|
+
return iso.slice(0, 10);
|
|
301
|
+
};
|
|
302
|
+
/** `42s`, `3m12s`, `1h05m`. */
|
|
303
|
+
const duration = (ms) => {
|
|
304
|
+
const s = Math.max(0, Math.round(ms / 1e3));
|
|
305
|
+
if (s < 60) return `${String(s)}s`;
|
|
306
|
+
if (s < 3600) return `${String(Math.floor(s / 60))}m${String(s % 60).padStart(2, "0")}s`;
|
|
307
|
+
return `${String(Math.floor(s / 3600))}h${String(Math.floor(s % 3600 / 60)).padStart(2, "0")}m`;
|
|
308
|
+
};
|
|
309
|
+
/** The `ork ps` table; `withProject` adds each run's project. */
|
|
310
|
+
const formatRuns = (runs, withProject, now = Date.now(), statusOf = liveStatus) => {
|
|
311
|
+
if (runs.length === 0) return ["No runs."];
|
|
312
|
+
const rows = runs.map((run) => {
|
|
313
|
+
const status = statusOf(run);
|
|
314
|
+
const end = run.finishedAt === void 0 ? now : Date.parse(run.finishedAt);
|
|
315
|
+
return {
|
|
316
|
+
id: run.id,
|
|
317
|
+
workflow: run.workflow,
|
|
318
|
+
status,
|
|
319
|
+
when: `${ago(run.startedAt, now)} · ${duration(end - Date.parse(run.startedAt))}`,
|
|
320
|
+
cost: cost(run.costUsd),
|
|
321
|
+
project: run.project
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
const width = (pick, title) => Math.max(title.length, ...rows.map((r) => pick(r).length));
|
|
325
|
+
const w = {
|
|
326
|
+
id: width((r) => r.id, "RUN"),
|
|
327
|
+
workflow: width((r) => r.workflow, "WORKFLOW"),
|
|
328
|
+
status: width((r) => r.status, "STATUS"),
|
|
329
|
+
when: width((r) => r.when, "STARTED"),
|
|
330
|
+
cost: width((r) => r.cost, "COST")
|
|
331
|
+
};
|
|
332
|
+
const header = [
|
|
333
|
+
"RUN".padEnd(w.id),
|
|
334
|
+
"WORKFLOW".padEnd(w.workflow),
|
|
335
|
+
"STATUS".padEnd(w.status),
|
|
336
|
+
"STARTED".padEnd(w.when),
|
|
337
|
+
"COST".padStart(w.cost),
|
|
338
|
+
...withProject ? ["PROJECT"] : []
|
|
339
|
+
].join(" ");
|
|
340
|
+
return [style("dim", header), ...rows.map((r) => [
|
|
341
|
+
style("bold", r.id.padEnd(w.id)),
|
|
342
|
+
r.workflow.padEnd(w.workflow),
|
|
343
|
+
style(STATUS_STYLE[r.status] ?? "reset", r.status.padEnd(w.status)),
|
|
344
|
+
style("dim", r.when.padEnd(w.when)),
|
|
345
|
+
r.cost.padStart(w.cost),
|
|
346
|
+
...withProject ? [style("dim", r.project)] : []
|
|
347
|
+
].join(" "))];
|
|
348
|
+
};
|
|
349
|
+
const STATE_MARK = {
|
|
350
|
+
waiting: style("dim", "·"),
|
|
351
|
+
running: style("cyan", "●"),
|
|
352
|
+
ok: style("green", "✓"),
|
|
353
|
+
failed: style("red", "✗"),
|
|
354
|
+
skipped: style("dim", "-")
|
|
355
|
+
};
|
|
356
|
+
/** `950`, `12.3k`, `1.2M`. */
|
|
357
|
+
const count = (n) => {
|
|
358
|
+
if (n < 1e3) return String(n);
|
|
359
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
|
|
360
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
361
|
+
};
|
|
362
|
+
const stateText = (node) => {
|
|
363
|
+
if (node.state === "running") return node.runs > 1 ? `running (round ${String(node.runs)})` : "running";
|
|
364
|
+
if (node.state === "ok" || node.state === "failed") {
|
|
365
|
+
const verdict = nodeVerdict(node.output ?? {});
|
|
366
|
+
const line = verdict.length > 60 ? `${verdict.slice(0, 59)}…` : verdict;
|
|
367
|
+
return node.runs > 1 ? `${line} (${String(node.runs)} runs)` : line;
|
|
368
|
+
}
|
|
369
|
+
return node.state;
|
|
370
|
+
};
|
|
371
|
+
/** The ownership tree, one line per node, children under their owner. */
|
|
372
|
+
const formatTree = (roots) => {
|
|
373
|
+
const lines = [];
|
|
374
|
+
const walk = (node, lead, branch) => {
|
|
375
|
+
lines.push({
|
|
376
|
+
label: `${lead}${branch}${node.id}`,
|
|
377
|
+
node
|
|
378
|
+
});
|
|
379
|
+
const next = lead + (branch === "├─ " ? "│ " : branch === "" ? "" : " ");
|
|
380
|
+
node.children.forEach((child, index) => {
|
|
381
|
+
walk(child, next, index === node.children.length - 1 ? "└─ " : "├─ ");
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
for (const root of roots) walk(root, "", "");
|
|
385
|
+
const label = Math.max(...lines.map((l) => l.label.length));
|
|
386
|
+
const type = Math.max(...lines.map((l) => l.node.type.length));
|
|
387
|
+
return lines.map(({ label: text, node }) => {
|
|
388
|
+
const usage = node.inputTokens + node.outputTokens === 0 ? "" : style("dim", ` ${count(node.inputTokens)} in / ${count(node.outputTokens)} out · ${cost(node.costUsd)}`);
|
|
389
|
+
const state = node.held === true ? `${style("yellow", "‖")} paused` : `${STATE_MARK[node.state]} ${stateText(node)}`;
|
|
390
|
+
return `${text.padEnd(label)} ${style("dim", node.type.padEnd(type))} ${state}${usage}`;
|
|
391
|
+
});
|
|
392
|
+
};
|
|
393
|
+
export { formatRunEvent as a, formatEvent as i, formatRuns as n, createTerminalHuman as o, formatTree as r, ago as t };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { p as runMarkerIn } from "./src-UWkk1sl9.mjs";
|
|
2
|
+
// src/commands/in-a-run.ts
|
|
3
|
+
/**
|
|
4
|
+
* @file The commands that act as the user refuse a program or an agent in a run
|
|
5
|
+
* (ADR-047). The refusal names the variable it saw, for someone at their own
|
|
6
|
+
* terminal who inherited it. It stops accidents; it is not a boundary.
|
|
7
|
+
*/
|
|
8
|
+
const refusal = (why, env) => {
|
|
9
|
+
const marker = runMarkerIn(env);
|
|
10
|
+
return marker === null ? null : `${why} ${marker} is set here. If this is your own terminal, it inherited that from a program in a run (a tmux server started by one, say); open a shell without it.`;
|
|
11
|
+
};
|
|
12
|
+
/** Why `ork watch --accept` refuses here, or `null` when it may run. */
|
|
13
|
+
const acceptRefusal = (env = process.env) => refusal("`ork watch --accept` approves code to run with nobody there, so a program or an agent in a run cannot use it.", env);
|
|
14
|
+
/** Why `ork chat` refuses here, or `null` when it may run. */
|
|
15
|
+
const chatRefusal = (env = process.env) => refusal("`ork chat` speaks as you, so a program or an agent in a run cannot use it. A program writes to a session with `ork send`.", env);
|
|
16
|
+
/** Why `ork decide` will not answer here, or `null` when it may. */
|
|
17
|
+
const decideRefusal = (env = process.env) => refusal("`ork decide` answers as you, so a program or an agent in a run cannot answer with it; `ork decide --list` still shows what is open.", env);
|
|
18
|
+
/** Why `ork continue` refuses here, or `null` when it may run. */
|
|
19
|
+
const continueRefusal = (env = process.env) => refusal("`ork continue` undoes a pause you set, so a program or an agent in a run cannot use it.", env);
|
|
20
|
+
/** Whether `ork ui` may print its sign-in link, which acts as the user. */
|
|
21
|
+
const mayShowSignIn = (env = process.env) => runMarkerIn(env) === null;
|
|
22
|
+
export { mayShowSignIn as a, decideRefusal as i, chatRefusal as n, continueRefusal as r, acceptRefusal as t };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { a as limitsNow, i as NOT_STARTED, o as limitsReport, r as capsHere } from "./src-CGqMati3.mjs";
|
|
2
|
+
// src/run/limits.ts
|
|
3
|
+
/**
|
|
4
|
+
* @file The limits across runs that apply here (RFC-007): the user's settings
|
|
5
|
+
* and the project's rules, read fresh for each command, and what a run does
|
|
6
|
+
* when one is spent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* What a run started here is held to. The files are read each time, so a cap
|
|
10
|
+
* changed during a run of hours reaches it; one that breaks meanwhile keeps the
|
|
11
|
+
* last it could read.
|
|
12
|
+
*/
|
|
13
|
+
const runLimits = (store, place, home) => {
|
|
14
|
+
const first = capsHere(home, place);
|
|
15
|
+
let { caps } = first;
|
|
16
|
+
return {
|
|
17
|
+
problems: first.problems,
|
|
18
|
+
now: () => {
|
|
19
|
+
const read = capsHere(home, place);
|
|
20
|
+
if (read.problems.length === 0) ({caps} = read);
|
|
21
|
+
return limitsNow(store, caps, /* @__PURE__ */ new Date());
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Whether what is left of a workflow can spend: a tool-only one, or a resume
|
|
27
|
+
* with only tools to go, is not held back by a quota.
|
|
28
|
+
*/
|
|
29
|
+
const spendsAnything = (workflow, history) => Object.entries(workflow.nodes).some(([id, node]) => (node.type === "claude.leader" || node.type === "claude.worker" || node.type === "workflow.call") && history?.get(id)?.at(-1)?.ok !== true);
|
|
30
|
+
/** Why a run won't start: a settings file it can't read, or a spent limit. */
|
|
31
|
+
const limitsRefusal = (limits, workflow, history) => {
|
|
32
|
+
if (!spendsAnything(workflow, history)) return [];
|
|
33
|
+
if (limits.problems.length > 0) return limits.problems;
|
|
34
|
+
const { blocked } = limits.now();
|
|
35
|
+
return blocked === void 0 ? [] : [`${NOT_STARTED}${blocked}.`];
|
|
36
|
+
};
|
|
37
|
+
/** What `ork usage` shows. */
|
|
38
|
+
const limitsHere = (store, place, home) => limitsReport(store, capsHere(home, place), /* @__PURE__ */ new Date());
|
|
39
|
+
/** A line for `ork chat`, which counts and isn't stopped; `null` with no limit. */
|
|
40
|
+
const chatNotice = (store, place, home) => {
|
|
41
|
+
const { blocked } = runLimits(store, place, home).now();
|
|
42
|
+
return blocked === void 0 ? null : `Note: ${blocked}. This chat counts towards it.`;
|
|
43
|
+
};
|
|
44
|
+
export { runLimits as i, limitsHere as n, limitsRefusal as r, chatNotice as t };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { D as git, O as gitOk, k as lines } from "./src-CGqMati3.mjs";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
// ../core/src/worktree/list.ts
|
|
4
|
+
/**
|
|
5
|
+
* @file What `ork worktrees` shows: every worktree and branch Ork's runs
|
|
6
|
+
* recorded creating, whatever the project's git rules named them, and any
|
|
7
|
+
* `ork/*` branch besides, with their state read from git itself. What clean
|
|
8
|
+
* and remove do with them is clean.ts.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The worktrees the project's runs recorded creating, oldest run first; one
|
|
12
|
+
* entry per branch or folder, from the latest run that created it.
|
|
13
|
+
*/
|
|
14
|
+
const createdWorktrees = (store, project) => {
|
|
15
|
+
const byName = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const run of store.runs({ project }).toReversed()) for (const { event } of store.events(run.id)) {
|
|
17
|
+
if (event.type !== "worktree" || event.action !== "created") continue;
|
|
18
|
+
byName.set(event.branch ?? event.path, {
|
|
19
|
+
runId: run.id,
|
|
20
|
+
nodeId: event.id,
|
|
21
|
+
path: event.path,
|
|
22
|
+
...event.branch === void 0 ? {} : { branch: event.branch }
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return [...byName.values()];
|
|
26
|
+
};
|
|
27
|
+
const parsePorcelain = (text) => text.split("\n\n").map((block) => lines(block)).map((fields) => {
|
|
28
|
+
const value = (key) => fields.find((f) => f.startsWith(`${key} `))?.slice(key.length + 1);
|
|
29
|
+
return {
|
|
30
|
+
path: value("worktree") ?? "",
|
|
31
|
+
branch: (value("branch") ?? "").replace(/^refs\/heads\//u, ""),
|
|
32
|
+
prunable: fields.some((f) => f.startsWith("prunable"))
|
|
33
|
+
};
|
|
34
|
+
}).filter((entry) => entry.path !== "");
|
|
35
|
+
/**
|
|
36
|
+
* The `ork/<run>/<node>` branches of runs that recorded nothing; with no path
|
|
37
|
+
* recorded, the worktree is whichever has the branch checked out.
|
|
38
|
+
*/
|
|
39
|
+
const prefixed = (branch) => {
|
|
40
|
+
const [, runId = "", ...rest] = branch.split("/");
|
|
41
|
+
return {
|
|
42
|
+
runId,
|
|
43
|
+
nodeId: rest.join("/"),
|
|
44
|
+
path: "",
|
|
45
|
+
branch
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
/** How many commits a detached worktree holds that no branch, tag or remote has. */
|
|
49
|
+
const onNoBranch = async (entry) => entry === void 0 ? "0" : (await git(entry.path, [
|
|
50
|
+
"rev-list",
|
|
51
|
+
"--count",
|
|
52
|
+
"HEAD",
|
|
53
|
+
"--not",
|
|
54
|
+
"--branches",
|
|
55
|
+
"--tags",
|
|
56
|
+
"--remotes"
|
|
57
|
+
])).stdout;
|
|
58
|
+
/** One created worktree or branch, with its state read from git. */
|
|
59
|
+
const stateOf = async (root, entries, item) => {
|
|
60
|
+
const { branch } = item;
|
|
61
|
+
const entry = branch === void 0 || item.path !== "" ? entries.find((e) => e.path === item.path) : entries.find((e) => e.branch === branch);
|
|
62
|
+
const holder = branch === void 0 ? void 0 : entries.find((e) => e.branch === branch && e.path !== entry?.path);
|
|
63
|
+
const missing = entry !== void 0 && (entry.prunable || !existsSync(entry.path));
|
|
64
|
+
const status = entry === void 0 || missing ? "" : (await git(entry.path, ["status", "--porcelain"])).stdout;
|
|
65
|
+
const checkedOut = branch === void 0 && entry !== void 0 && entry.branch !== "" ? entry.branch : void 0;
|
|
66
|
+
const on = branch ?? checkedOut;
|
|
67
|
+
const counted = on === void 0 ? await onNoBranch(entry === void 0 || missing ? void 0 : entry) : (await git(root, [
|
|
68
|
+
"rev-list",
|
|
69
|
+
"--count",
|
|
70
|
+
`HEAD..${on}`
|
|
71
|
+
])).stdout;
|
|
72
|
+
const ahead = Number(counted);
|
|
73
|
+
const changes = lines(status).length;
|
|
74
|
+
return {
|
|
75
|
+
...branch === void 0 ? {} : { branch },
|
|
76
|
+
...checkedOut === void 0 ? {} : { checkedOut },
|
|
77
|
+
runId: item.runId,
|
|
78
|
+
nodeId: item.nodeId,
|
|
79
|
+
path: entry?.path ?? null,
|
|
80
|
+
missing,
|
|
81
|
+
dirty: changes > 0,
|
|
82
|
+
changes,
|
|
83
|
+
ahead: Number.isNaN(ahead) ? 0 : ahead,
|
|
84
|
+
merged: on !== void 0 && ahead === 0,
|
|
85
|
+
inMainCheckout: entry?.path === root || holder?.path === root,
|
|
86
|
+
...holder === void 0 || holder.path === root ? {} : { heldBy: holder.path }
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Every worktree and branch Ork's runs created in the repository at `root`,
|
|
91
|
+
* with its state: those the runs recorded (`created`, whatever the project's
|
|
92
|
+
* git rules named them), and any `ork/` branch besides. A recorded one that git
|
|
93
|
+
* no longer has is left out.
|
|
94
|
+
*/
|
|
95
|
+
const listOrkWorktrees = async (root, created = []) => {
|
|
96
|
+
const entries = parsePorcelain(await gitOk(root, [
|
|
97
|
+
"worktree",
|
|
98
|
+
"list",
|
|
99
|
+
"--porcelain"
|
|
100
|
+
]));
|
|
101
|
+
const branches = lines(await gitOk(root, [
|
|
102
|
+
"for-each-ref",
|
|
103
|
+
"--format=%(refname:short)",
|
|
104
|
+
"refs/heads/"
|
|
105
|
+
]));
|
|
106
|
+
const recordedBranches = new Set(created.flatMap((c) => c.branch ?? []));
|
|
107
|
+
const items = [...created.filter((c) => c.branch === void 0 ? entries.some((e) => e.path === c.path) : branches.includes(c.branch)), ...branches.filter((b) => b.startsWith("ork/") && !recordedBranches.has(b)).map((b) => prefixed(b))];
|
|
108
|
+
return Promise.all(items.map((item) => stateOf(root, entries, item)));
|
|
109
|
+
};
|
|
110
|
+
export { listOrkWorktrees as n, createdWorktrees as t };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { m as isNewer } from "./src-CGqMati3.mjs";
|
|
2
|
+
import { n as ORK_VERSION } from "./__build_info__-CtSVVuZE.mjs";
|
|
3
|
+
import { a as openMcp, c as refused, o as tool, s as answer } from "./src-CpDKIulI.mjs";
|
|
4
|
+
import { n as explainNode } from "./context-C_bksb0C.mjs";
|
|
5
|
+
import { p as runMarkerIn } from "./src-UWkk1sl9.mjs";
|
|
6
|
+
import { defineCommand } from "citty";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
// src/commands/mcp.ts
|
|
11
|
+
/**
|
|
12
|
+
* @file `ork mcp`: Ork's tools for Claude Code over stdio (RFC-012), on the
|
|
13
|
+
* same handlers as `ork ui`'s API and the same checks as `ork validate`. They
|
|
14
|
+
* read, check a draft, explain a node's context, and start a run; none
|
|
15
|
+
* answers a decision or acts on a run. Inside a run nothing starts another,
|
|
16
|
+
* and an `ork` older than its caller expects says so on every call. It opens
|
|
17
|
+
* no port and holds no token, and stdout carries only the protocol.
|
|
18
|
+
*/
|
|
19
|
+
const INSTRUCTIONS = "Ork runs workflows of Claude sessions and programs on this machine. These tools read what Ork has (projects, workflows, the workflow format, runs, their events and cost, the decisions waiting on a person), check a workflow as `ork validate` does, and explain what a node's session loads. None answers a decision: a person does, in `ork ui` or their own terminal.";
|
|
20
|
+
const STARTS = " `start_run` starts a run in the background.";
|
|
21
|
+
const explainTool = (opened, home) => tool("explain_context", "What one node's session would load, as `ork context explain` shows it: its agent, instructions, skills and files, layer by layer, with their tokens.", {
|
|
22
|
+
project: z.string().min(1),
|
|
23
|
+
workflow: z.string().min(1).describe("A workflow's name, or a path"),
|
|
24
|
+
node: z.string().min(1)
|
|
25
|
+
}, ({ project, workflow, node }) => {
|
|
26
|
+
const found = opened.fileOf(project, workflow);
|
|
27
|
+
if ("error" in found) return refused(found.error);
|
|
28
|
+
const result = explainNode(found.file, node, found.project.root, home);
|
|
29
|
+
return result.ok ? answer(result.explanation) : refused(result.lines.join("\n"));
|
|
30
|
+
});
|
|
31
|
+
/** Every call refused, when this `ork` is older than its caller needs. */
|
|
32
|
+
const tooOld = (tools, expects) => tools.map((each) => ({
|
|
33
|
+
...each,
|
|
34
|
+
call: () => Promise.resolve(refused(`This needs Ork ${expects} or newer, and this is ${ORK_VERSION}. Ask the person to upgrade, with \`ork upgrade\`.`))
|
|
35
|
+
}));
|
|
36
|
+
/** The server with Ork's tools on it, for stdio here and in tests. */
|
|
37
|
+
const orkMcpServer = (options = {}) => {
|
|
38
|
+
const start = runMarkerIn(options.env ?? process.env) === null;
|
|
39
|
+
const opened = openMcp({
|
|
40
|
+
version: ORK_VERSION,
|
|
41
|
+
start,
|
|
42
|
+
...options.home === void 0 ? {} : { home: options.home },
|
|
43
|
+
...options.script === void 0 ? {} : { script: options.script }
|
|
44
|
+
});
|
|
45
|
+
const all = [...opened.tools, explainTool(opened, options.home)];
|
|
46
|
+
const tools = options.expects !== void 0 && isNewer(options.expects, "0.4.0") ? tooOld(all, options.expects) : all;
|
|
47
|
+
const server = new McpServer({
|
|
48
|
+
name: "ork",
|
|
49
|
+
version: ORK_VERSION
|
|
50
|
+
}, { instructions: start ? `${INSTRUCTIONS}${STARTS}` : INSTRUCTIONS });
|
|
51
|
+
for (const each of tools) server.registerTool(each.name, {
|
|
52
|
+
description: each.description,
|
|
53
|
+
inputSchema: z.strictObject(each.shape),
|
|
54
|
+
annotations: {
|
|
55
|
+
readOnlyHint: each.readOnly,
|
|
56
|
+
openWorldHint: false
|
|
57
|
+
}
|
|
58
|
+
}, async (args) => {
|
|
59
|
+
const { text, isError } = await each.call(args);
|
|
60
|
+
return {
|
|
61
|
+
content: [{
|
|
62
|
+
type: "text",
|
|
63
|
+
text
|
|
64
|
+
}],
|
|
65
|
+
...isError === true ? { isError } : {}
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
server,
|
|
70
|
+
close: opened.close
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const mcp = defineCommand({
|
|
74
|
+
meta: {
|
|
75
|
+
name: "mcp",
|
|
76
|
+
description: "Serve Ork's tools to Claude Code over stdio (MCP)"
|
|
77
|
+
},
|
|
78
|
+
args: { expects: {
|
|
79
|
+
type: "string",
|
|
80
|
+
description: "The oldest Ork the caller works with, such as a plugin"
|
|
81
|
+
} },
|
|
82
|
+
run: async ({ args }) => {
|
|
83
|
+
const { server, close } = orkMcpServer(args.expects === void 0 ? {} : { expects: args.expects });
|
|
84
|
+
const transport = new StdioServerTransport();
|
|
85
|
+
transport.onclose = () => {
|
|
86
|
+
close();
|
|
87
|
+
};
|
|
88
|
+
await server.connect(transport);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
export { mcp };
|