@smithers-orchestrator/cli 0.20.0 → 0.20.3

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 (37) hide show
  1. package/dist/agent-detection.d.ts +20 -3
  2. package/package.json +17 -19
  3. package/src/AgentAvailability.ts +3 -0
  4. package/src/agent-detection.js +226 -14
  5. package/src/ask.js +4 -6
  6. package/src/index.js +89 -45
  7. package/src/workflow-pack.js +48 -10
  8. package/src/tui/app.jsx +0 -139
  9. package/src/tui/app.tsx +0 -5
  10. package/src/tui/components/AskModal.jsx +0 -109
  11. package/src/tui/components/AskModal.tsx +0 -3
  12. package/src/tui/components/AttentionPane.jsx +0 -112
  13. package/src/tui/components/AttentionPane.tsx +0 -6
  14. package/src/tui/components/ChatPane.jsx +0 -57
  15. package/src/tui/components/ChatPane.tsx +0 -7
  16. package/src/tui/components/CronList.jsx +0 -87
  17. package/src/tui/components/CronList.tsx +0 -5
  18. package/src/tui/components/DetailsPane.jsx +0 -96
  19. package/src/tui/components/DetailsPane.tsx +0 -7
  20. package/src/tui/components/FramesPane.jsx +0 -147
  21. package/src/tui/components/FramesPane.tsx +0 -8
  22. package/src/tui/components/LogsPane.jsx +0 -46
  23. package/src/tui/components/LogsPane.tsx +0 -6
  24. package/src/tui/components/MetricsPane.jsx +0 -108
  25. package/src/tui/components/MetricsPane.tsx +0 -5
  26. package/src/tui/components/NodeDetailView.jsx +0 -284
  27. package/src/tui/components/NodeDetailView.tsx +0 -7
  28. package/src/tui/components/NodeInspector.jsx +0 -51
  29. package/src/tui/components/NodeInspector.tsx +0 -7
  30. package/src/tui/components/RunDetailView.jsx +0 -190
  31. package/src/tui/components/RunDetailView.tsx +0 -7
  32. package/src/tui/components/RunsList.jsx +0 -184
  33. package/src/tui/components/RunsList.tsx +0 -7
  34. package/src/tui/components/SqliteBrowser.jsx +0 -131
  35. package/src/tui/components/SqliteBrowser.tsx +0 -5
  36. package/src/tui/components/WorkflowLauncher.jsx +0 -63
  37. package/src/tui/components/WorkflowLauncher.tsx +0 -3
@@ -1,184 +0,0 @@
1
- // @ts-nocheck
2
- import React, { useEffect, useState } from "react";
3
- import { useKeyboard } from "@opentui/react";
4
- import { formatAge } from "../../format.js";
5
- import { basename } from "node:path";
6
- /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
7
-
8
- /**
9
- * @param {{ adapter: SmithersDb; focused: boolean; onChange: (runId: string) => void; onSubmit: (runId: string) => void; }} value
10
- */
11
- export function RunsList({ adapter, focused, onChange, onSubmit, }) {
12
- const [runs, setRuns] = useState([]);
13
- const [selectedIndex, setSelectedIndex] = useState(0);
14
- const [filterMode, setFilterMode] = useState("all");
15
- const [selectedRunStats, setSelectedRunStats] = useState(null);
16
- useEffect(() => {
17
- let mounted = true;
18
- async function pollRuns() {
19
- if (!mounted)
20
- return;
21
- try {
22
- const fetchedRuns = await adapter.listRuns(20, filterMode === "pending" ? "waiting-approval" : undefined);
23
- if (mounted) {
24
- setRuns(fetchedRuns);
25
- if (fetchedRuns.length > 0 && selectedIndex === 0) {
26
- // Auto select the first run initially if nothing was selected
27
- // But we don't want to call onSelect constantly, so we'll just let the list render.
28
- }
29
- }
30
- }
31
- catch (err) { }
32
- if (mounted)
33
- setTimeout(pollRuns, 1000);
34
- }
35
- pollRuns();
36
- return () => {
37
- mounted = false;
38
- };
39
- }, [adapter, selectedIndex, filterMode]);
40
- useEffect(() => {
41
- if (runs.length > 0) {
42
- onChange(runs[selectedIndex]?.runId ?? runs[0].runId);
43
- }
44
- }, [runs, selectedIndex, onChange]);
45
- const selectedRun = runs[selectedIndex];
46
- useEffect(() => {
47
- let mounted = true;
48
- if (!selectedRun) {
49
- setSelectedRunStats(null);
50
- return;
51
- }
52
- async function fetchStats() {
53
- try {
54
- const events = await adapter.listEvents(selectedRun.runId, -1, 10000);
55
- const nodes = await adapter.listNodes(selectedRun.runId);
56
- let tIn = 0, tOut = 0, tCache = 0;
57
- for (const e of events) {
58
- if (e.type === "TokenUsageReported") {
59
- try {
60
- const p = JSON.parse(e.payloadJson);
61
- tIn += (p.inputTokens ?? 0);
62
- tOut += (p.outputTokens ?? 0);
63
- tCache += (p.cacheReadTokens ?? 0);
64
- }
65
- catch { }
66
- }
67
- }
68
- const sMs = selectedRun.startedAtMs || selectedRun.createdAtMs;
69
- const durationMs = selectedRun.finishedAtMs && sMs
70
- ? selectedRun.finishedAtMs - sMs
71
- : sMs ? Date.now() - sMs : 0;
72
- if (mounted) {
73
- setSelectedRunStats({
74
- tokensIn: tIn,
75
- tokensOut: tOut,
76
- tokensCache: tCache,
77
- durationMs,
78
- nodeCount: nodes.length,
79
- });
80
- }
81
- }
82
- catch { }
83
- }
84
- fetchStats();
85
- return () => { mounted = false; };
86
- }, [adapter, selectedRun?.runId]);
87
- useKeyboard((key) => {
88
- if (!focused)
89
- return;
90
- if (key.name === "p" || key.name === "P") {
91
- setFilterMode((m) => m === "all" ? "pending" : "all");
92
- setSelectedIndex(0);
93
- return;
94
- }
95
- if (runs[selectedIndex]) {
96
- const runId = runs[selectedIndex].runId;
97
- const status = runs[selectedIndex].status;
98
- if (key.name === "enter" || key.name === "return") {
99
- onSubmit(runId);
100
- return;
101
- }
102
- if (key.name === "y" && status === "waiting-approval") {
103
- Bun.spawn(["bun", "run", "src/index.js", "approve", runId], { stdout: "ignore", stderr: "ignore" }).unref();
104
- return;
105
- }
106
- if (key.name === "d" && status === "waiting-approval") {
107
- Bun.spawn(["bun", "run", "src/index.js", "deny", runId], { stdout: "ignore", stderr: "ignore" }).unref();
108
- return;
109
- }
110
- if (key.name === "c" && (status === "running" || status === "waiting-timer")) {
111
- Bun.spawn(["bun", "run", "src/index.js", "cancel", runId], { stdout: "ignore", stderr: "ignore" }).unref();
112
- return;
113
- }
114
- if (key.name === "r" && (status === "failed" || status === "cancelled")) {
115
- const path = runs[selectedIndex].workflowPath;
116
- if (path) {
117
- Bun.spawn(["bun", "run", "src/index.js", "up", path, "--resume", "--runId", runId, "-d"], { stdout: "ignore", stderr: "ignore" }).unref();
118
- }
119
- return;
120
- }
121
- }
122
- if (key.name === "k") {
123
- Bun.spawn(["bun", "run", "src/index.js", "down"], { stdout: "ignore", stderr: "ignore" }).unref();
124
- return;
125
- }
126
- });
127
- const options = runs.map((run) => {
128
- const workflowName = run.workflowName ?? (run.workflowPath ? basename(run.workflowPath) : "—");
129
- const started = run.startedAtMs ? formatAge(run.startedAtMs) : run.createdAtMs ? formatAge(run.createdAtMs) : "—";
130
- return {
131
- name: `${workflowName} (${run.status})`,
132
- description: `${run.runId.slice(-6)} - ${started}`,
133
- value: run.runId,
134
- };
135
- });
136
- if (runs.length === 0) {
137
- return <text style={{ margin: 1 }}>No runs found.</text>;
138
- }
139
- let truncatedOutput = selectedRun?.errorJson;
140
- if (!truncatedOutput) {
141
- if (selectedRun?.status === "finished") {
142
- truncatedOutput = "Run completed successfully without generic failure traces.\nPress [Enter] to inspect individual task payloads.";
143
- }
144
- else if (selectedRun?.status === "failed") {
145
- truncatedOutput = "Workflow failed, but no global error stack trace was captured. Press [Enter] to inspect which individual node crashed.";
146
- }
147
- else {
148
- truncatedOutput = "Workflow is still active or pending execution...";
149
- }
150
- }
151
- if (truncatedOutput.length > 500) {
152
- const head = truncatedOutput.substring(0, 200);
153
- const tail = truncatedOutput.substring(truncatedOutput.length - 200);
154
- truncatedOutput = `${head}\n\n... [ TRUNCATED ${truncatedOutput.length - 400} CHARACTERS ] ...\n\n${tail}`;
155
- }
156
- return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "row" }}>
157
- {/* Left List */}
158
- <box style={{ width: 40, height: "100%", borderRight: true, borderColor: "#34d399", flexDirection: "column" }}>
159
- <select style={{ width: "100%", height: "100%" }} options={options} focused={focused} onChange={(index) => {
160
- setSelectedIndex(index);
161
- }} textColor="#E2E8F0" selectedTextColor="#34d399" selectedBackgroundColor="#1f2937" descriptionColor="#9ca3af" selectedDescriptionColor="#a7f3d0" showScrollIndicator wrapSelection/>
162
- </box>
163
-
164
- {/* Right Preview */}
165
- <box style={{ flexGrow: 1, height: "100%", flexDirection: "column", paddingLeft: 1 }}>
166
- <text style={{ color: "yellow" }}>State: {selectedRun?.status}</text>
167
- {selectedRunStats && (<text style={{ color: "#a855f7", marginTop: 1 }}>
168
- ⏱️ {(selectedRunStats.durationMs / 1000).toFixed(1)}s | 🧩 {selectedRunStats.nodeCount} Tasks | 🪙 {selectedRunStats.tokensIn} IN, {selectedRunStats.tokensOut} OUT, {selectedRunStats.tokensCache} CACHE
169
- </text>)}
170
- <text style={{ color: "gray", marginTop: 1 }}>--- Run Status / Error Summary ---</text>
171
- <scrollbox style={{ flexGrow: 1, width: "100%", marginTop: 1 }}>
172
- <text>{truncatedOutput}</text>
173
- </scrollbox>
174
-
175
- {/* Action Footer */}
176
- <box style={{ width: "100%", height: 3, borderTop: true, borderColor: "gray", flexDirection: "column" }}>
177
- <text style={{ color: "#93c5fd" }}>{filterMode === "all" ? "[P] Filter: Show Pending Approvals Only" : "[P] Filter: Show All Workflows"}</text>
178
- {selectedRun?.status === "waiting-approval" && <text style={{ color: "green" }}>[Y] Approve | [D] Deny Human Task</text>}
179
- {(selectedRun?.status === "running" || selectedRun?.status === "waiting-timer") && <text style={{ color: "red" }}>[C] Safely Cancel/Halt Workflow | [K] Kill All Active Workflows</text>}
180
- {(selectedRun?.status === "failed" || selectedRun?.status === "cancelled") && <text style={{ color: "yellow" }}>[R] Resume from latest checkpoint</text>}
181
- </box>
182
- </box>
183
- </box>);
184
- }
@@ -1,7 +0,0 @@
1
- import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
- export declare function RunsList({ adapter, focused, onChange, onSubmit, }: {
3
- adapter: SmithersDb;
4
- focused: boolean;
5
- onChange: (runId: string) => void;
6
- onSubmit: (runId: string) => void;
7
- }): import("react/jsx-runtime").JSX.Element;
@@ -1,131 +0,0 @@
1
- // @ts-nocheck
2
- import React, { useEffect, useState } from "react";
3
- import { useKeyboard } from "@opentui/react";
4
- /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
5
-
6
- /**
7
- * @param {{ adapter: SmithersDb; onBack: () => void; }} value
8
- */
9
- export function SqliteBrowser({ adapter, onBack, }) {
10
- const [tables, setTables] = useState([]);
11
- const [selectedTableIdx, setSelectedTableIdx] = useState(0);
12
- const [query, setQuery] = useState("");
13
- const [results, setResults] = useState([]);
14
- const [error, setError] = useState(null);
15
- const [focusedPane, setFocusedPane] = useState("tables");
16
- // Fetch tables on mount
17
- useEffect(() => {
18
- let mounted = true;
19
- adapter.rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
20
- .then((res) => {
21
- if (mounted) {
22
- const names = res.map((r) => r.name).sort();
23
- setTables(names);
24
- if (names.length > 0) {
25
- setQuery(`SELECT * FROM ${names[0]} LIMIT 50;`);
26
- }
27
- }
28
- })
29
- .catch((err) => {
30
- if (mounted)
31
- setError(err.message);
32
- });
33
- return () => { mounted = false; };
34
- }, [adapter]);
35
- // Execute query whenever it changes
36
- useEffect(() => {
37
- let mounted = true;
38
- if (!query.trim()) {
39
- setResults([]);
40
- return;
41
- }
42
- adapter.rawQuery(query)
43
- .then((res) => {
44
- if (mounted) {
45
- setResults(res);
46
- setError(null);
47
- }
48
- })
49
- .catch((err) => {
50
- if (mounted) {
51
- setError(err.message);
52
- setResults([]);
53
- }
54
- });
55
- return () => { mounted = false; };
56
- }, [adapter, query]);
57
- useKeyboard((key) => {
58
- if (key.name === "escape") {
59
- if (focusedPane === "query") {
60
- setFocusedPane("tables");
61
- }
62
- else {
63
- onBack();
64
- }
65
- return;
66
- }
67
- if (key.name === "tab") {
68
- setFocusedPane((p) => p === "tables" ? "query" : p === "query" ? "results" : "tables");
69
- return;
70
- }
71
- if (focusedPane === "tables" && tables.length > 0) {
72
- if (key.name === "down" || key.name === "j") {
73
- const next = Math.min(tables.length - 1, selectedTableIdx + 1);
74
- setSelectedTableIdx(next);
75
- setQuery(`SELECT * FROM ${tables[next]} LIMIT 50;`);
76
- }
77
- if (key.name === "up" || key.name === "k") {
78
- const prev = Math.max(0, selectedTableIdx - 1);
79
- setSelectedTableIdx(prev);
80
- setQuery(`SELECT * FROM ${tables[prev]} LIMIT 50;`);
81
- }
82
- }
83
- if (focusedPane === "query") {
84
- if (key.name === "backspace") {
85
- setQuery((q) => q.slice(0, -1));
86
- return;
87
- }
88
- if (key.name === "enter" || key.name === "return") {
89
- setFocusedPane("results");
90
- return;
91
- }
92
- if (!key.ctrl && !key.meta && key.sequence && key.sequence.length === 1 && key.name !== "up" && key.name !== "down" && key.name !== "left" && key.name !== "right") {
93
- setQuery((q) => q + key.sequence);
94
- }
95
- }
96
- });
97
- const tableOptions = tables.map((t) => ({
98
- name: t,
99
- value: t,
100
- }));
101
- const resultText = error
102
- ? `[!] Query Error:\n${error}`
103
- : results.length === 0
104
- ? "No results."
105
- : JSON.stringify(results, null, 2);
106
- return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "row" }}>
107
- {/* Left Pane: Tables List */}
108
- <box style={{ width: 30, height: "100%", borderRight: true, borderColor: focusedPane === "tables" ? "#34d399" : "gray", flexDirection: "column" }}>
109
- <text style={{ color: focusedPane === "tables" ? "white" : "gray", marginBottom: 1 }}> Tables </text>
110
- <select style={{ width: "100%", flexGrow: 1 }} options={tableOptions} focused={focusedPane === "tables"} onChange={(idx) => {
111
- setSelectedTableIdx(idx);
112
- setQuery(`SELECT * FROM ${tables[idx]} LIMIT 50;`);
113
- }} textColor="#E2E8F0" selectedTextColor="#34d399" selectedBackgroundColor="#1f2937"/>
114
- </box>
115
-
116
- {/* Right Pane: Query & Results */}
117
- <box style={{ flexGrow: 1, height: "100%", flexDirection: "column", paddingLeft: 1 }}>
118
- <box style={{ width: "100%", height: 3, borderBottom: true, borderColor: focusedPane === "query" ? "#34d399" : "gray", flexDirection: "column" }}>
119
- <text style={{ color: "yellow" }}> SQL Query: {focusedPane === "query" ? "(Press Enter to run)" : ""} </text>
120
- {focusedPane === "query" ? (<text style={{ color: "white", marginTop: 1 }}>{"> "}{query}█</text>) : (<text style={{ color: "white", marginTop: 1 }}>{query}</text>)}
121
- </box>
122
-
123
- <box style={{ flexGrow: 1, width: "100%", flexDirection: "column", marginTop: 1 }}>
124
- <text style={{ color: "gray", marginBottom: 1 }}> Results ({results.length} rows) </text>
125
- <scrollbox style={{ flexGrow: 1, width: "100%" }}>
126
- <text style={{ color: error ? "red" : "#a7f3d0" }}>{resultText}</text>
127
- </scrollbox>
128
- </box>
129
- </box>
130
- </box>);
131
- }
@@ -1,5 +0,0 @@
1
- import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
- export declare function SqliteBrowser({ adapter, onBack, }: {
3
- adapter: SmithersDb;
4
- onBack: () => void;
5
- }): import("react/jsx-runtime").JSX.Element;
@@ -1,63 +0,0 @@
1
- // @ts-nocheck
2
- import React, { useEffect, useState } from "react";
3
- import { useKeyboard } from "@opentui/react";
4
- import { readdirSync } from "node:fs";
5
- /**
6
- * @param {{ onClose: () => void }} value
7
- */
8
- export function WorkflowLauncher({ onClose }) {
9
- const [examples, setExamples] = useState([]);
10
- const [selectedIndex, setSelectedIndex] = useState(0);
11
- useEffect(() => {
12
- try {
13
- const files = readdirSync("examples").filter((f) => /\.(?:jsx?|tsx?)$/.test(f)).filter(f => !f.startsWith("_"));
14
- setExamples(files);
15
- }
16
- catch {
17
- setExamples([]);
18
- }
19
- }, []);
20
- useKeyboard((key) => {
21
- if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
22
- onClose();
23
- return;
24
- }
25
- if (key.name === "down" || key.name === "j") {
26
- setSelectedIndex((s) => Math.min(s + 1, Math.max(0, examples.length - 1)));
27
- }
28
- if (key.name === "up" || key.name === "k") {
29
- setSelectedIndex((s) => Math.max(s - 1, 0));
30
- }
31
- if (key.name === "enter" || key.name === "return") {
32
- if (examples[selectedIndex]) {
33
- // spawn the workflow
34
- const file = examples[selectedIndex];
35
- try {
36
- const proc = Bun.spawn(["bun", "run", "src/index.js", "up", `examples/${file}`, "-d"], {
37
- stdin: "ignore",
38
- stdout: "ignore",
39
- stderr: "ignore",
40
- });
41
- proc.unref(); // allow the parent process (TUI) to exit independently
42
- }
43
- catch { }
44
- onClose();
45
- }
46
- }
47
- });
48
- return (<box style={{
49
- flexGrow: 1,
50
- width: "100%",
51
- height: "100%",
52
- border: true,
53
- borderColor: "yellow",
54
- flexDirection: "column",
55
- }} title="Launch Workflow [Esc to Close, Enter to Run]">
56
- <text style={{ margin: 1, color: "gray" }}>Select an example to run in the background:</text>
57
- {examples.length === 0 ? (<text style={{ margin: 1 }}>No examples found in ./examples</text>) : (<scrollbox style={{ width: "100%", height: "100%", flexDirection: "column", paddingLeft: 2 }}>
58
- {examples.map((ex, i) => (<text key={ex} style={{ color: selectedIndex === i ? "green" : "white" }}>
59
- {selectedIndex === i ? "▶ " : " "}{ex}
60
- </text>))}
61
- </scrollbox>)}
62
- </box>);
63
- }
@@ -1,3 +0,0 @@
1
- export declare function WorkflowLauncher({ onClose }: {
2
- onClose: () => void;
3
- }): import("react/jsx-runtime").JSX.Element;