@smithers-orchestrator/cli 0.16.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.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +55 -0
  3. package/src/AgentAvailability.ts +13 -0
  4. package/src/AgentAvailabilityStatus.ts +5 -0
  5. package/src/AggregateNodeDetailParams.ts +5 -0
  6. package/src/AskOptions.ts +12 -0
  7. package/src/ChatAttemptMeta.ts +7 -0
  8. package/src/ChatAttemptRow.ts +12 -0
  9. package/src/ChatOutputEvent.ts +6 -0
  10. package/src/DiffBundleLike.ts +6 -0
  11. package/src/DiscoveredWorkflow.ts +9 -0
  12. package/src/EnrichedNodeDetail.ts +60 -0
  13. package/src/EventCategory.ts +18 -0
  14. package/src/FindDbWaitOptions.ts +4 -0
  15. package/src/FormatEventLineOptions.ts +4 -0
  16. package/src/HijackCandidate.ts +11 -0
  17. package/src/HijackLaunchSpec.ts +6 -0
  18. package/src/InitWorkflowPackOptions.ts +4 -0
  19. package/src/InitWorkflowPackResult.ts +6 -0
  20. package/src/NativeHijackEngine.ts +8 -0
  21. package/src/NodeDetailAttempt.ts +22 -0
  22. package/src/NodeDetailTokenUsage.ts +11 -0
  23. package/src/NodeDetailToolCall.ts +12 -0
  24. package/src/ParsedNodeOutputEvent.ts +9 -0
  25. package/src/RenderNodeDetailOptions.ts +4 -0
  26. package/src/RunAutoResumeSkipReason.ts +4 -0
  27. package/src/RunDiffCommandInput.ts +13 -0
  28. package/src/RunDiffCommandResult.ts +3 -0
  29. package/src/RunOutputCommandInput.ts +12 -0
  30. package/src/RunOutputCommandResult.ts +3 -0
  31. package/src/RunRewindCommandInput.ts +14 -0
  32. package/src/RunRewindCommandResult.ts +3 -0
  33. package/src/RunTreeCommandInput.ts +14 -0
  34. package/src/RunTreeCommandResult.ts +3 -0
  35. package/src/SmithersEventType.ts +3 -0
  36. package/src/SupervisorOptions.ts +33 -0
  37. package/src/SupervisorPollSummary.ts +6 -0
  38. package/src/TreeRenderOptions.ts +5 -0
  39. package/src/WatchLoopOptions.ts +9 -0
  40. package/src/WatchLoopResult.ts +8 -0
  41. package/src/WatchRenderContext.ts +4 -0
  42. package/src/WhyBlocker.ts +17 -0
  43. package/src/WhyBlockerKind.ts +9 -0
  44. package/src/WhyDiagnosis.ts +10 -0
  45. package/src/WorkflowCta.ts +4 -0
  46. package/src/WorkflowSourceType.ts +1 -0
  47. package/src/agent-detection.js +257 -0
  48. package/src/ask.js +491 -0
  49. package/src/chat.js +226 -0
  50. package/src/diff.js +221 -0
  51. package/src/event-categories.js +141 -0
  52. package/src/find-db.js +93 -0
  53. package/src/format.js +272 -0
  54. package/src/hijack-session.js +207 -0
  55. package/src/hijack.js +226 -0
  56. package/src/index.d.ts +1 -0
  57. package/src/index.js +4868 -0
  58. package/src/mcp/SemanticMcpServerOptions.ts +4 -0
  59. package/src/mcp/SemanticToolCallResult.ts +14 -0
  60. package/src/mcp/SemanticToolContext.ts +6 -0
  61. package/src/mcp/SemanticToolDefinition.ts +13 -0
  62. package/src/mcp/SemanticToolError.ts +6 -0
  63. package/src/mcp/semantic-server.js +41 -0
  64. package/src/mcp/semantic-tools.js +1242 -0
  65. package/src/node-detail.js +682 -0
  66. package/src/output.js +111 -0
  67. package/src/resume-detached.js +37 -0
  68. package/src/rewind.js +88 -0
  69. package/src/scheduler.js +112 -0
  70. package/src/smithersRuntime.js +63 -0
  71. package/src/supervisor.js +418 -0
  72. package/src/tree.js +307 -0
  73. package/src/tui/app.jsx +139 -0
  74. package/src/tui/app.tsx +5 -0
  75. package/src/tui/components/AskModal.jsx +109 -0
  76. package/src/tui/components/AskModal.tsx +3 -0
  77. package/src/tui/components/AttentionPane.jsx +112 -0
  78. package/src/tui/components/AttentionPane.tsx +6 -0
  79. package/src/tui/components/ChatPane.jsx +57 -0
  80. package/src/tui/components/ChatPane.tsx +7 -0
  81. package/src/tui/components/CronList.jsx +87 -0
  82. package/src/tui/components/CronList.tsx +5 -0
  83. package/src/tui/components/DetailsPane.jsx +96 -0
  84. package/src/tui/components/DetailsPane.tsx +7 -0
  85. package/src/tui/components/FramesPane.jsx +147 -0
  86. package/src/tui/components/FramesPane.tsx +8 -0
  87. package/src/tui/components/LogsPane.jsx +46 -0
  88. package/src/tui/components/LogsPane.tsx +6 -0
  89. package/src/tui/components/MetricsPane.jsx +108 -0
  90. package/src/tui/components/MetricsPane.tsx +5 -0
  91. package/src/tui/components/NodeDetailView.jsx +284 -0
  92. package/src/tui/components/NodeDetailView.tsx +7 -0
  93. package/src/tui/components/NodeInspector.jsx +51 -0
  94. package/src/tui/components/NodeInspector.tsx +7 -0
  95. package/src/tui/components/RunDetailView.jsx +190 -0
  96. package/src/tui/components/RunDetailView.tsx +7 -0
  97. package/src/tui/components/RunsList.jsx +184 -0
  98. package/src/tui/components/RunsList.tsx +7 -0
  99. package/src/tui/components/SqliteBrowser.jsx +131 -0
  100. package/src/tui/components/SqliteBrowser.tsx +5 -0
  101. package/src/tui/components/WorkflowLauncher.jsx +63 -0
  102. package/src/tui/components/WorkflowLauncher.tsx +3 -0
  103. package/src/util/CliErrorMapping.ts +7 -0
  104. package/src/util/CliExitCode.ts +10 -0
  105. package/src/util/errorMessage.js +212 -0
  106. package/src/util/exitCodes.js +18 -0
  107. package/src/watch.js +128 -0
  108. package/src/why-diagnosis.js +1000 -0
  109. package/src/workflow-pack.js +2151 -0
  110. package/src/workflows.js +122 -0
package/src/chat.js ADDED
@@ -0,0 +1,226 @@
1
+ import { formatTimestamp } from "./format.js";
2
+
3
+ /** @typedef {import("./ChatAttemptMeta.ts").ChatAttemptMeta} ChatAttemptMeta */
4
+ /** @typedef {import("./ChatAttemptRow.ts").ChatAttemptRow} ChatAttemptRow */
5
+ /** @typedef {import("./ChatOutputEvent.ts").ChatOutputEvent} ChatOutputEvent */
6
+ /** @typedef {import("./ParsedNodeOutputEvent.ts").ParsedNodeOutputEvent} ParsedNodeOutputEvent */
7
+
8
+ /**
9
+ * @param {string | null} [metaJson]
10
+ * @returns {ChatAttemptMeta}
11
+ */
12
+ export function parseChatAttemptMeta(metaJson) {
13
+ if (!metaJson)
14
+ return {};
15
+ try {
16
+ const parsed = JSON.parse(metaJson);
17
+ if (!parsed || typeof parsed !== "object")
18
+ return {};
19
+ return parsed;
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ /**
26
+ * @param {Pick<ChatAttemptRow, "nodeId" | "iteration" | "attempt">} attempt
27
+ */
28
+ export function chatAttemptKey(attempt) {
29
+ return `${attempt.nodeId}:${attempt.iteration}:${attempt.attempt}`;
30
+ }
31
+ /**
32
+ * @param {ChatOutputEvent} event
33
+ * @returns {ParsedNodeOutputEvent | null}
34
+ */
35
+ export function parseNodeOutputEvent(event) {
36
+ if (event.type !== "NodeOutput")
37
+ return null;
38
+ try {
39
+ const payload = JSON.parse(event.payloadJson);
40
+ if (!payload || typeof payload !== "object")
41
+ return null;
42
+ const text = typeof payload.text === "string" ? payload.text : "";
43
+ const stream = payload.stream === "stderr" ? "stderr" : "stdout";
44
+ if (!text)
45
+ return null;
46
+ return {
47
+ seq: event.seq,
48
+ timestampMs: event.timestampMs,
49
+ nodeId: String(payload.nodeId ?? ""),
50
+ iteration: Number(payload.iteration ?? 0),
51
+ attempt: Number(payload.attempt ?? 1),
52
+ stream,
53
+ text,
54
+ };
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ /**
61
+ * @param {ChatOutputEvent} event
62
+ * @returns {ParsedNodeOutputEvent | null}
63
+ */
64
+ export function parseAgentEvent(event) {
65
+ if (event.type !== "AgentEvent")
66
+ return null;
67
+ try {
68
+ const payload = JSON.parse(event.payloadJson);
69
+ if (!payload || typeof payload !== "object")
70
+ return null;
71
+ const agentEvent = payload.event;
72
+ if (!agentEvent || typeof agentEvent !== "object")
73
+ return null;
74
+ const nodeId = String(payload.nodeId ?? "");
75
+ const iteration = Number(payload.iteration ?? 0);
76
+ const attempt = Number(payload.attempt ?? 1);
77
+ if (agentEvent.type === "action") {
78
+ const action = agentEvent.action;
79
+ const phase = agentEvent.phase ?? "";
80
+ const kind = action?.kind ?? "unknown";
81
+ const title = action?.title ?? "";
82
+ const message = agentEvent.message ?? "";
83
+ const detail = action?.detail ?? {};
84
+ let text = "";
85
+ if (kind === "tool" || kind === "command") {
86
+ if (phase === "started") {
87
+ const input = detail.input ? JSON.stringify(detail.input) : "";
88
+ text = `[tool] ${title}${input ? `: ${truncate(input, 200)}` : ""}`;
89
+ }
90
+ else if (phase === "completed") {
91
+ const output = detail.output ? String(detail.output) : message;
92
+ text = `[tool] ${title} → ${truncate(output || "done", 200)}`;
93
+ }
94
+ else {
95
+ return null;
96
+ }
97
+ }
98
+ else if (kind === "file_change") {
99
+ const changes = detail.changes;
100
+ if (Array.isArray(changes)) {
101
+ text = `[file_change] ${changes.map((c) => `${c.type ?? "change"}: ${c.file ?? c.path ?? "?"}`).join(", ")}`;
102
+ }
103
+ else {
104
+ text = `[file_change] ${title || message || "files changed"}`;
105
+ }
106
+ }
107
+ else if (kind === "reasoning") {
108
+ if (!message)
109
+ return null;
110
+ text = `[reasoning] ${truncate(message, 300)}`;
111
+ }
112
+ else if (kind === "note" && agentEvent.entryType === "thought") {
113
+ if (!message)
114
+ return null;
115
+ text = `[thought] ${truncate(message, 300)}`;
116
+ }
117
+ else if (kind === "web_search") {
118
+ text = `[web_search] ${title || message || "searching"}`;
119
+ }
120
+ else {
121
+ // Skip other action kinds (turn, todo_list, generic notes)
122
+ return null;
123
+ }
124
+ if (!text)
125
+ return null;
126
+ return {
127
+ seq: event.seq,
128
+ timestampMs: event.timestampMs,
129
+ nodeId,
130
+ iteration,
131
+ attempt,
132
+ stream: "stdout",
133
+ text,
134
+ };
135
+ }
136
+ return null;
137
+ }
138
+ catch {
139
+ return null;
140
+ }
141
+ }
142
+ /**
143
+ * @param {string} str
144
+ * @param {number} max
145
+ */
146
+ function truncate(str, max) {
147
+ if (str.length <= max) return str;
148
+ return str.slice(0, max) + "…";
149
+ }
150
+ /**
151
+ * @param {ChatAttemptRow} attempt
152
+ * @param {ReadonlySet<string>} outputAttemptKeys
153
+ * @returns {boolean}
154
+ */
155
+ export function isAgentAttempt(attempt, outputAttemptKeys) {
156
+ const meta = parseChatAttemptMeta(attempt.metaJson);
157
+ if (meta.kind === "agent")
158
+ return true;
159
+ if (attempt.responseText?.trim())
160
+ return true;
161
+ return outputAttemptKeys.has(chatAttemptKey(attempt));
162
+ }
163
+ /**
164
+ * @param {ChatAttemptRow[]} attempts
165
+ * @param {ReadonlySet<string>} outputAttemptKeys
166
+ * @param {boolean} includeAll
167
+ * @returns {ChatAttemptRow[]}
168
+ */
169
+ export function selectChatAttempts(attempts, outputAttemptKeys, includeAll) {
170
+ const agentAttempts = attempts
171
+ .filter((attempt) => isAgentAttempt(attempt, outputAttemptKeys))
172
+ .sort((a, b) => {
173
+ if (a.startedAtMs !== b.startedAtMs)
174
+ return a.startedAtMs - b.startedAtMs;
175
+ if (a.nodeId !== b.nodeId)
176
+ return a.nodeId.localeCompare(b.nodeId);
177
+ if (a.iteration !== b.iteration)
178
+ return a.iteration - b.iteration;
179
+ return a.attempt - b.attempt;
180
+ });
181
+ if (includeAll)
182
+ return agentAttempts;
183
+ const latest = agentAttempts[agentAttempts.length - 1];
184
+ return latest ? [latest] : [];
185
+ }
186
+ /**
187
+ * @param {ChatAttemptRow} attempt
188
+ * @returns {string}
189
+ */
190
+ export function formatChatAttemptHeader(attempt) {
191
+ const meta = parseChatAttemptMeta(attempt.metaJson);
192
+ const title = meta.label?.trim() || attempt.nodeId;
193
+ const agentBits = [meta.agentId, meta.agentModel].filter(Boolean).join(" · ");
194
+ const parts = [
195
+ title,
196
+ `attempt ${attempt.attempt}`,
197
+ attempt.iteration > 0 ? `iteration ${attempt.iteration}` : null,
198
+ attempt.state,
199
+ agentBits || null,
200
+ ].filter(Boolean);
201
+ return `=== ${parts.join(" · ")} ===`;
202
+ }
203
+ /**
204
+ * @param {{ baseMs: number; timestampMs: number; role: "user" | "assistant" | "stderr"; attempt: Pick<ChatAttemptRow, "nodeId" | "iteration" | "attempt">; text: string; }} options
205
+ * @returns {string}
206
+ */
207
+ export function formatChatBlock(options) {
208
+ const { baseMs, timestampMs, role, attempt, text } = options;
209
+ const ts = formatTimestamp(baseMs, timestampMs);
210
+ const ref = `${attempt.nodeId}#${attempt.attempt}${attempt.iteration > 0 ? `.${attempt.iteration}` : ""}`;
211
+ const body = text.replace(/\s+$/, "");
212
+ const prefix = `[${ts}] ${role} ${ref}`;
213
+ if (!body.includes("\n")) {
214
+ return `${prefix}: ${body}`;
215
+ }
216
+ return `${prefix}:\n${indentBlock(body)}`;
217
+ }
218
+ /**
219
+ * @param {string} text
220
+ */
221
+ function indentBlock(text) {
222
+ return text
223
+ .split(/\r?\n/)
224
+ .map((line) => ` ${line}`)
225
+ .join("\n");
226
+ }
package/src/diff.js ADDED
@@ -0,0 +1,221 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./DiffBundleLike.ts").DiffBundleLike} DiffBundleLike */
3
+ /** @typedef {import("./RunDiffCommandInput.ts").RunDiffCommandInput} RunDiffCommandInput */
4
+ /** @typedef {import("./RunDiffCommandResult.ts").RunDiffCommandResult} RunDiffCommandResult */
5
+ // @smithers-type-exports-end
6
+
7
+ import pc from "picocolors";
8
+ import { getNodeDiffRoute } from "@smithers-orchestrator/server/gatewayRoutes/getNodeDiff";
9
+ import { EXIT_OK, EXIT_SERVER_ERROR } from "./util/exitCodes.js";
10
+ import { formatCliErrorForStderr, getCliErrorMapping } from "./util/errorMessage.js";
11
+
12
+ /** @param {boolean} color */
13
+ function colors(color) {
14
+ return color ? pc.createColors(true) : pc.createColors(false);
15
+ }
16
+
17
+ // ANSI CSI sequences: `ESC [ ... letter`. Strip covers colors (m), cursor
18
+ // moves, etc. Only used when the caller disables color so that existing
19
+ // escapes from upstream (jj, git) do not leak into non-TTY pipes.
20
+ const ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
21
+
22
+ /**
23
+ * @param {string} value
24
+ * @returns {string}
25
+ */
26
+ function stripAnsi(value) {
27
+ return value.replace(ANSI_ESCAPE_REGEX, "");
28
+ }
29
+
30
+ /**
31
+ * @param {string} diffText
32
+ * @param {boolean} useColor
33
+ * @returns {string}
34
+ */
35
+ function colorizePatch(diffText, useColor) {
36
+ // Finding #6: when color is disabled (explicit --color never or non-TTY),
37
+ // also strip any ANSI escapes the upstream VCS layer may have embedded
38
+ // in patch text. Without this, piping to `less` or capturing to a file
39
+ // leaks raw CSI codes. Color is only added here when useColor is true.
40
+ if (!useColor) return stripAnsi(diffText);
41
+ const c = colors(true);
42
+ const lines = diffText.split("\n");
43
+ const out = [];
44
+ for (const line of lines) {
45
+ if (line.startsWith("+++") || line.startsWith("---")) {
46
+ out.push(c.bold(line));
47
+ } else if (line.startsWith("+")) {
48
+ out.push(c.green(line));
49
+ } else if (line.startsWith("-")) {
50
+ out.push(c.red(line));
51
+ } else if (line.startsWith("@@")) {
52
+ out.push(c.cyan(line));
53
+ } else if (line.startsWith("diff ") || line.startsWith("index ")) {
54
+ out.push(c.dim(line));
55
+ } else {
56
+ out.push(line);
57
+ }
58
+ }
59
+ return out.join("\n");
60
+ }
61
+
62
+ /**
63
+ * @param {DiffBundleLike} bundle
64
+ * @param {{ color?: boolean }} [options]
65
+ * @returns {string}
66
+ */
67
+ export function renderUnifiedDiff(bundle, options) {
68
+ const useColor = Boolean(options?.color);
69
+ if (!bundle || !Array.isArray(bundle.patches) || bundle.patches.length === 0) {
70
+ return "(no changes)";
71
+ }
72
+ return bundle.patches
73
+ .map((patch) => colorizePatch(String(patch.diff ?? ""), useColor))
74
+ .join("\n");
75
+ }
76
+
77
+ /**
78
+ * Streaming per-line count of +/- markers in a unified diff. Counts
79
+ * content lines only (skips "+++ "/"--- " file headers). Scans via
80
+ * indexOf to avoid a giant split() allocation on large diffs.
81
+ *
82
+ * @param {string} diffText
83
+ * @returns {{ added: number; removed: number }}
84
+ */
85
+ function countDiffLines(diffText) {
86
+ let added = 0;
87
+ let removed = 0;
88
+ let cursor = 0;
89
+ while (cursor < diffText.length) {
90
+ const nl = diffText.indexOf("\n", cursor);
91
+ const end = nl === -1 ? diffText.length : nl;
92
+ const ch = diffText.charCodeAt(cursor);
93
+ if (ch === 43 /* + */ && !(diffText.charCodeAt(cursor + 1) === 43 && diffText.charCodeAt(cursor + 2) === 43)) {
94
+ added++;
95
+ }
96
+ else if (ch === 45 /* - */ && !(diffText.charCodeAt(cursor + 1) === 45 && diffText.charCodeAt(cursor + 2) === 45)) {
97
+ removed++;
98
+ }
99
+ cursor = end + 1;
100
+ }
101
+ return { added, removed };
102
+ }
103
+
104
+ /**
105
+ * Render a stat summary. Accepts either:
106
+ * - a server-side `summary` response ({ filesChanged, added, removed, files })
107
+ * - or a legacy `DiffBundle` (computes summary on the fly, streaming
108
+ * through patches so no intermediate array of full patch text is held).
109
+ *
110
+ * @param {DiffBundleLike | { summary: { filesChanged: number; added: number; removed: number; files: Array<{ path: string; added: number; removed: number }> } }} input
111
+ * @returns {string}
112
+ */
113
+ export function renderDiffStat(input) {
114
+ const summary = summaryFromInput(input);
115
+ if (summary.filesChanged === 0) {
116
+ return " 0 files changed";
117
+ }
118
+ /** @type {string[]} */
119
+ const lines = [];
120
+ for (const file of summary.files) {
121
+ const added = file.added;
122
+ const removed = file.removed;
123
+ lines.push(` ${file.path} | ${added + removed} ${"+".repeat(Math.min(added, 20))}${"-".repeat(Math.min(removed, 20))}`);
124
+ }
125
+ lines.push(` ${summary.filesChanged} file${summary.filesChanged === 1 ? "" : "s"} changed, ${summary.added} insertion${summary.added === 1 ? "" : "s"}(+), ${summary.removed} deletion${summary.removed === 1 ? "" : "s"}(-)`);
126
+ return lines.join("\n");
127
+ }
128
+
129
+ /**
130
+ * @param {any} input
131
+ * @returns {{ filesChanged: number; added: number; removed: number; files: Array<{ path: string; added: number; removed: number }> }}
132
+ */
133
+ function summaryFromInput(input) {
134
+ if (input && input.summary && typeof input.summary === "object") {
135
+ const s = input.summary;
136
+ return {
137
+ filesChanged: Number(s.filesChanged ?? 0),
138
+ added: Number(s.added ?? 0),
139
+ removed: Number(s.removed ?? 0),
140
+ files: Array.isArray(s.files) ? s.files : [],
141
+ };
142
+ }
143
+ if (!input || !Array.isArray(input.patches) || input.patches.length === 0) {
144
+ return { filesChanged: 0, added: 0, removed: 0, files: [] };
145
+ }
146
+ const files = [];
147
+ let totalAdded = 0;
148
+ let totalRemoved = 0;
149
+ for (const patch of input.patches) {
150
+ const { added, removed } = countDiffLines(String(patch.diff ?? ""));
151
+ totalAdded += added;
152
+ totalRemoved += removed;
153
+ files.push({ path: String(patch.path ?? ""), added, removed });
154
+ }
155
+ return { filesChanged: files.length, added: totalAdded, removed: totalRemoved, files };
156
+ }
157
+
158
+ /**
159
+ * @param {SmithersDb} adapter
160
+ * @param {string} runId
161
+ * @param {string} nodeId
162
+ * @returns {Promise<number | null>}
163
+ */
164
+ async function resolveLatestIteration(adapter, runId, nodeId) {
165
+ try {
166
+ const iterations = await adapter.listNodeIterations(runId, nodeId);
167
+ if (!Array.isArray(iterations) || iterations.length === 0) return null;
168
+ return iterations.reduce((max, row) => {
169
+ const it = typeof row?.iteration === "number" ? row.iteration : 0;
170
+ return it > max ? it : max;
171
+ }, 0);
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * @param {RunDiffCommandInput} input
179
+ * @returns {Promise<RunDiffCommandResult>}
180
+ */
181
+ export async function runDiffOnce(input) {
182
+ let iteration = input.iteration;
183
+ if (typeof iteration !== "number") {
184
+ const latest = await resolveLatestIteration(input.adapter, input.runId, input.nodeId);
185
+ iteration = latest ?? 0;
186
+ }
187
+ const result = await getNodeDiffRoute({
188
+ runId: input.runId,
189
+ nodeId: input.nodeId,
190
+ iteration,
191
+ async resolveRun(runId) {
192
+ if (runId !== input.runId) return null;
193
+ const run = await input.adapter.getRun(runId);
194
+ if (!run) return null;
195
+ return { adapter: input.adapter };
196
+ },
197
+ // Finding #5: stat mode asks the route for a summary only so very
198
+ // large diffs (>50MB) still return without hitting DiffTooLarge.
199
+ ...(input.stat ? { stat: true } : undefined),
200
+ });
201
+ if (!result.ok) {
202
+ input.stderr.write(`${formatCliErrorForStderr(result.error.code, result.error.message)}\n`);
203
+ const mapping = getCliErrorMapping(result.error.code, result.error.message);
204
+ return { exitCode: mapping.exitCode };
205
+ }
206
+ const payload = result.payload;
207
+ if (input.json) {
208
+ // Ticket 0014: --json emits the raw bundle (or summary when stat).
209
+ input.stdout.write(`${JSON.stringify(payload)}\n`);
210
+ return { exitCode: EXIT_OK };
211
+ }
212
+ if (input.stat) {
213
+ input.stdout.write(`${renderDiffStat(payload)}\n`);
214
+ return { exitCode: EXIT_OK };
215
+ }
216
+ input.stdout.write(`${renderUnifiedDiff(payload, { color: input.color })}\n`);
217
+ return { exitCode: EXIT_OK };
218
+ }
219
+
220
+ // ensure EXIT_SERVER_ERROR is considered used by linters / tree-shakers.
221
+ void EXIT_SERVER_ERROR;
@@ -0,0 +1,141 @@
1
+
2
+ /** @typedef {import("./EventCategory.ts").EventCategory} EventCategory */
3
+ /** @typedef {import("./SmithersEventType.ts").SmithersEventType} SmithersEventType */
4
+ const EVENT_CATEGORY_BY_TYPE = {
5
+ SupervisorStarted: "supervisor",
6
+ SupervisorPollCompleted: "supervisor",
7
+ RunAutoResumed: "run",
8
+ RunAutoResumeSkipped: "run",
9
+ RunStarted: "run",
10
+ RunStatusChanged: "run",
11
+ RunFinished: "run",
12
+ RunFailed: "run",
13
+ RunCancelled: "run",
14
+ RunContinuedAsNew: "run",
15
+ RunHijackRequested: "run",
16
+ RunHijacked: "run",
17
+ SandboxCreated: "sandbox",
18
+ SandboxShipped: "sandbox",
19
+ SandboxHeartbeat: "sandbox",
20
+ SandboxBundleReceived: "sandbox",
21
+ SandboxCompleted: "sandbox",
22
+ SandboxFailed: "sandbox",
23
+ SandboxDiffReviewRequested: "sandbox",
24
+ SandboxDiffAccepted: "sandbox",
25
+ SandboxDiffRejected: "sandbox",
26
+ FrameCommitted: "frame",
27
+ NodePending: "node",
28
+ NodeStarted: "node",
29
+ TaskHeartbeat: "node",
30
+ TaskHeartbeatTimeout: "node",
31
+ NodeFinished: "node",
32
+ NodeFailed: "node",
33
+ NodeCancelled: "node",
34
+ NodeSkipped: "node",
35
+ NodeRetrying: "node",
36
+ NodeWaitingApproval: "node",
37
+ NodeWaitingTimer: "node",
38
+ ApprovalRequested: "approval",
39
+ ApprovalGranted: "approval",
40
+ ApprovalAutoApproved: "approval",
41
+ ApprovalDenied: "approval",
42
+ ToolCallStarted: "tool-call",
43
+ ToolCallFinished: "tool-call",
44
+ NodeOutput: "output",
45
+ AgentEvent: "agent",
46
+ RetryTaskStarted: "run",
47
+ RetryTaskFinished: "run",
48
+ RevertStarted: "revert",
49
+ RevertFinished: "revert",
50
+ TimeTravelStarted: "revert",
51
+ TimeTravelFinished: "revert",
52
+ WorkflowReloadDetected: "workflow",
53
+ WorkflowReloaded: "workflow",
54
+ WorkflowReloadFailed: "workflow",
55
+ WorkflowReloadUnsafe: "workflow",
56
+ ScorerStarted: "scorer",
57
+ ScorerFinished: "scorer",
58
+ ScorerFailed: "scorer",
59
+ TokenUsageReported: "token",
60
+ SnapshotCaptured: "snapshot",
61
+ RunForked: "run",
62
+ ReplayStarted: "run",
63
+ MemoryFactSet: "memory",
64
+ MemoryRecalled: "memory",
65
+ MemoryMessageSaved: "memory",
66
+ OpenApiToolCalled: "openapi",
67
+ TimerCreated: "timer",
68
+ TimerFired: "timer",
69
+ TimerCancelled: "timer",
70
+ };
71
+ const CATEGORY_ALIASES = {
72
+ agent: "agent",
73
+ approval: "approval",
74
+ approvals: "approval",
75
+ frame: "frame",
76
+ memory: "memory",
77
+ node: "node",
78
+ openapi: "openapi",
79
+ output: "output",
80
+ revert: "revert",
81
+ run: "run",
82
+ sandbox: "sandbox",
83
+ scorer: "scorer",
84
+ snapshot: "snapshot",
85
+ supervisor: "supervisor",
86
+ timer: "timer",
87
+ token: "token",
88
+ tool: "tool-call",
89
+ toolcall: "tool-call",
90
+ "tool-call": "tool-call",
91
+ "tool_call": "tool-call",
92
+ workflow: "workflow",
93
+ reload: "workflow",
94
+ };
95
+ const EVENT_TYPES_BY_CATEGORY = Object.entries(EVENT_CATEGORY_BY_TYPE).reduce((acc, [type, category]) => {
96
+ if (!acc[category])
97
+ acc[category] = [];
98
+ acc[category].push(type);
99
+ return acc;
100
+ }, {
101
+ agent: [],
102
+ approval: [],
103
+ frame: [],
104
+ memory: [],
105
+ node: [],
106
+ openapi: [],
107
+ output: [],
108
+ revert: [],
109
+ run: [],
110
+ sandbox: [],
111
+ scorer: [],
112
+ snapshot: [],
113
+ supervisor: [],
114
+ timer: [],
115
+ token: [],
116
+ "tool-call": [],
117
+ workflow: [],
118
+ });
119
+ export const EVENT_CATEGORY_VALUES = Object.keys(EVENT_TYPES_BY_CATEGORY);
120
+ /**
121
+ * @param {string} raw
122
+ * @returns {EventCategory | null}
123
+ */
124
+ export function normalizeEventCategory(raw) {
125
+ const key = raw.trim().toLowerCase();
126
+ return CATEGORY_ALIASES[key] ?? null;
127
+ }
128
+ /**
129
+ * @param {string} type
130
+ * @returns {EventCategory | null}
131
+ */
132
+ export function eventCategoryForType(type) {
133
+ return EVENT_CATEGORY_BY_TYPE[type] ?? null;
134
+ }
135
+ /**
136
+ * @param {EventCategory} category
137
+ * @returns {readonly SmithersEventType[]}
138
+ */
139
+ export function eventTypesForCategory(category) {
140
+ return EVENT_TYPES_BY_CATEGORY[category];
141
+ }
package/src/find-db.js ADDED
@@ -0,0 +1,93 @@
1
+ import { resolve, dirname } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { SmithersDb } from "@smithers-orchestrator/db/adapter";
4
+ import { ensureSmithersTables } from "@smithers-orchestrator/db/ensure";
5
+ import { SmithersError } from "@smithers-orchestrator/errors";
6
+ /** @typedef {import("./FindDbWaitOptions.ts").FindDbWaitOptions} FindDbWaitOptions */
7
+
8
+ /**
9
+ * Walk from `from` (default: cwd) upward looking for smithers.db.
10
+ * Returns the absolute path to the database file.
11
+ *
12
+ * @param {string} [from]
13
+ * @returns {string}
14
+ */
15
+ export function findSmithersDb(from) {
16
+ let dir = resolve(from ?? process.cwd());
17
+ const root = resolve("/");
18
+ while (true) {
19
+ const candidate = resolve(dir, "smithers.db");
20
+ if (existsSync(candidate))
21
+ return candidate;
22
+ const parent = dirname(dir);
23
+ if (parent === dir || dir === root) {
24
+ throw new SmithersError("CLI_DB_NOT_FOUND", "No smithers.db found. Run this command from a directory containing a smithers.db, or use 'smithers up <workflow>' to start a run first.");
25
+ }
26
+ dir = parent;
27
+ }
28
+ }
29
+ /**
30
+ * @param {number} ms
31
+ */
32
+ function sleep(ms) {
33
+ return new Promise((resolve) => setTimeout(resolve, ms));
34
+ }
35
+ /**
36
+ * @param {string} [from]
37
+ * @param {FindDbWaitOptions} [opts]
38
+ * @returns {Promise<string>}
39
+ */
40
+ export async function waitForSmithersDb(from, opts = {}) {
41
+ const timeoutMs = Math.max(0, opts.timeoutMs ?? 0);
42
+ const intervalMs = Math.max(1, opts.intervalMs ?? 100);
43
+ const startedAt = Date.now();
44
+ while (true) {
45
+ try {
46
+ return findSmithersDb(from);
47
+ }
48
+ catch (err) {
49
+ if (!(err instanceof SmithersError) || err.code !== "CLI_DB_NOT_FOUND") {
50
+ throw err;
51
+ }
52
+ const elapsedMs = Date.now() - startedAt;
53
+ if (elapsedMs >= timeoutMs) {
54
+ throw err;
55
+ }
56
+ await sleep(Math.min(intervalMs, timeoutMs - elapsedMs));
57
+ }
58
+ }
59
+ }
60
+ /**
61
+ * Open a smithers.db file and return a SmithersDb adapter with cleanup function.
62
+ *
63
+ * @param {string} dbPath
64
+ * @returns {Promise<{ adapter: SmithersDb; cleanup: () => void }>}
65
+ */
66
+ export async function openSmithersDb(dbPath) {
67
+ const { Database } = await import("bun:sqlite");
68
+ const { drizzle } = await import("drizzle-orm/bun-sqlite");
69
+ const sqlite = new Database(dbPath);
70
+ const db = drizzle(sqlite);
71
+ ensureSmithersTables(db);
72
+ return {
73
+ adapter: new SmithersDb(db),
74
+ cleanup: () => {
75
+ try {
76
+ sqlite.close();
77
+ }
78
+ catch { }
79
+ },
80
+ };
81
+ }
82
+ /**
83
+ * Find and open the nearest smithers.db.
84
+ *
85
+ * @param {string} [from]
86
+ * @param {FindDbWaitOptions} [opts]
87
+ * @returns {Promise<{ adapter: SmithersDb; dbPath: string; cleanup: () => void }>}
88
+ */
89
+ export async function findAndOpenDb(from, opts) {
90
+ const dbPath = await waitForSmithersDb(from, opts);
91
+ const { adapter, cleanup } = await openSmithersDb(dbPath);
92
+ return { adapter, dbPath, cleanup };
93
+ }