@krapht/ork 1.0.0-alpha.0 → 1.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +5 -6
  2. package/dist/{__build_info__-DdTVd2p5.mjs → __build_info__-DFF18K8y.mjs} +72 -3
  3. package/dist/bin.mjs +603 -384
  4. package/dist/{clean-NnboOuz6.mjs → clean-DVaEH9e7.mjs} +5 -2
  5. package/dist/{completion-snnYGjgK.mjs → completion-Dv5kf598.mjs} +6 -6
  6. package/dist/context-CEaKOVmG.mjs +683 -0
  7. package/dist/{decide-B81wCZQg.mjs → decide-3l9q8UtX.mjs} +3 -3
  8. package/dist/{find-BKW-uGey.mjs → find-DEjjOlTG.mjs} +1 -1
  9. package/dist/format-Cb5_m-Qd.mjs +191 -0
  10. package/dist/{in-a-run-BPXX09Bs.mjs → in-a-run-BS9Nc-JW.mjs} +1 -1
  11. package/dist/{limits-JCve2QpI.mjs → limits-BSwEjQST.mjs} +1 -1
  12. package/dist/{list-C7UEgmtd.mjs → list-Qxr6rwsY.mjs} +11 -4
  13. package/dist/{mcp-CKPyNaKD.mjs → mcp-CslnG5wf.mjs} +6 -6
  14. package/dist/output-Bqljtmp_.mjs +40 -0
  15. package/dist/{pause-B_SL_yDz.mjs → pause-D7OVB5Ac.mjs} +4 -4
  16. package/dist/{relay-o-Luee1c.mjs → relay-EceRIebO.mjs} +2 -2
  17. package/dist/{remove-DAZ7ZCzt.mjs → remove-0IQZVWmI.mjs} +14 -7
  18. package/dist/{send-3B64fBNB.mjs → send-C0uwfUYi.mjs} +3 -3
  19. package/dist/{src-DHvWU4tX.mjs → src-Df6KOIDC.mjs} +5682 -3248
  20. package/dist/{src-C38BPzit.mjs → src-cmmXKHn1.mjs} +266 -203
  21. package/dist/{src-BJTegEzY.mjs → src-i5k2Y-kf.mjs} +40 -88
  22. package/dist/src-oaJYIddp.mjs +2255 -0
  23. package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
  24. package/dist/ui/assets/app-C8uhHVYp.css +1 -0
  25. package/dist/ui/assets/app-qs8Lofv6.js +75 -0
  26. package/dist/ui/assets/{index-D83F2OKZ.js → index-DV1ZaOk-.js} +3 -3
  27. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  28. package/dist/ui/index.html +2 -2
  29. package/dist/{ui-CY4xxJUu.mjs → ui-CBkHJdqk.mjs} +4 -4
  30. package/dist/{usage-CJnytoe4.mjs → usage-Cd8pEYyK.mjs} +5 -5
  31. package/dist/{watch-BJB82ZJ7.mjs → watch-DW5R2OPO.mjs} +6 -6
  32. package/dist/{workflows-CiM7S3l0.mjs → workflows-BT1gDRIV.mjs} +4 -3
  33. package/dist/{worktrees-86ySXxR6.mjs → worktrees-g1MQMGie.mjs} +78 -19
  34. package/package.json +2 -4
  35. package/dist/context-DO3sBy9v.mjs +0 -274
  36. package/dist/format-CmTlTOjT.mjs +0 -427
  37. package/dist/output-B1V6ZJSx.mjs +0 -950
  38. package/dist/src-Dyz_Gy1q.mjs +0 -1183
  39. package/dist/ui/assets/app-C8nJ6NpT.js +0 -67
  40. package/dist/ui/assets/app-CH7YC3in.css +0 -1
  41. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
@@ -1,427 +0,0 @@
1
- import { ht as liveStatus } from "./src-DHvWU4tX.mjs";
2
- import { i as style, n as cost } from "./output-B1V6ZJSx.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. After a choice it offers one optional line for a
9
- * note, which Enter skips. In a run it is the decision queue's local
10
- * answerer, racing `ork decide`.
11
- */
12
- const PREVIEW = 160;
13
- const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
14
- const NOTE_PROMPT = " Note for the run (optional, Enter to skip): ";
15
- const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
16
- /** One line that shows what a tool is about to do. */
17
- const previewInput = (toolName, input) => {
18
- const key = [
19
- "command",
20
- "file_path",
21
- "path",
22
- "url",
23
- "pattern"
24
- ].find((k) => typeof input[k] === "string");
25
- const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
26
- return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
27
- };
28
- /** The option an answer names: its number, or its id. */
29
- const pickOption = (answer, options, recommended) => {
30
- const value = answer.trim();
31
- if (value === "") return recommended;
32
- const index = Number(value);
33
- if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
34
- return options.find((o) => o.id === value)?.id;
35
- };
36
- const optionLine = (option, index, recommended) => {
37
- const label = option.label === void 0 ? "" : ` ${option.label}`;
38
- const description = option.description === void 0 ? "" : style("dim", ` - ${option.description}`);
39
- const mark = option.id === recommended ? style("green", " (recommended)") : "";
40
- return ` ${String(index + 1)}) ${style("bold", option.id)}${label}${description}${mark}`;
41
- };
42
- /**
43
- * Answers permission prompts and `ask_user` questions on the terminal, one at a
44
- * time. Without a terminal it denies tools and leaves questions to the node. A
45
- * withdrawn prompt (answered elsewhere) rejects, which frees the turn.
46
- */
47
- const createTerminalHuman = (io) => {
48
- let rl;
49
- let turn = Promise.resolve();
50
- const say = (text) => {
51
- io.output.write(`${text}\n`);
52
- };
53
- const prompt = (text, signal) => {
54
- rl ??= createInterface({
55
- input: io.input,
56
- output: io.output
57
- });
58
- return signal === void 0 ? rl.question(text) : rl.question(text, { signal });
59
- };
60
- /** The optional line after a choice, when the asker takes a note. */
61
- const takeNote = async (onNote, signal) => {
62
- if (onNote === void 0) return;
63
- const note = (await prompt(NOTE_PROMPT, signal)).trim();
64
- if (note !== "") onNote(note);
65
- };
66
- const choose = async (hint, options, recommended, signal) => {
67
- const picked = pickOption(await prompt(` Choose${hint}: `, signal), options, recommended);
68
- if (picked !== void 0) return picked;
69
- say(style("dim", " Type an option's number or id."));
70
- return choose(hint, options, recommended, signal);
71
- };
72
- const oneAtATime = (work) => {
73
- const result = turn.then(work);
74
- turn = result.catch(() => null);
75
- return result;
76
- };
77
- return {
78
- permit: ({ nodeId, toolName, input, signal, onNote }) => oneAtATime(async () => {
79
- if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
80
- say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} wants to run ${previewInput(toolName, input)}`);
81
- if (!io.interactive) {
82
- say(style("dim", ` denied: ${NO_ONE_TO_PERMIT}`));
83
- return {
84
- allow: false,
85
- message: NO_ONE_TO_PERMIT
86
- };
87
- }
88
- const answer = (await prompt(" Allow? [y/N] ", signal)).trim().toLowerCase();
89
- if (answer === "y" || answer === "yes") return { allow: true };
90
- await takeNote(onNote, signal);
91
- return {
92
- allow: false,
93
- message: "The user did not allow this tool."
94
- };
95
- }),
96
- ask: ({ nodeId, question, options, recommended, context, signal, onNote }) => oneAtATime(async () => {
97
- if (signal?.aborted === true) throw new Error("the question was withdrawn");
98
- say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} asks: ${question}`);
99
- if (context !== void 0) say(style("dim", ` ${context}`));
100
- for (const [index, option] of options.entries()) say(optionLine(option, index, recommended));
101
- if (!io.interactive) {
102
- const answer = recommended ?? NO_ONE_TO_ASK;
103
- say(style("dim", ` no terminal; answered ${answer}`));
104
- return answer;
105
- }
106
- const fallback = options.findIndex((o) => o.id === recommended);
107
- const hint = fallback === -1 ? "" : ` [${String(fallback + 1)}]`;
108
- const picked = await choose(hint, options, recommended, signal);
109
- await takeNote(onNote, signal);
110
- return picked;
111
- }),
112
- close: () => {
113
- rl?.close();
114
- }
115
- };
116
- };
117
- // src/run/format-bus.ts
118
- /** The first line of a message body, cut to fit one terminal line. */
119
- const firstLine = (body) => {
120
- const [line = ""] = body.trim().split("\n");
121
- return line.length > 100 ? `${line.slice(0, 99)}…` : line;
122
- };
123
- /** Messages, owner questions and claims, without the node prefix. */
124
- const formatBusEvent = (event) => {
125
- if (event.type === "claim.taken") return [style("dim", `⚑ claimed ${event.resource}`)];
126
- if (event.type === "claim.refused") {
127
- const held = event.held === event.resource ? "" : ` (it holds ${event.held})`;
128
- return [style("yellow", `⚑ ${event.resource} is ${event.holder}'s${held}`)];
129
- }
130
- if (event.type === "claim.released") return [style("dim", `⚑ released ${event.resource}`)];
131
- if (event.type === "message.sent") {
132
- const how = event.via === void 0 ? "" : ` (${event.via})`;
133
- return [style("dim", `✉ to ${event.to}${how}: ${firstLine(event.body)}`)];
134
- }
135
- if (event.type === "message.received") {
136
- const how = event.via === void 0 ? "" : ` (${event.via})`;
137
- return [style("dim", `✉ from ${event.from}${how}: ${firstLine(event.body)}`)];
138
- }
139
- if (event.type === "call.started") return [style("dim", `↳ started ${event.workflow} as run ${event.run}`)];
140
- if (event.type === "link.notice") return [style("dim", `↔ ${event.text}`)];
141
- if (event.type === "message.read") return [];
142
- if (event.type === "owner.asked") return [`${style(["yellow", "bold"], "?")} asked as the owner of ${event.about}: ${event.text} ${style("dim", `(${event.question})`)}`];
143
- if (event.type === "owner.answered") return [style("dim", `→ ${event.answer} (owner)`)];
144
- return [style("dim", "→ its session ended first; asking you")];
145
- };
146
- // src/run/format-result.ts
147
- const RUN_MARK = {
148
- succeeded: style("green", "✓"),
149
- failed: style("red", "✗"),
150
- budget_exhausted: style("yellow", "!"),
151
- quota_exhausted: style("yellow", "!"),
152
- timed_out: style("yellow", "!"),
153
- interrupted: style("yellow", "!")
154
- };
155
- /** `1 failure handled`, `2 failures handled`. */
156
- const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" : "failures"} handled`;
157
- /**
158
- * The run summary: status, cost, what ran, what was skipped and which failures
159
- * an edge handled, branches to look at, why, and mail to nodes that never ran.
160
- */
161
- const formatRunResult = (result) => {
162
- const ran = result.nodes.filter((n) => n.runs > 0).length;
163
- const skipped = result.skipped ?? result.nodes.length - ran;
164
- const handled = result.failuresHandled ?? 0;
165
- const lines = ["", `${RUN_MARK[result.status]} run ${result.runId} ${result.status.replace("_", " ")} ${style("dim", `· ${cost(result.costUsd)} · ${String(ran)} ran, ${String(skipped)} skipped${handled === 0 ? "" : `, ${failuresHandled(handled)}`}`)}`];
166
- for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
167
- for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
168
- for (const { node, messages } of result.unread ?? []) lines.push(` ${style(["yellow", "bold"], "warning")} ${node} never ran, so ${messages === 1 ? "1 message" : `${String(messages)} messages`} to it went unread`);
169
- return lines;
170
- };
171
- // src/run/format.ts
172
- /** Who answered a decision, as the log says it. */
173
- const ANSWERED_BY = {
174
- terminal: "terminal",
175
- decide: "ork decide",
176
- ui: "ork ui",
177
- owner: "its owner"
178
- };
179
- const STATUS_MARK = {
180
- pass: style("green", "✓"),
181
- done: style("green", "✓"),
182
- changes_requested: style("yellow", "!"),
183
- fail: style("red", "✗")
184
- };
185
- const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
186
- const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${line}`);
187
- /** The usage line: cost, tokens, turns and time. */
188
- const formatUsage = ({ usage }) => style("dim", [
189
- cost(usage.costUsd),
190
- `${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
191
- `${String(usage.turns)} turns`,
192
- seconds(usage.durationMs)
193
- ].join(" · "));
194
- /** How a finished session reads on the terminal. */
195
- const formatOutcome = (nodeId, outcome) => {
196
- const { report } = outcome;
197
- if (report === null) return [
198
- `${style("red", "✗")} ${style("bold", nodeId)} ended without a report (${outcome.subtype})`,
199
- ...outcome.errors.map((e) => ` ${style("red", e)}`),
200
- ` ${formatUsage(outcome)}`
201
- ];
202
- const lines = [`${STATUS_MARK[report.status] ?? "-"} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
203
- if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
204
- for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
205
- for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
206
- lines.push(` ${formatUsage(outcome)}`);
207
- return lines;
208
- };
209
- /** The transcript lines for one event; tool names come from `tools`. */
210
- const formatEvent = (nodeId, event, tools) => {
211
- if (event.type === "session.started") {
212
- const fork = event.fork === true ? ", a fork to answer a question" : "";
213
- return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId}${fork})`)];
214
- }
215
- if (event.type === "assistant.text") return indent(event.text);
216
- if (event.type === "tool.called") {
217
- tools.set(event.id, event.name);
218
- return [style("dim", ` · ${previewInput(event.name, event.input)}`)];
219
- }
220
- if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
221
- if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
222
- return formatOutcome(nodeId, event.outcome);
223
- };
224
- /** A one-line verdict for a finished node, by what its type outputs. */
225
- const nodeVerdict = (output) => {
226
- const { status, summary, exitCode, choice, answeredBy, count } = output;
227
- if (typeof status === "string") return typeof summary === "string" && summary !== "" ? `${status}: ${summary}` : status;
228
- if (typeof exitCode === "number") return `exit ${String(exitCode)}`;
229
- if (typeof choice === "string") return `${choice} (${String(answeredBy)})`;
230
- if (typeof count === "number") return `${String(count)} ran`;
231
- return "done";
232
- };
233
- /** Transcript lines for one run event, prefixed with the node they are from. */
234
- const formatRunEvent = (event, width, tools) => {
235
- const tag = (id) => style("cyan", `${id.padEnd(width)} │`);
236
- 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"}`)]];
237
- if (event.type === "node.started") {
238
- const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
239
- const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
240
- return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}`)}`];
241
- }
242
- if (event.type === "node.event") {
243
- const inner = event.event;
244
- if (inner.type === "session.finished") return [];
245
- const fork = inner.fork === true ? style("dim", "fork · ") : "";
246
- if (inner.type === "session.started") {
247
- const why = inner.fork === true ? ", to answer a question" : "";
248
- return [`${tag(event.id)} ${fork}${style("dim", `${inner.model}, session ${inner.sessionId}${why}`)}`];
249
- }
250
- return formatEvent(event.id, event.event, tools).map((line) => `${tag(event.id)} ${fork}${line.replace(/^ {2}/u, "")}`);
251
- }
252
- if (event.type === "node.finished") {
253
- const mark = event.ok ? style("green", "✓") : style("red", "✗");
254
- const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
255
- return [`${tag(event.id)} ${mark} ${nodeVerdict(event.output)}${spent}`];
256
- }
257
- if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
258
- if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
259
- if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
260
- if (event.type === "watch.changed") return [`${tag(event.id)} ${style("dim", `look ${String(event.looks)}:`)} ${event.said}`];
261
- if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
262
- if (event.type === "edge.taken") return [];
263
- if (event.type === "worktree") {
264
- const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
265
- const line = `${event.action} ${event.branch ?? event.path}${detail}`;
266
- return [`${tag(event.id)} ${event.action === "conflict" ? style("yellow", `! ${line}`) : style("dim", `⎇ ${line}`)}`];
267
- }
268
- if (event.type === "decision.opened") {
269
- const what = event.kind === "permission" ? "asks to" : "asks:";
270
- const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
271
- const where = event.owner === void 0 ? `decision ${event.decision}; ork decide` : `decision ${event.decision}, via ${event.owner}`;
272
- return [`${tag(event.node)} ${style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
273
- }
274
- if (event.type === "decision.answered") {
275
- const by = event.by === "owner" && event.owner !== void 0 ? event.owner : ANSWERED_BY[event.by];
276
- const said = event.note === void 0 ? "" : ` · said: ${event.note.replaceAll(/\s+/gu, " ")}`;
277
- return [`${tag(event.node)} ${style("dim", `→ ${event.answer} (${by})${said}`)}`];
278
- }
279
- if (event.type === "decision.withdrawn") return [`${tag(event.node)} ${style("dim", "→ question withdrawn")}`];
280
- if (event.type === "run.command") {
281
- const node = event.node === void 0 ? "" : ` --node ${event.node}`;
282
- return [style(["yellow", "bold"], `! ork ${event.command}${node} received`)];
283
- }
284
- if (event.type === "node.held") {
285
- const at = event.at === "tool" ? "its tool call waits for ork continue" : "it starts after ork continue";
286
- return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
287
- }
288
- if (event.type === "node.released") return [`${tag(event.id)} ${style("dim", "▸ continues")}`];
289
- if (event.type === "run.finished") return formatRunResult(event.result);
290
- if (event.type === "run.resumed") {
291
- const [one] = event.kept;
292
- const kept = one === void 0 ? "every node runs again" : event.kept.length === 1 ? `${one} keeps its result` : `${event.kept.join(", ")} keep their results`;
293
- return ["", style("dim", `▸ resumed: ${kept}`)];
294
- }
295
- if (event.type === "feedback.recorded") {
296
- const { target, kind, title } = event.feedback;
297
- const line = style("dim", `✎ feedback (${target}, ${kind}): ${title}`);
298
- return [event.node === void 0 ? line : `${tag(event.node)} ${line}`];
299
- }
300
- if (event.type === "user.message") {
301
- const note = event.delivered === "queued" ? " (for its next session)" : "";
302
- return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
303
- }
304
- return formatBusEvent(event).map((line) => `${tag(event.node)} ${line}`);
305
- };
306
- // src/runs/format.ts
307
- /**
308
- * @file How stored runs read on the terminal: the `ork ps` table and the `ork
309
- * tree` ownership tree with each node's state, tokens and cost, and what is
310
- * paused (ADR-041).
311
- */
312
- const STATUS_STYLE = {
313
- running: "cyan",
314
- paused: "yellow",
315
- succeeded: "green",
316
- failed: "red",
317
- crashed: "red",
318
- budget_exhausted: "yellow",
319
- quota_exhausted: "yellow",
320
- timed_out: "yellow",
321
- interrupted: "yellow"
322
- };
323
- /** `just now`, `5m ago`, `3h ago`, `2d ago`, then the date. */
324
- const ago = (iso, now = Date.now()) => {
325
- const minutes = Math.floor((now - Date.parse(iso)) / 6e4);
326
- if (minutes < 1) return "just now";
327
- if (minutes < 60) return `${String(minutes)}m ago`;
328
- if (minutes < 1440) return `${String(Math.floor(minutes / 60))}h ago`;
329
- if (minutes < 10080) return `${String(Math.floor(minutes / 1440))}d ago`;
330
- return iso.slice(0, 10);
331
- };
332
- /** `42s`, `3m12s`, `1h05m`. */
333
- const duration = (ms) => {
334
- const s = Math.max(0, Math.round(ms / 1e3));
335
- if (s < 60) return `${String(s)}s`;
336
- if (s < 3600) return `${String(Math.floor(s / 60))}m${String(s % 60).padStart(2, "0")}s`;
337
- return `${String(Math.floor(s / 3600))}h${String(Math.floor(s % 3600 / 60)).padStart(2, "0")}m`;
338
- };
339
- /**
340
- * The `ork ps` table; `withProject` adds each run's project. A called run's
341
- * cost is dimmed and says its caller's, whose cost already counts it.
342
- */
343
- const formatRuns = (runs, withProject, now = Date.now(), statusOf = liveStatus) => {
344
- if (runs.length === 0) return ["No runs."];
345
- const rows = runs.map((run) => {
346
- const status = statusOf(run);
347
- const end = run.finishedAt === void 0 ? now : Date.parse(run.finishedAt);
348
- return {
349
- id: run.id,
350
- workflow: run.workflow,
351
- status,
352
- when: `${ago(run.startedAt, now)} · ${duration(end - Date.parse(run.startedAt))}`,
353
- cost: run.parent === void 0 ? cost(run.costUsd) : `${cost(run.costUsd)} in ${run.parent}`,
354
- called: run.parent !== void 0,
355
- project: run.project
356
- };
357
- });
358
- const width = (pick, title) => Math.max(title.length, ...rows.map((r) => pick(r).length));
359
- const w = {
360
- id: width((r) => r.id, "RUN"),
361
- workflow: width((r) => r.workflow, "WORKFLOW"),
362
- status: width((r) => r.status, "STATUS"),
363
- when: width((r) => r.when, "STARTED"),
364
- cost: width((r) => r.cost, "COST")
365
- };
366
- const header = [
367
- "RUN".padEnd(w.id),
368
- "WORKFLOW".padEnd(w.workflow),
369
- "STATUS".padEnd(w.status),
370
- "STARTED".padEnd(w.when),
371
- "COST".padStart(w.cost),
372
- ...withProject ? ["PROJECT"] : []
373
- ].join(" ");
374
- return [style("dim", header), ...rows.map((r) => [
375
- style("bold", r.id.padEnd(w.id)),
376
- r.workflow.padEnd(w.workflow),
377
- style(STATUS_STYLE[r.status] ?? "reset", r.status.padEnd(w.status)),
378
- style("dim", r.when.padEnd(w.when)),
379
- r.called ? style("dim", r.cost.padStart(w.cost)) : r.cost.padStart(w.cost),
380
- ...withProject ? [style("dim", r.project)] : []
381
- ].join(" "))];
382
- };
383
- const STATE_MARK = {
384
- waiting: style("dim", "·"),
385
- running: style("cyan", "●"),
386
- ok: style("green", "✓"),
387
- failed: style("red", "✗"),
388
- skipped: style("dim", "-")
389
- };
390
- /** `950`, `12.3k`, `1.2M`. */
391
- const count = (n) => {
392
- if (n < 1e3) return String(n);
393
- if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
394
- return `${(n / 1e6).toFixed(1)}M`;
395
- };
396
- const stateText = (node) => {
397
- if (node.state === "running") return node.runs > 1 ? `running (round ${String(node.runs)})` : "running";
398
- if (node.state === "ok" || node.state === "failed") {
399
- const verdict = nodeVerdict(node.output ?? {});
400
- const line = verdict.length > 60 ? `${verdict.slice(0, 59)}…` : verdict;
401
- return node.runs > 1 ? `${line} (${String(node.runs)} runs)` : line;
402
- }
403
- return node.state;
404
- };
405
- /** The ownership tree, one line per node, children under their owner. */
406
- const formatTree = (roots) => {
407
- const lines = [];
408
- const walk = (node, lead, branch) => {
409
- lines.push({
410
- label: `${lead}${branch}${node.id}`,
411
- node
412
- });
413
- const next = lead + (branch === "├─ " ? "│ " : branch === "" ? "" : " ");
414
- node.children.forEach((child, index) => {
415
- walk(child, next, index === node.children.length - 1 ? "└─ " : "├─ ");
416
- });
417
- };
418
- for (const root of roots) walk(root, "", "");
419
- const label = Math.max(...lines.map((l) => l.label.length));
420
- const type = Math.max(...lines.map((l) => l.node.type.length));
421
- return lines.map(({ label: text, node }) => {
422
- const usage = node.inputTokens + node.outputTokens === 0 ? "" : style("dim", ` ${count(node.inputTokens)} in / ${count(node.outputTokens)} out · ${cost(node.costUsd)}`);
423
- const state = node.held === true ? `${style("yellow", "‖")} paused` : `${STATE_MARK[node.state]} ${stateText(node)}`;
424
- return `${text.padEnd(label)} ${style("dim", node.type.padEnd(type))} ${state}${usage}`;
425
- });
426
- };
427
- export { formatRunEvent as a, formatEvent as i, formatRuns as n, createTerminalHuman as o, formatTree as r, ago as t };