@smithers-orchestrator/cli 0.20.1 → 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.
- package/package.json +17 -19
- package/src/index.js +52 -44
- package/src/workflow-pack.js +15 -6
- package/src/tui/app.jsx +0 -139
- package/src/tui/app.tsx +0 -5
- package/src/tui/components/AskModal.jsx +0 -109
- package/src/tui/components/AskModal.tsx +0 -3
- package/src/tui/components/AttentionPane.jsx +0 -112
- package/src/tui/components/AttentionPane.tsx +0 -6
- package/src/tui/components/ChatPane.jsx +0 -57
- package/src/tui/components/ChatPane.tsx +0 -7
- package/src/tui/components/CronList.jsx +0 -87
- package/src/tui/components/CronList.tsx +0 -5
- package/src/tui/components/DetailsPane.jsx +0 -96
- package/src/tui/components/DetailsPane.tsx +0 -7
- package/src/tui/components/FramesPane.jsx +0 -147
- package/src/tui/components/FramesPane.tsx +0 -8
- package/src/tui/components/LogsPane.jsx +0 -46
- package/src/tui/components/LogsPane.tsx +0 -6
- package/src/tui/components/MetricsPane.jsx +0 -108
- package/src/tui/components/MetricsPane.tsx +0 -5
- package/src/tui/components/NodeDetailView.jsx +0 -284
- package/src/tui/components/NodeDetailView.tsx +0 -7
- package/src/tui/components/NodeInspector.jsx +0 -51
- package/src/tui/components/NodeInspector.tsx +0 -7
- package/src/tui/components/RunDetailView.jsx +0 -190
- package/src/tui/components/RunDetailView.tsx +0 -7
- package/src/tui/components/RunsList.jsx +0 -184
- package/src/tui/components/RunsList.tsx +0 -7
- package/src/tui/components/SqliteBrowser.jsx +0 -131
- package/src/tui/components/SqliteBrowser.tsx +0 -5
- package/src/tui/components/WorkflowLauncher.jsx +0 -63
- package/src/tui/components/WorkflowLauncher.tsx +0 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/cli",
|
|
3
|
-
"version": "0.20.
|
|
4
|
-
"description": "Smithers command-line interface,
|
|
3
|
+
"version": "0.20.3",
|
|
4
|
+
"description": "Smithers command-line interface, MCP server, and local workflow tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"exports": {
|
|
@@ -25,8 +25,6 @@
|
|
|
25
25
|
"@effect/workflow": "^0.18.0",
|
|
26
26
|
"@clack/prompts": "^0.10.1",
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
|
-
"@opentui/core": "^0.1.100",
|
|
29
|
-
"@opentui/react": "^0.1.100",
|
|
30
28
|
"cron-parser": "^5.5.0",
|
|
31
29
|
"drizzle-orm": "^0.45.2",
|
|
32
30
|
"effect": "^3.21.1",
|
|
@@ -34,21 +32,21 @@
|
|
|
34
32
|
"picocolors": "^1.1.1",
|
|
35
33
|
"react": "^19.2.5",
|
|
36
34
|
"zod": "^4.3.6",
|
|
37
|
-
"@smithers-orchestrator/accounts": "0.20.
|
|
38
|
-
"@smithers-orchestrator/agents": "0.20.
|
|
39
|
-
"@smithers-orchestrator/components": "0.20.
|
|
40
|
-
"@smithers-orchestrator/db": "0.20.
|
|
41
|
-
"@smithers-orchestrator/devtools": "0.20.
|
|
42
|
-
"@smithers-orchestrator/driver": "0.20.
|
|
43
|
-
"@smithers-orchestrator/engine": "0.20.
|
|
44
|
-
"@smithers-orchestrator/errors": "0.20.
|
|
45
|
-
"@smithers-orchestrator/memory": "0.20.
|
|
46
|
-
"@smithers-orchestrator/observability": "0.20.
|
|
47
|
-
"@smithers-orchestrator/openapi": "0.20.
|
|
48
|
-
"@smithers-orchestrator/protocol": "0.20.
|
|
49
|
-
"@smithers-orchestrator/scheduler": "0.20.
|
|
50
|
-
"@smithers-orchestrator/server": "0.20.
|
|
51
|
-
"@smithers-orchestrator/time-travel": "0.20.
|
|
35
|
+
"@smithers-orchestrator/accounts": "0.20.3",
|
|
36
|
+
"@smithers-orchestrator/agents": "0.20.3",
|
|
37
|
+
"@smithers-orchestrator/components": "0.20.3",
|
|
38
|
+
"@smithers-orchestrator/db": "0.20.3",
|
|
39
|
+
"@smithers-orchestrator/devtools": "0.20.3",
|
|
40
|
+
"@smithers-orchestrator/driver": "0.20.3",
|
|
41
|
+
"@smithers-orchestrator/engine": "0.20.3",
|
|
42
|
+
"@smithers-orchestrator/errors": "0.20.3",
|
|
43
|
+
"@smithers-orchestrator/memory": "0.20.3",
|
|
44
|
+
"@smithers-orchestrator/observability": "0.20.3",
|
|
45
|
+
"@smithers-orchestrator/openapi": "0.20.3",
|
|
46
|
+
"@smithers-orchestrator/protocol": "0.20.3",
|
|
47
|
+
"@smithers-orchestrator/scheduler": "0.20.3",
|
|
48
|
+
"@smithers-orchestrator/server": "0.20.3",
|
|
49
|
+
"@smithers-orchestrator/time-travel": "0.20.3"
|
|
52
50
|
},
|
|
53
51
|
"devDependencies": {
|
|
54
52
|
"@types/bun": "latest",
|
package/src/index.js
CHANGED
|
@@ -484,6 +484,41 @@ function isRunStatusTerminal(status) {
|
|
|
484
484
|
status !== "waiting-timer" &&
|
|
485
485
|
status !== "waiting-event");
|
|
486
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* Fetch a docs file from smithers.sh and write it to stdout.
|
|
489
|
+
* Honors --json (global) by emitting `{ url, content }`.
|
|
490
|
+
*
|
|
491
|
+
* @param {{ error: Function; ok: Function; format?: string; options?: { json?: boolean } }} c
|
|
492
|
+
* @param {string} url
|
|
493
|
+
* @param {string} errorCode
|
|
494
|
+
*/
|
|
495
|
+
async function printSmithersDocs(c, url, errorCode) {
|
|
496
|
+
let body;
|
|
497
|
+
try {
|
|
498
|
+
const res = await fetch(url);
|
|
499
|
+
if (!res.ok) {
|
|
500
|
+
return c.error({
|
|
501
|
+
code: errorCode,
|
|
502
|
+
message: `Failed to fetch ${url}: HTTP ${res.status}`,
|
|
503
|
+
exitCode: 1,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
body = await res.text();
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
return c.error({
|
|
510
|
+
code: errorCode,
|
|
511
|
+
message: `Failed to fetch ${url}: ${err?.message ?? String(err)}`,
|
|
512
|
+
exitCode: 1,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
const wantsJson = Boolean(c.options?.json) || c.format === "json";
|
|
516
|
+
if (wantsJson) {
|
|
517
|
+
return c.ok({ url, content: body });
|
|
518
|
+
}
|
|
519
|
+
process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
|
|
520
|
+
return c.ok(undefined);
|
|
521
|
+
}
|
|
487
522
|
/**
|
|
488
523
|
* @param {string | undefined} format
|
|
489
524
|
* @param {unknown} payload
|
|
@@ -2551,8 +2586,8 @@ const cli = Cli.create({
|
|
|
2551
2586
|
{ command: "workflow run implement", description: "Run the implementation workflow" },
|
|
2552
2587
|
]
|
|
2553
2588
|
: [
|
|
2554
|
-
{ command: "tui", description: "Open the interactive dashboard" },
|
|
2555
2589
|
{ command: "workflow list", description: "View all available workflows" },
|
|
2590
|
+
{ command: "workflow run implement", description: "Run the implementation workflow" },
|
|
2556
2591
|
],
|
|
2557
2592
|
},
|
|
2558
2593
|
});
|
|
@@ -2641,49 +2676,6 @@ const cli = Cli.create({
|
|
|
2641
2676
|
cleanup();
|
|
2642
2677
|
}
|
|
2643
2678
|
},
|
|
2644
|
-
})
|
|
2645
|
-
// =========================================================================
|
|
2646
|
-
// smithers tui
|
|
2647
|
-
// =========================================================================
|
|
2648
|
-
.command("tui", {
|
|
2649
|
-
description: "Open the interactive Smithers observability dashboard",
|
|
2650
|
-
async run(c) {
|
|
2651
|
-
const fail = (opts) => {
|
|
2652
|
-
commandExitOverride = opts.exitCode ?? 1;
|
|
2653
|
-
return c.error(opts);
|
|
2654
|
-
};
|
|
2655
|
-
let cleanup;
|
|
2656
|
-
let renderer;
|
|
2657
|
-
try {
|
|
2658
|
-
const db = await findAndOpenDb(undefined, {
|
|
2659
|
-
timeoutMs: 5000,
|
|
2660
|
-
intervalMs: 100,
|
|
2661
|
-
});
|
|
2662
|
-
const adapter = db.adapter;
|
|
2663
|
-
cleanup = db.cleanup;
|
|
2664
|
-
const { createCliRenderer } = await import("@opentui/core");
|
|
2665
|
-
const { createRoot } = await import("@opentui/react");
|
|
2666
|
-
const { TuiApp } = await import("./tui/app.jsx");
|
|
2667
|
-
const React = await import("react");
|
|
2668
|
-
renderer = await createCliRenderer({ exitOnCtrlC: false });
|
|
2669
|
-
const root = createRoot(renderer);
|
|
2670
|
-
await new Promise((resolve) => {
|
|
2671
|
-
root.render(React.createElement(TuiApp, {
|
|
2672
|
-
adapter,
|
|
2673
|
-
onExit: () => resolve(true),
|
|
2674
|
-
}));
|
|
2675
|
-
});
|
|
2676
|
-
return c.ok(undefined);
|
|
2677
|
-
}
|
|
2678
|
-
catch (err) {
|
|
2679
|
-
return fail({ code: "TUI_FAILED", message: err?.message ?? String(err), exitCode: 1 });
|
|
2680
|
-
}
|
|
2681
|
-
finally {
|
|
2682
|
-
if (renderer)
|
|
2683
|
-
renderer.destroy();
|
|
2684
|
-
cleanup?.();
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
2679
|
})
|
|
2688
2680
|
// =========================================================================
|
|
2689
2681
|
// smithers ps
|
|
@@ -4966,6 +4958,22 @@ const cli = Cli.create({
|
|
|
4966
4958
|
return c.error({ code: "GUI_LAUNCH_FAILED", message: err?.message ?? String(err), exitCode: 1 });
|
|
4967
4959
|
}
|
|
4968
4960
|
},
|
|
4961
|
+
})
|
|
4962
|
+
// =========================================================================
|
|
4963
|
+
// smithers docs / smithers docs-full
|
|
4964
|
+
// Print the published llms.txt / llms-full.txt from smithers.sh.
|
|
4965
|
+
// =========================================================================
|
|
4966
|
+
.command("docs", {
|
|
4967
|
+
description: "Print llms.txt (concise docs index for LLMs) from smithers.sh.",
|
|
4968
|
+
async run(c) {
|
|
4969
|
+
return printSmithersDocs(c, "https://smithers.sh/llms.txt", "DOCS_FETCH_FAILED");
|
|
4970
|
+
},
|
|
4971
|
+
})
|
|
4972
|
+
.command("docs-full", {
|
|
4973
|
+
description: "Print llms-full.txt (full docs bundle for LLMs) from smithers.sh.",
|
|
4974
|
+
async run(c) {
|
|
4975
|
+
return printSmithersDocs(c, "https://smithers.sh/llms-full.txt", "DOCS_FULL_FETCH_FAILED");
|
|
4976
|
+
},
|
|
4969
4977
|
})
|
|
4970
4978
|
.command(workflowCli)
|
|
4971
4979
|
.command(cronCli)
|
package/src/workflow-pack.js
CHANGED
|
@@ -133,7 +133,6 @@ function renderPackageJson(versions) {
|
|
|
133
133
|
dependencies: {
|
|
134
134
|
react: versions.reactVersion,
|
|
135
135
|
"react-dom": versions.reactDomVersion,
|
|
136
|
-
skills: "github:mattpocock/skills",
|
|
137
136
|
"smithers-orchestrator": smithersSpec,
|
|
138
137
|
zod: versions.zodVersion,
|
|
139
138
|
},
|
|
@@ -413,6 +412,17 @@ function renderPrompts() {
|
|
|
413
412
|
"",
|
|
414
413
|
].join("\n"),
|
|
415
414
|
},
|
|
415
|
+
{
|
|
416
|
+
path: ".smithers/prompts/grill-me.mdx",
|
|
417
|
+
contents: [
|
|
418
|
+
"Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.",
|
|
419
|
+
"",
|
|
420
|
+
"Ask the questions one at a time.",
|
|
421
|
+
"",
|
|
422
|
+
"If a question can be answered by exploring the codebase, explore the codebase instead.",
|
|
423
|
+
"",
|
|
424
|
+
].join("\n"),
|
|
425
|
+
},
|
|
416
426
|
{
|
|
417
427
|
path: ".smithers/prompts/write-a-prd.mdx",
|
|
418
428
|
contents: [
|
|
@@ -1185,7 +1195,7 @@ function renderComponents() {
|
|
|
1185
1195
|
"/** @jsxImportSource smithers-orchestrator */",
|
|
1186
1196
|
'import { Loop, Sequence, Task, type AgentLike, type OutputTarget } from "smithers-orchestrator";',
|
|
1187
1197
|
'import { z } from "zod/v4";',
|
|
1188
|
-
'import GrillMeSkill from "
|
|
1198
|
+
'import GrillMeSkill from "../prompts/grill-me.mdx";',
|
|
1189
1199
|
'import AskUserInstructions from "../prompts/ask-user-instructions.mdx";',
|
|
1190
1200
|
"",
|
|
1191
1201
|
"export const grillOutputSchema = z.looseObject({",
|
|
@@ -3417,10 +3427,9 @@ export function initWorkflowPack(options = {}) {
|
|
|
3417
3427
|
};
|
|
3418
3428
|
}
|
|
3419
3429
|
/**
|
|
3420
|
-
* Install `.smithers/` workspace deps so
|
|
3421
|
-
*
|
|
3422
|
-
*
|
|
3423
|
-
* the scaffold is on disk, the user can always re-run `bun install` by hand.
|
|
3430
|
+
* Install `.smithers/` workspace deps so the first workflow run isn't blocked
|
|
3431
|
+
* on a cold install. Failures here don't fail init: the scaffold is on disk,
|
|
3432
|
+
* the user can always re-run `bun install` by hand.
|
|
3424
3433
|
*
|
|
3425
3434
|
* @param {string} rootDir
|
|
3426
3435
|
* @param {boolean} skip
|
package/src/tui/app.jsx
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import React, { useEffect, useState } from "react";
|
|
3
|
-
import { useKeyboard } from "@opentui/react";
|
|
4
|
-
import { RunsList } from "./components/RunsList.jsx";
|
|
5
|
-
import { WorkflowLauncher } from "./components/WorkflowLauncher.jsx";
|
|
6
|
-
import { RunDetailView } from "./components/RunDetailView.jsx";
|
|
7
|
-
import { NodeDetailView } from "./components/NodeDetailView.jsx";
|
|
8
|
-
import { AskModal } from "./components/AskModal.jsx";
|
|
9
|
-
import { SqliteBrowser } from "./components/SqliteBrowser.jsx";
|
|
10
|
-
import { CronList } from "./components/CronList.jsx";
|
|
11
|
-
import { MetricsPane } from "./components/MetricsPane.jsx";
|
|
12
|
-
/** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
|
|
13
|
-
|
|
14
|
-
const TABS = [
|
|
15
|
-
{ id: "runs", label: "Runs", access: "r" },
|
|
16
|
-
{ id: "ask", label: "Agent Console", access: "a" },
|
|
17
|
-
{ id: "crons", label: "Triggers", access: "t" },
|
|
18
|
-
{ id: "metrics", label: "Telemetry", access: "m" },
|
|
19
|
-
{ id: "sqlite", label: "Data Grid", access: "s" },
|
|
20
|
-
];
|
|
21
|
-
/**
|
|
22
|
-
* @param {{ adapter: SmithersDb; onExit: () => void; }} value
|
|
23
|
-
*/
|
|
24
|
-
export function TuiApp({ adapter, onExit, }) {
|
|
25
|
-
const [view, setView] = useState("runs");
|
|
26
|
-
const [selectedRunId, setSelectedRunId] = useState(null);
|
|
27
|
-
const [selectedNodeId, setSelectedNodeId] = useState(null);
|
|
28
|
-
const activeTabId = (view === "detail" || view === "node" || view === "launcher") ? "runs" : view;
|
|
29
|
-
useKeyboard(async (key) => {
|
|
30
|
-
// Tab switching
|
|
31
|
-
if (key.name === "left" || key.name === "right") {
|
|
32
|
-
// only accept left/right at the root view so we don't clobber text inputs
|
|
33
|
-
if (view === "runs" || view === "crons" || view === "metrics") {
|
|
34
|
-
const currentIndex = TABS.findIndex((t) => t.id === activeTabId);
|
|
35
|
-
if (key.name === "right") {
|
|
36
|
-
const next = TABS[(currentIndex + 1) % TABS.length];
|
|
37
|
-
setView(next.id);
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
const prev = TABS[(currentIndex - 1 + TABS.length) % TABS.length];
|
|
41
|
-
setView(prev.id);
|
|
42
|
-
}
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (key.name === "s" && view !== "sqlite" && view !== "ask") {
|
|
47
|
-
setView("sqlite");
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (key.name === "t" && view !== "crons" && view !== "ask") {
|
|
51
|
-
setView("crons");
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
if (key.name === "m" && view !== "metrics" && view !== "ask") {
|
|
55
|
-
setView("metrics");
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (view === "runs") {
|
|
59
|
-
if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
|
|
60
|
-
onExit();
|
|
61
|
-
}
|
|
62
|
-
if (key.name === "n") {
|
|
63
|
-
setView("launcher");
|
|
64
|
-
}
|
|
65
|
-
if (key.name === "a") {
|
|
66
|
-
setView("ask");
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
else if (view === "detail") {
|
|
70
|
-
if (key.name === "escape") {
|
|
71
|
-
setView("runs");
|
|
72
|
-
}
|
|
73
|
-
else if (key.name === "c" && key.ctrl) {
|
|
74
|
-
onExit();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
else if (view === "node") {
|
|
78
|
-
if (key.name === "escape") {
|
|
79
|
-
setView("detail");
|
|
80
|
-
}
|
|
81
|
-
else if (key.name === "c" && key.ctrl) {
|
|
82
|
-
onExit();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
else if (view === "launcher") {
|
|
86
|
-
if (key.name === "escape") {
|
|
87
|
-
setView("runs");
|
|
88
|
-
}
|
|
89
|
-
else if (key.name === "c" && key.ctrl) {
|
|
90
|
-
onExit();
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
else if (view === "ask") {
|
|
94
|
-
if (key.name === "escape") {
|
|
95
|
-
setView("runs");
|
|
96
|
-
}
|
|
97
|
-
else if (key.name === "c" && key.ctrl) {
|
|
98
|
-
onExit();
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "column" }}>
|
|
103
|
-
{/* Global Tab Header */}
|
|
104
|
-
<box style={{ width: "100%", height: 3, borderBottom: true, borderColor: "gray", flexDirection: "row", paddingLeft: 1 }}>
|
|
105
|
-
{TABS.map((tab) => {
|
|
106
|
-
const isActive = activeTabId === tab.id;
|
|
107
|
-
return (<text key={tab.id} style={{ color: isActive ? "#a7f3d0" : "gray", marginRight: 3 }}>
|
|
108
|
-
{isActive ? "▶ " : " "}[{tab.access.toUpperCase()}] {tab.label}
|
|
109
|
-
</text>);
|
|
110
|
-
})}
|
|
111
|
-
</box>
|
|
112
|
-
|
|
113
|
-
{view === "runs" && (<box style={{ flexGrow: 1, width: "100%", height: "100%", border: true, borderColor: "#34d399", flexDirection: "column" }} title="Smithers Runs - [Enter] View Details | [N] New Run | [Esc] Exit">
|
|
114
|
-
<RunsList adapter={adapter} focused={view === "runs"} onChange={setSelectedRunId} onSubmit={(runId) => {
|
|
115
|
-
setSelectedRunId(runId);
|
|
116
|
-
setView("detail");
|
|
117
|
-
}}/>
|
|
118
|
-
</box>)}
|
|
119
|
-
|
|
120
|
-
{view === "detail" && selectedRunId && (<RunDetailView adapter={adapter} runId={selectedRunId} onBack={() => setView("runs")} onSelectNode={(nodeId) => {
|
|
121
|
-
setSelectedNodeId(nodeId);
|
|
122
|
-
setView("node");
|
|
123
|
-
}}/>)}
|
|
124
|
-
|
|
125
|
-
{view === "node" && selectedRunId && (<NodeDetailView adapter={adapter} runId={selectedRunId} nodeId={selectedNodeId} onBack={() => setView("detail")}/>)}
|
|
126
|
-
|
|
127
|
-
{view === "launcher" && (<WorkflowLauncher onClose={() => setView("runs")}/>)}
|
|
128
|
-
{view === "ask" && (<AskModal onClose={() => setView("runs")}/>)}
|
|
129
|
-
{view === "sqlite" && (<box style={{ flexGrow: 1, width: "100%", height: "100%", border: true, borderColor: "#34d399", flexDirection: "column" }} title="Smithers DB - [Esc] Return to Runs | [Tab] Switch Panes | [Up/Down] Query Table">
|
|
130
|
-
<SqliteBrowser adapter={adapter} onBack={() => setView("runs")}/>
|
|
131
|
-
</box>)}
|
|
132
|
-
{view === "crons" && (<box style={{ flexGrow: 1, width: "100%", height: "100%", border: true, borderColor: "#34d399", flexDirection: "column" }} title="Smithers Schedule Triggers - [Esc] Return to Runs | [Up/Down] Select | [Del] Remove">
|
|
133
|
-
<CronList adapter={adapter} onBack={() => setView("runs")}/>
|
|
134
|
-
</box>)}
|
|
135
|
-
{view === "metrics" && (<box style={{ flexGrow: 1, width: "100%", height: "100%", border: true, borderColor: "#34d399", flexDirection: "column" }} title="Smithers Telemetry (Prometheus Rollup) - [Esc] Return to Runs">
|
|
136
|
-
<MetricsPane adapter={adapter} onBack={() => setView("runs")}/>
|
|
137
|
-
</box>)}
|
|
138
|
-
</box>);
|
|
139
|
-
}
|
package/src/tui/app.tsx
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import React, { useState, useEffect } from "react";
|
|
3
|
-
import { useKeyboard } from "@opentui/react";
|
|
4
|
-
/**
|
|
5
|
-
* @param {{ onClose: () => void }} value
|
|
6
|
-
*/
|
|
7
|
-
export function AskModal({ onClose }) {
|
|
8
|
-
const [question, setQuestion] = useState("");
|
|
9
|
-
const [answer, setAnswer] = useState("");
|
|
10
|
-
const [status, setStatus] = useState("input");
|
|
11
|
-
useKeyboard((key) => {
|
|
12
|
-
if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
|
|
13
|
-
if (status !== "streaming") { // Or allow cancel mid-stream if we kill the proc? Keep simple for now
|
|
14
|
-
onClose();
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
if (status === "input") {
|
|
19
|
-
if (key.name === "backspace") {
|
|
20
|
-
setQuestion((q) => q.slice(0, -1));
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (key.name === "enter" || key.name === "return") {
|
|
24
|
-
if (question.trim().length > 0) {
|
|
25
|
-
startAsk();
|
|
26
|
-
}
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
// Basic typing capture (sequence is the literal ansi char)
|
|
30
|
-
if (!key.ctrl && !key.meta && key.sequence && key.sequence.length === 1 && key.name !== "up" && key.name !== "down" && key.name !== "left" && key.name !== "right") {
|
|
31
|
-
setQuestion((q) => q + key.sequence);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
async function startAsk() {
|
|
36
|
-
setStatus("streaming");
|
|
37
|
-
setAnswer("");
|
|
38
|
-
try {
|
|
39
|
-
const proc = Bun.spawn(["bun", "run", "src/index.js", "ask", question], {
|
|
40
|
-
stdout: "pipe",
|
|
41
|
-
stderr: "pipe",
|
|
42
|
-
});
|
|
43
|
-
// Stream stdout async
|
|
44
|
-
(async () => {
|
|
45
|
-
try {
|
|
46
|
-
const stream = proc.stdout;
|
|
47
|
-
const reader = stream.getReader();
|
|
48
|
-
const decoder = new TextDecoder();
|
|
49
|
-
while (true) {
|
|
50
|
-
const { done, value } = await reader.read();
|
|
51
|
-
if (done)
|
|
52
|
-
break;
|
|
53
|
-
const text = decoder.decode(value);
|
|
54
|
-
setAnswer((a) => a + text);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
catch { }
|
|
58
|
-
})();
|
|
59
|
-
// Stream stderr async (in case the agent prints progress or errors there)
|
|
60
|
-
(async () => {
|
|
61
|
-
try {
|
|
62
|
-
const stream = proc.stderr;
|
|
63
|
-
const reader = stream.getReader();
|
|
64
|
-
const decoder = new TextDecoder();
|
|
65
|
-
while (true) {
|
|
66
|
-
const { done, value } = await reader.read();
|
|
67
|
-
if (done)
|
|
68
|
-
break;
|
|
69
|
-
const text = decoder.decode(value);
|
|
70
|
-
setAnswer((a) => a + text);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch { }
|
|
74
|
-
})();
|
|
75
|
-
await proc.exited;
|
|
76
|
-
if (proc.exitCode !== 0 && answer.trim().length === 0) {
|
|
77
|
-
setAnswer("Failed to run ask command. Is your agent installed?");
|
|
78
|
-
setStatus("error");
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
setStatus("done");
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
setAnswer(`Spawn error: ${err.message}`);
|
|
86
|
-
setStatus("error");
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
// Auto scroll logic in OpenTUI: scrollboxes generally stay at the top unless navigated?
|
|
90
|
-
// For streaming, we'll just append text.
|
|
91
|
-
return (<box style={{
|
|
92
|
-
flexGrow: 1,
|
|
93
|
-
width: "100%",
|
|
94
|
-
height: "100%",
|
|
95
|
-
border: true,
|
|
96
|
-
borderColor: "magenta",
|
|
97
|
-
flexDirection: "column",
|
|
98
|
-
}} title={`Ask Smithers ${status === "input" ? "[Type Question, Enter to Submit, Esc to Close]" : "[Streaming... Esc to Close]"}`}>
|
|
99
|
-
{status === "input" ? (<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
100
|
-
<text style={{ color: "cyan" }}>What would you like to know about the Smithers orchestrator?</text>
|
|
101
|
-
<text style={{ color: "white", marginTop: 1 }}>{"> "}{question}█</text>
|
|
102
|
-
</box>) : (<scrollbox style={{ width: "100%", height: "100%", flexDirection: "column", paddingLeft: 1 }}>
|
|
103
|
-
<text style={{ color: "cyan", marginBottom: 1 }}>Q: {question}</text>
|
|
104
|
-
<text style={{ color: "white" }}>{answer}</text>
|
|
105
|
-
{status === "done" && <text style={{ color: "green", marginTop: 1 }}>[ Agent finished. Press Esc to close. ]</text>}
|
|
106
|
-
{status === "error" && <text style={{ color: "red", marginTop: 1 }}>[ Agent failed. Press Esc to close. ]</text>}
|
|
107
|
-
</scrollbox>)}
|
|
108
|
-
</box>);
|
|
109
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import React, { useEffect, useState, useCallback } from "react";
|
|
3
|
-
import { useKeyboard } from "@opentui/react";
|
|
4
|
-
import { formatAge } from "../../format.js";
|
|
5
|
-
/** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @param {{ adapter: SmithersDb; focused: boolean; onSelectRun?: (runId: string) => void; }} value
|
|
9
|
-
*/
|
|
10
|
-
export function AttentionPane({ adapter, focused, onSelectRun, }) {
|
|
11
|
-
const [items, setItems] = useState([]);
|
|
12
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
let mounted = true;
|
|
15
|
-
async function poll() {
|
|
16
|
-
if (!mounted)
|
|
17
|
-
return;
|
|
18
|
-
try {
|
|
19
|
-
const result = [];
|
|
20
|
-
// Active alerts
|
|
21
|
-
const alerts = await adapter.listAlerts(100, ["firing", "acknowledged"]);
|
|
22
|
-
for (const alert of alerts) {
|
|
23
|
-
result.push({
|
|
24
|
-
kind: "alert",
|
|
25
|
-
id: alert.alertId,
|
|
26
|
-
severity: alert.severity,
|
|
27
|
-
status: alert.status,
|
|
28
|
-
runId: alert.runId ?? null,
|
|
29
|
-
nodeId: alert.nodeId ?? null,
|
|
30
|
-
message: alert.message,
|
|
31
|
-
firedAtMs: alert.firedAtMs ?? null,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
// Pending approvals
|
|
35
|
-
const runs = await adapter.listRuns(100);
|
|
36
|
-
for (const run of runs) {
|
|
37
|
-
const pending = await adapter.listPendingApprovals(run.runId);
|
|
38
|
-
for (const ap of pending) {
|
|
39
|
-
result.push({
|
|
40
|
-
kind: "approval",
|
|
41
|
-
id: `${ap.runId}:${ap.nodeId}:${ap.iteration ?? 0}`,
|
|
42
|
-
severity: "info",
|
|
43
|
-
status: "pending",
|
|
44
|
-
runId: ap.runId,
|
|
45
|
-
nodeId: ap.nodeId,
|
|
46
|
-
message: ap.note ?? `Approval for ${ap.nodeId}`,
|
|
47
|
-
firedAtMs: ap.requestedAtMs ?? null,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// Sort: critical first, then warning, then info
|
|
52
|
-
const order = { critical: 0, warning: 1, info: 2 };
|
|
53
|
-
result.sort((a, b) => (order[a.severity] ?? 3) - (order[b.severity] ?? 3));
|
|
54
|
-
if (mounted)
|
|
55
|
-
setItems(result);
|
|
56
|
-
}
|
|
57
|
-
catch { }
|
|
58
|
-
if (mounted)
|
|
59
|
-
setTimeout(poll, 2000);
|
|
60
|
-
}
|
|
61
|
-
poll();
|
|
62
|
-
return () => { mounted = false; };
|
|
63
|
-
}, [adapter]);
|
|
64
|
-
const selected = items[selectedIndex];
|
|
65
|
-
useKeyboard(focused, useCallback((key) => {
|
|
66
|
-
if (key === "up" || key === "k") {
|
|
67
|
-
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
68
|
-
}
|
|
69
|
-
else if (key === "down" || key === "j") {
|
|
70
|
-
setSelectedIndex((i) => Math.min(items.length - 1, i + 1));
|
|
71
|
-
}
|
|
72
|
-
else if (key === "a" && selected?.kind === "alert" && selected.status === "firing") {
|
|
73
|
-
// Ack
|
|
74
|
-
void adapter.acknowledgeAlert(selected.id, Date.now());
|
|
75
|
-
}
|
|
76
|
-
else if (key === "r" && selected?.kind === "alert") {
|
|
77
|
-
// Resolve
|
|
78
|
-
void adapter.resolveAlert(selected.id, Date.now());
|
|
79
|
-
}
|
|
80
|
-
else if (key === "s" && selected?.kind === "alert") {
|
|
81
|
-
// Silence for 1h
|
|
82
|
-
void adapter.silenceAlert(selected.id, Date.now() + 3_600_000);
|
|
83
|
-
}
|
|
84
|
-
else if (key === "enter" && selected?.runId && onSelectRun) {
|
|
85
|
-
onSelectRun(selected.runId);
|
|
86
|
-
}
|
|
87
|
-
}, [items, selectedIndex, selected, adapter, onSelectRun]));
|
|
88
|
-
const severityCounts = {
|
|
89
|
-
critical: items.filter((i) => i.severity === "critical").length,
|
|
90
|
-
warning: items.filter((i) => i.severity === "warning").length,
|
|
91
|
-
info: items.filter((i) => i.severity === "info").length,
|
|
92
|
-
};
|
|
93
|
-
const header = [
|
|
94
|
-
`Attention (${items.length})`,
|
|
95
|
-
severityCounts.critical > 0 ? ` 🔴${severityCounts.critical}` : "",
|
|
96
|
-
severityCounts.warning > 0 ? ` 🟡${severityCounts.warning}` : "",
|
|
97
|
-
severityCounts.info > 0 ? ` 🔵${severityCounts.info}` : "",
|
|
98
|
-
].join("");
|
|
99
|
-
return (<box flexDirection="column">
|
|
100
|
-
<text bold>{header}</text>
|
|
101
|
-
<text dimColor> [a]ck [r]esolve [s]ilence [Enter] open run</text>
|
|
102
|
-
{items.length === 0 ? (<text dimColor> All clear — no attention items.</text>) : (items.map((item, i) => {
|
|
103
|
-
const isSelected = i === selectedIndex && focused;
|
|
104
|
-
const sev = item.severity === "critical" ? "🔴" : item.severity === "warning" ? "🟡" : "🔵";
|
|
105
|
-
const age = item.firedAtMs ? formatAge(item.firedAtMs) : "";
|
|
106
|
-
const prefix = isSelected ? "▸ " : " ";
|
|
107
|
-
return (<text key={item.id} inverse={isSelected}>
|
|
108
|
-
{prefix}{sev} [{item.kind}] {item.message} ({item.status}) {age}
|
|
109
|
-
</text>);
|
|
110
|
-
}))}
|
|
111
|
-
</box>);
|
|
112
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
|
|
2
|
-
export declare function AttentionPane({ adapter, focused, onSelectRun, }: {
|
|
3
|
-
adapter: SmithersDb;
|
|
4
|
-
focused: boolean;
|
|
5
|
-
onSelectRun?: (runId: string) => void;
|
|
6
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import React, { useEffect, useState } from "react";
|
|
3
|
-
import { formatChatBlock, parseChatAttemptMeta, selectChatAttempts } from "../../chat.js";
|
|
4
|
-
/** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @param {{ adapter: SmithersDb; runId: string; focused: boolean; filterNodeId?: string; }} value
|
|
8
|
-
*/
|
|
9
|
-
export function ChatPane({ adapter, runId, focused, filterNodeId, }) {
|
|
10
|
-
const [chatLines, setChatLines] = useState([]);
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
let mounted = true;
|
|
13
|
-
async function fetchChat() {
|
|
14
|
-
if (!mounted)
|
|
15
|
-
return;
|
|
16
|
-
try {
|
|
17
|
-
const attempts = await adapter.listAttemptsForRun(runId);
|
|
18
|
-
let lines = [];
|
|
19
|
-
for (const attempt of attempts) {
|
|
20
|
-
if (filterNodeId && attempt.nodeId !== filterNodeId)
|
|
21
|
-
continue;
|
|
22
|
-
const meta = parseChatAttemptMeta(attempt.metaJson ?? "");
|
|
23
|
-
if (!meta.prompt && !attempt.responseText)
|
|
24
|
-
continue; // Skip empty attempts
|
|
25
|
-
if (lines.length > 0)
|
|
26
|
-
lines.push("");
|
|
27
|
-
lines.push(`=== ${attempt.nodeId} (Attempt ${attempt.attempt}, Iteration ${attempt.iteration}) ===`);
|
|
28
|
-
if (meta.prompt) {
|
|
29
|
-
lines.push(`[USER]`);
|
|
30
|
-
lines.push(...String(meta.prompt).trim().split("\n").map(l => ` ${l}`));
|
|
31
|
-
lines.push("");
|
|
32
|
-
}
|
|
33
|
-
if (attempt.responseText) {
|
|
34
|
-
lines.push(`[ASSISTANT]`);
|
|
35
|
-
lines.push(...String(attempt.responseText).trim().split("\n").map(l => ` ${l}`));
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (mounted) {
|
|
39
|
-
setChatLines(lines);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
catch (err) { }
|
|
43
|
-
if (mounted)
|
|
44
|
-
setTimeout(fetchChat, 1000);
|
|
45
|
-
}
|
|
46
|
-
fetchChat();
|
|
47
|
-
return () => {
|
|
48
|
-
mounted = false;
|
|
49
|
-
};
|
|
50
|
-
}, [adapter, runId]);
|
|
51
|
-
return (<scrollbox focused={focused} style={{ width: "100%", height: "100%", paddingLeft: 1, paddingRight: 1 }}>
|
|
52
|
-
<box flexDirection="column">
|
|
53
|
-
{chatLines.map((line, index) => (<text key={index}>{line}</text>))}
|
|
54
|
-
{chatLines.length === 0 && <text>No chat history available.</text>}
|
|
55
|
-
</box>
|
|
56
|
-
</scrollbox>);
|
|
57
|
-
}
|