catherd-cli 0.2.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -33
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/catalog/jev.json +89 -0
- package/catalog/models.json +236 -0
- package/catalog/scores.json +366 -0
- package/package.json +24 -8
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.mcp.json +2 -2
- package/plugin/bin/catherd-mcp +30 -0
- package/plugin/commands/catherd.md +2 -2
- package/plugin/skills/catherd/SKILL.md +160 -97
- package/plugin/skills/catherd-setup/SKILL.md +34 -24
- package/src/adapters/access.ts +63 -0
- package/src/adapters/all.ts +8 -0
- package/src/adapters/backend.ts +159 -0
- package/src/adapters/claude-code/events.ts +86 -0
- package/src/adapters/claude-code/index.ts +317 -0
- package/src/adapters/claude-code/models-api.ts +65 -0
- package/src/adapters/claude-code/models.ts +26 -0
- package/src/adapters/cli.ts +74 -0
- package/src/adapters/codex/events.ts +65 -0
- package/src/adapters/codex/home.ts +47 -0
- package/src/adapters/codex/index.ts +282 -0
- package/src/adapters/discovery.ts +87 -0
- package/src/adapters/opencode/agents.ts +84 -0
- package/src/adapters/opencode/events.ts +109 -0
- package/src/adapters/opencode/index.ts +333 -0
- package/src/adapters/registry.ts +14 -0
- package/src/cli.ts +159 -19
- package/src/domain/agents.ts +60 -0
- package/src/domain/budget.ts +45 -0
- package/src/domain/catalog.ts +262 -0
- package/src/domain/changes.ts +52 -0
- package/src/domain/cost.ts +89 -0
- package/src/domain/errors.ts +60 -0
- package/src/domain/failover.ts +119 -0
- package/src/domain/hints.ts +22 -0
- package/src/domain/ids.ts +97 -0
- package/src/domain/jev.ts +283 -0
- package/src/domain/lane.ts +92 -0
- package/src/domain/notice.ts +95 -0
- package/src/domain/profile-rules.ts +239 -0
- package/src/domain/profile.ts +389 -0
- package/src/domain/record.ts +86 -0
- package/src/{profile → domain}/role-prompts.ts +75 -27
- package/src/domain/roles.ts +25 -0
- package/src/domain/route.ts +126 -0
- package/src/domain/runtime.ts +21 -0
- package/src/domain/sanitize.ts +29 -0
- package/src/domain/secrets.ts +57 -0
- package/src/domain/select.ts +136 -0
- package/src/domain/state.ts +49 -0
- package/src/domain/util.ts +18 -0
- package/src/entry/capture-fixtures-command.ts +57 -0
- package/src/entry/catalog-command.ts +106 -0
- package/src/entry/cli-kit.ts +36 -0
- package/src/entry/deps.ts +17 -0
- package/src/entry/doctor-command.ts +51 -0
- package/src/entry/glyphs.ts +34 -0
- package/src/entry/init-command.ts +182 -0
- package/src/entry/lock-command.ts +123 -0
- package/src/{mcp/commands.ts → entry/mcp/command.ts} +5 -1
- package/src/entry/mcp/dispatch-tools.ts +52 -0
- package/src/entry/mcp/handshake.ts +58 -0
- package/src/entry/mcp/lane-tools.ts +84 -0
- package/src/entry/mcp/protocol-tools.ts +57 -0
- package/src/entry/mcp/result.ts +51 -0
- package/src/entry/mcp/run-tools.ts +145 -0
- package/src/entry/mcp/server.ts +88 -0
- package/src/entry/mcp/setup-tools.ts +81 -0
- package/src/entry/profile-command.ts +318 -0
- package/src/entry/prompt.ts +103 -0
- package/src/entry/runs-command.ts +260 -0
- package/src/entry/supervise-bin.ts +18 -0
- package/src/entry/supervise-command.ts +37 -0
- package/src/entry/tui/commands.ts +545 -0
- package/src/entry/tui/effects.ts +312 -0
- package/src/entry/tui/fixtures.ts +585 -0
- package/src/entry/tui/profile-edits.ts +138 -0
- package/src/entry/tui/profile-tree.ts +420 -0
- package/src/entry/tui/providers/app.tsx +162 -0
- package/src/entry/tui/providers/boundary.tsx +57 -0
- package/src/entry/tui/providers/data.tsx +160 -0
- package/src/entry/tui/providers/keymap.tsx +166 -0
- package/src/entry/tui/providers/root.tsx +45 -0
- package/src/entry/tui/providers/theme.tsx +17 -0
- package/src/entry/tui/providers/toast.tsx +77 -0
- package/src/entry/tui/run.tsx +98 -0
- package/src/entry/tui/state.ts +356 -0
- package/src/entry/tui/stories.ts +107 -0
- package/src/entry/tui/text.ts +130 -0
- package/src/entry/tui/theme.ts +140 -0
- package/src/entry/tui/views/app.tsx +208 -0
- package/src/entry/tui/views/dialogs.tsx +41 -0
- package/src/entry/tui/views/profile-actions.ts +321 -0
- package/src/entry/tui/views/profiles.tsx +440 -0
- package/src/entry/tui/views/runs.tsx +573 -0
- package/src/entry/tui/views/save-dialog.tsx +332 -0
- package/src/entry/tui/views/status.tsx +209 -0
- package/src/entry/tui/widgets/chrome.tsx +114 -0
- package/src/entry/tui/widgets/dialog-confirm.tsx +58 -0
- package/src/entry/tui/widgets/dialog-prompt.tsx +52 -0
- package/src/entry/tui/widgets/dialog-select.tsx +212 -0
- package/src/entry/tui/widgets/dialog.tsx +68 -0
- package/src/entry/tui/widgets/line.tsx +92 -0
- package/src/entry/tui/widgets/list.tsx +197 -0
- package/src/entry/tui/widgets/spinner.tsx +49 -0
- package/src/entry/tui/widgets/toast.tsx +62 -0
- package/src/infra/assets.ts +4 -0
- package/src/infra/claude-session.ts +98 -0
- package/src/infra/dispatch-dir.ts +286 -0
- package/src/infra/env.ts +67 -0
- package/src/infra/filelock.ts +159 -0
- package/src/infra/git.ts +91 -0
- package/src/infra/heavy-lock.ts +36 -0
- package/src/infra/jev-client.ts +162 -0
- package/src/infra/launch.ts +25 -0
- package/src/infra/log.ts +84 -0
- package/src/infra/paths.ts +38 -0
- package/src/infra/peer-inbox.ts +92 -0
- package/src/infra/proc.ts +110 -0
- package/src/infra/store.ts +229 -0
- package/src/infra/supervisor.ts +287 -0
- package/src/infra/version.ts +6 -0
- package/src/services/admission.ts +303 -0
- package/src/services/agent-links.ts +139 -0
- package/src/services/backends.ts +44 -0
- package/src/services/budget.ts +47 -0
- package/src/services/capture.ts +233 -0
- package/src/services/catalog-service.ts +382 -0
- package/src/services/dispatch-service.ts +620 -0
- package/src/services/dispatches.ts +174 -0
- package/src/services/doctor-access.ts +231 -0
- package/src/services/doctor-backends.ts +170 -0
- package/src/services/doctor-checks.ts +148 -0
- package/src/services/doctor-push.ts +109 -0
- package/src/services/doctor.ts +338 -0
- package/src/services/finalize.ts +377 -0
- package/src/services/gate-service.ts +288 -0
- package/src/services/global-install.ts +99 -0
- package/src/services/jev-service.ts +183 -0
- package/src/services/lane-service.ts +390 -0
- package/src/services/milestones.ts +208 -0
- package/src/services/notifier.ts +296 -0
- package/src/services/peek.ts +103 -0
- package/src/services/ports.ts +135 -0
- package/src/services/preflight.ts +221 -0
- package/src/services/profile-service.ts +292 -0
- package/src/services/profile-store.ts +141 -0
- package/src/services/protocol.ts +212 -0
- package/src/services/questions.ts +102 -0
- package/src/services/reconcile.ts +130 -0
- package/src/services/reentry.ts +22 -0
- package/src/services/routing-service.ts +190 -0
- package/src/services/run-debug.ts +79 -0
- package/src/services/run-service.ts +194 -0
- package/src/services/run-store.ts +334 -0
- package/src/services/runs-page.ts +254 -0
- package/src/services/session-view.ts +152 -0
- package/src/services/sessions.ts +92 -0
- package/src/services/setup.ts +87 -0
- package/src/services/state.ts +111 -0
- package/src/services/summary.ts +241 -0
- package/catalog/catalog.json +0 -112
- package/catalog/models-dev.json +0 -56453
- package/src/core/codex-events.ts +0 -49
- package/src/core/codex.ts +0 -166
- package/src/core/harness.ts +0 -89
- package/src/core/lanes.ts +0 -67
- package/src/core/lock.ts +0 -111
- package/src/core/opencode.ts +0 -207
- package/src/core/reconcile.ts +0 -26
- package/src/core/reply.ts +0 -54
- package/src/core/runstore.ts +0 -163
- package/src/core/spawn.ts +0 -89
- package/src/core/status.ts +0 -193
- package/src/files.ts +0 -19
- package/src/mcp/dispatch.ts +0 -154
- package/src/mcp/lane-tools.ts +0 -200
- package/src/mcp/out.ts +0 -6
- package/src/mcp/preflight.ts +0 -84
- package/src/mcp/run-tools.ts +0 -121
- package/src/mcp/runs.ts +0 -118
- package/src/mcp/server.ts +0 -26
- package/src/mcp/setup-tools.ts +0 -175
- package/src/paths.ts +0 -20
- package/src/profile/agents.ts +0 -130
- package/src/profile/profile.ts +0 -228
- package/src/routing/catalog.ts +0 -227
- package/src/routing/commands.ts +0 -22
- package/src/routing/jev.ts +0 -185
- package/src/routing/route.ts +0 -155
- package/src/routing/select.ts +0 -90
- package/src/tui/backends.ts +0 -57
- package/src/tui/commands.ts +0 -53
- package/src/tui/dashboard.tsx +0 -245
- package/src/tui/editor.tsx +0 -214
- package/src/tui/init.tsx +0 -208
- package/src/tui/matrix-model.ts +0 -279
- package/src/tui/matrix.tsx +0 -512
- package/src/tui/profiles.ts +0 -38
- package/src/tui/theme.ts +0 -177
- package/src/tui/ui.tsx +0 -267
- package/src/tui/watch-model.ts +0 -83
- package/src/tui/watch.tsx +0 -236
- package/src/types.ts +0 -184
- package/src/version.ts +0 -7
package/src/tui/matrix.tsx
DELETED
|
@@ -1,512 +0,0 @@
|
|
|
1
|
-
import { TextAttributes } from "@opentui/core";
|
|
2
|
-
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
|
-
import { useState } from "react";
|
|
5
|
-
import type { HarnessCost } from "../core/harness.ts";
|
|
6
|
-
import { entryFor, saveTreatLike } from "../routing/catalog.ts";
|
|
7
|
-
import { candidates } from "../routing/select.ts";
|
|
8
|
-
import { type Backend, type Catalog, type Profile, type RungId, rungOf, splitRung } from "../types.ts";
|
|
9
|
-
import type { BackendStatus } from "./backends.ts";
|
|
10
|
-
import {
|
|
11
|
-
budgetDetailRows,
|
|
12
|
-
type BudgetField,
|
|
13
|
-
budgetSummary,
|
|
14
|
-
type DetailRow,
|
|
15
|
-
failoverDetailRows,
|
|
16
|
-
failoverOptions,
|
|
17
|
-
failoverSummary,
|
|
18
|
-
isTicked,
|
|
19
|
-
openKey,
|
|
20
|
-
readySet,
|
|
21
|
-
type Row,
|
|
22
|
-
rowId,
|
|
23
|
-
roleDetailRows,
|
|
24
|
-
sectionOf,
|
|
25
|
-
toggle,
|
|
26
|
-
topLevelRows,
|
|
27
|
-
type TopRow,
|
|
28
|
-
treatLikeOptions,
|
|
29
|
-
} from "./matrix-model.ts";
|
|
30
|
-
import { dot, glyph, harnessLine, shortRung, tint, type Ui } from "./theme.ts";
|
|
31
|
-
import { DetailPane, ListLine } from "./ui.tsx";
|
|
32
|
-
|
|
33
|
-
export interface MatrixProps {
|
|
34
|
-
ui: Ui;
|
|
35
|
-
profile: Profile;
|
|
36
|
-
catalog: Catalog;
|
|
37
|
-
backends: BackendStatus[];
|
|
38
|
-
costs?: HarnessCost[];
|
|
39
|
-
errors?: string[];
|
|
40
|
-
reloadCatalog: () => Catalog;
|
|
41
|
-
onChange: (p: Profile) => void;
|
|
42
|
-
onCatalog: (c: Catalog) => void;
|
|
43
|
-
onSubmit: () => void;
|
|
44
|
-
onQuit: () => void;
|
|
45
|
-
onKey?: (input: string) => void;
|
|
46
|
-
active?: boolean;
|
|
47
|
-
/** Controlled "?" help toggle, so the caller's `Frame` can render the hint bar it computes
|
|
48
|
-
* with `matrixHint` outside the panel border instead of Matrix drawing its own. */
|
|
49
|
-
onToggleHelp?: () => void;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** The hint bar text for a Matrix at the given help state — exported so the caller's `Frame`
|
|
53
|
-
* can render it outside the panel border while Matrix owns none of the layout itself. */
|
|
54
|
-
export function matrixHint(ui: Ui, submitLabel: string, showHelp: boolean, extraHint = ""): string {
|
|
55
|
-
const dotSep = ` ${glyph("dot", ui.plain)} `;
|
|
56
|
-
return showHelp
|
|
57
|
-
? `/ filter${dotSep}${extraHint}`
|
|
58
|
-
: `${glyph("keys", ui.plain)} navigate${dotSep}${glyph("arrow", ui.plain)} open${dotSep}space ticks${dotSep}enter ${submitLabel}${dotSep}? more${dotSep}q quit`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
type EffortRow = Extract<DetailRow, { kind: "effort" }>;
|
|
62
|
-
type Mode =
|
|
63
|
-
| "nav"
|
|
64
|
-
| "filter"
|
|
65
|
-
| { row: EffortRow; rung: RungId }
|
|
66
|
-
| { failover: RungId }
|
|
67
|
-
| { budgetField: BudgetField; buffer: string };
|
|
68
|
-
|
|
69
|
-
/** "sol", "luna", "opus 5.5" — the short display name the right pane uses for a model id
|
|
70
|
-
* (the full id still shows in the effort rows below it). */
|
|
71
|
-
export function shortModelName(id: string): string {
|
|
72
|
-
const base = id.replace(/^.*\//, "").replace(/^gpt-\d+-/, "");
|
|
73
|
-
return base.replace(/^claude-([a-z]+)-(\d+)-(\d+)$/, "$1 $2.$3");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function Matrix(props: MatrixProps) {
|
|
77
|
-
const { ui, profile, catalog, backends, costs = [], errors = [], active = true } = props;
|
|
78
|
-
const { height: termHeight } = useTerminalDimensions();
|
|
79
|
-
const [leftCursor, setLeftCursor] = useState(0);
|
|
80
|
-
const [focus, setFocus] = useState<"left" | "right">("left");
|
|
81
|
-
const [rightCursor, setRightCursor] = useState(0);
|
|
82
|
-
const [open, setOpen] = useState<ReadonlySet<string>>(new Set());
|
|
83
|
-
const [filter, setFilter] = useState("");
|
|
84
|
-
const [mode, setMode] = useState<Mode>("nav");
|
|
85
|
-
const [note, setNote] = useState("");
|
|
86
|
-
// "?" only does anything when the caller controls help (renders it outside the panel via
|
|
87
|
-
// `matrixHint`) — Matrix itself draws no help text, so there is nothing to toggle otherwise.
|
|
88
|
-
const toggleHelp = () => props.onToggleHelp?.();
|
|
89
|
-
const ready = readySet(backends);
|
|
90
|
-
const dotSep = ` ${glyph("dot", ui.plain)} `;
|
|
91
|
-
|
|
92
|
-
const top = topLevelRows();
|
|
93
|
-
const selTop = top[Math.min(leftCursor, top.length - 1)] as TopRow;
|
|
94
|
-
const detail: DetailRow[] =
|
|
95
|
-
selTop.kind === "role"
|
|
96
|
-
? roleDetailRows(profile, catalog, selTop.role, open, filter)
|
|
97
|
-
: selTop.kind === "budget-summary"
|
|
98
|
-
? budgetDetailRows()
|
|
99
|
-
: selTop.kind === "failover-summary"
|
|
100
|
-
? failoverDetailRows(profile, catalog)
|
|
101
|
-
: [];
|
|
102
|
-
const rightAt = Math.min(rightCursor, Math.max(0, detail.length - 1));
|
|
103
|
-
const hereRight = detail[rightAt];
|
|
104
|
-
|
|
105
|
-
const moveLeft = (idx: number) => {
|
|
106
|
-
setLeftCursor(Math.max(0, Math.min(top.length - 1, idx)));
|
|
107
|
-
setFocus("left");
|
|
108
|
-
setRightCursor(0);
|
|
109
|
-
setNote("");
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const apply = (row: Row, c: Catalog) => {
|
|
113
|
-
const t = toggle(profile, c, row, ready);
|
|
114
|
-
if ("profile" in t) {
|
|
115
|
-
props.onChange(t.profile);
|
|
116
|
-
setNote("");
|
|
117
|
-
} else if ("refused" in t) {
|
|
118
|
-
setNote(t.refused);
|
|
119
|
-
} else if (row.kind === "effort") {
|
|
120
|
-
if (treatLikeOptions(c, row.role).length === 0)
|
|
121
|
-
setNote(`No scored rung can stand in for ${t.needsTreatLike}.`);
|
|
122
|
-
else setMode({ row, rung: t.needsTreatLike });
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const collapse = () => {
|
|
127
|
-
const row = hereRight;
|
|
128
|
-
const key = row && (row.kind === "model" || row.kind === "effort") ? openKey(row) : null;
|
|
129
|
-
if (key && row && (row.kind === "effort" || open.has(key))) {
|
|
130
|
-
const next = new Set(open);
|
|
131
|
-
next.delete(key);
|
|
132
|
-
setOpen(next);
|
|
133
|
-
if (row.kind === "effort" && selTop.kind === "role") {
|
|
134
|
-
const list = roleDetailRows(profile, catalog, selTop.role, next, filter);
|
|
135
|
-
setRightCursor(
|
|
136
|
-
Math.max(
|
|
137
|
-
0,
|
|
138
|
-
list.findIndex((r) => r.kind === "model" && openKey(r) === key),
|
|
139
|
-
),
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
setFocus("left");
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
useKeyboard((key) => {
|
|
148
|
-
if (!active) return;
|
|
149
|
-
if (typeof mode !== "string") {
|
|
150
|
-
if (key.name === "escape") {
|
|
151
|
-
setMode("nav");
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if ("budgetField" in mode) {
|
|
155
|
-
if (key.name === "return") {
|
|
156
|
-
const n = mode.buffer === "" ? undefined : Number(mode.buffer);
|
|
157
|
-
const next = structuredClone(profile);
|
|
158
|
-
const budget = { ...next.budget };
|
|
159
|
-
// ponytail: "1.2.3" parses to NaN and clears the cap rather than rejecting the keystroke;
|
|
160
|
-
// the digit/"." filter below already keeps the common case sane.
|
|
161
|
-
if (n === undefined || Number.isNaN(n)) delete budget[mode.budgetField];
|
|
162
|
-
else budget[mode.budgetField] = n;
|
|
163
|
-
next.budget = Object.keys(budget).length > 0 ? budget : undefined;
|
|
164
|
-
props.onChange(next);
|
|
165
|
-
setMode("nav");
|
|
166
|
-
} else if (key.name === "backspace") {
|
|
167
|
-
setMode({ ...mode, buffer: mode.buffer.slice(0, -1) });
|
|
168
|
-
} else if (key.sequence && /^[0-9.]$/.test(key.sequence)) {
|
|
169
|
-
setMode({ ...mode, buffer: mode.buffer + key.sequence });
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (mode === "filter") {
|
|
175
|
-
if (key.name === "escape") {
|
|
176
|
-
setFilter("");
|
|
177
|
-
setMode("nav");
|
|
178
|
-
} else if (key.name === "return") setMode("nav");
|
|
179
|
-
else if (key.name === "backspace") setFilter((f) => f.slice(0, -1));
|
|
180
|
-
else if (!key.ctrl && !key.meta) {
|
|
181
|
-
const ch = key.name === "space" ? " " : key.sequence;
|
|
182
|
-
// Typed text can arrive as several keypress events inside one JS tick, before this
|
|
183
|
-
// component re-renders, so the update must build on the queued value, not the closure's.
|
|
184
|
-
if (ch && ch.length === 1) setFilter((f) => f + ch);
|
|
185
|
-
}
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
if (key.name === "up") {
|
|
189
|
-
if (focus === "left") moveLeft(leftCursor - 1);
|
|
190
|
-
else setRightCursor(Math.max(0, rightAt - 1));
|
|
191
|
-
} else if (key.name === "down") {
|
|
192
|
-
if (focus === "left") moveLeft(leftCursor + 1);
|
|
193
|
-
else setRightCursor(Math.min(Math.max(0, detail.length - 1), rightAt + 1));
|
|
194
|
-
} else if (key.name === "right") {
|
|
195
|
-
if (focus === "left") {
|
|
196
|
-
if (detail.length > 0) {
|
|
197
|
-
setFocus("right");
|
|
198
|
-
setRightCursor(0);
|
|
199
|
-
}
|
|
200
|
-
} else if (hereRight?.kind === "model") {
|
|
201
|
-
const k = openKey(hereRight);
|
|
202
|
-
if (k) setOpen(new Set(open).add(k));
|
|
203
|
-
}
|
|
204
|
-
} else if (key.name === "left") {
|
|
205
|
-
if (focus === "right") collapse();
|
|
206
|
-
} else if (key.name === "return") props.onSubmit();
|
|
207
|
-
else if (key.name === "space") {
|
|
208
|
-
if (focus === "left") {
|
|
209
|
-
if (selTop.kind === "budget-summary" || selTop.kind === "failover-summary") {
|
|
210
|
-
setFocus("right");
|
|
211
|
-
setRightCursor(0);
|
|
212
|
-
} else apply(selTop, catalog);
|
|
213
|
-
} else if (hereRight) {
|
|
214
|
-
if (hereRight.kind === "model" && !isTicked(profile, hereRight)) {
|
|
215
|
-
const k = openKey(hereRight);
|
|
216
|
-
if (k) setOpen(new Set(open).add(k));
|
|
217
|
-
} else if (hereRight.kind === "budget")
|
|
218
|
-
setMode({ budgetField: hereRight.field, buffer: String(profile.budget?.[hereRight.field] ?? "") });
|
|
219
|
-
else if (hereRight.kind === "failover") setMode({ failover: hereRight.rung });
|
|
220
|
-
else apply(hereRight, catalog);
|
|
221
|
-
}
|
|
222
|
-
} else if (key.sequence === "/") setMode("filter");
|
|
223
|
-
else if (key.sequence === "?") toggleHelp();
|
|
224
|
-
else if (key.sequence === "q") props.onQuit();
|
|
225
|
-
else if (key.sequence) props.onKey?.(key.sequence);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
if (typeof mode !== "string" && "budgetField" in mode) {
|
|
229
|
-
return (
|
|
230
|
-
<box style={{ flexDirection: "column" }}>
|
|
231
|
-
<text wrapMode="none" truncate>{`${mode.budgetField}: ${mode.buffer}_`}</text>
|
|
232
|
-
<text attributes={TextAttributes.DIM}>
|
|
233
|
-
{`digits set the cap${dotSep}backspace edits${dotSep}enter saves${dotSep}esc cancels${dotSep}empty clears it`}
|
|
234
|
-
</text>
|
|
235
|
-
</box>
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (typeof mode !== "string" && "failover" in mode) {
|
|
240
|
-
return (
|
|
241
|
-
<box style={{ flexDirection: "column" }}>
|
|
242
|
-
<text wrapMode="none" truncate>
|
|
243
|
-
{`Stand-in for ${mode.failover} on a quota limit:`}
|
|
244
|
-
</text>
|
|
245
|
-
<select
|
|
246
|
-
focused
|
|
247
|
-
showDescription={false}
|
|
248
|
-
style={{ height: 8 }}
|
|
249
|
-
options={[
|
|
250
|
-
{ name: "(none)", description: "", value: "" },
|
|
251
|
-
...failoverOptions(catalog, mode.failover).map((r) => ({ name: r, description: "", value: r })),
|
|
252
|
-
]}
|
|
253
|
-
onSelect={(_, option) => {
|
|
254
|
-
if (!option) return;
|
|
255
|
-
const next = structuredClone(profile);
|
|
256
|
-
const failover = { ...next.failover };
|
|
257
|
-
if (option.value) failover[mode.failover] = option.value as RungId;
|
|
258
|
-
else delete failover[mode.failover];
|
|
259
|
-
next.failover = Object.keys(failover).length > 0 ? failover : undefined;
|
|
260
|
-
props.onChange(next);
|
|
261
|
-
setMode("nav");
|
|
262
|
-
}}
|
|
263
|
-
/>
|
|
264
|
-
<text attributes={TextAttributes.DIM}>{`enter picks${dotSep}esc cancels`}</text>
|
|
265
|
-
</box>
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (typeof mode !== "string") {
|
|
270
|
-
return (
|
|
271
|
-
<box style={{ flexDirection: "column" }}>
|
|
272
|
-
<text wrapMode="none" truncate>
|
|
273
|
-
{`${mode.rung} has no scores yet. Treat it like which scored rung?`}
|
|
274
|
-
</text>
|
|
275
|
-
<select
|
|
276
|
-
focused
|
|
277
|
-
showDescription={false}
|
|
278
|
-
style={{ height: 8 }}
|
|
279
|
-
options={treatLikeOptions(catalog, mode.row.role).map((r) => ({
|
|
280
|
-
name: r,
|
|
281
|
-
description: "",
|
|
282
|
-
value: r,
|
|
283
|
-
}))}
|
|
284
|
-
onSelect={(_, option) => {
|
|
285
|
-
if (!option) return;
|
|
286
|
-
const like = option.value as RungId;
|
|
287
|
-
saveTreatLike(mode.rung, like);
|
|
288
|
-
const c = props.reloadCatalog();
|
|
289
|
-
props.onCatalog(c);
|
|
290
|
-
setMode("nav");
|
|
291
|
-
apply(mode.row, c);
|
|
292
|
-
}}
|
|
293
|
-
/>
|
|
294
|
-
<text attributes={TextAttributes.DIM}>{`enter picks${dotSep}esc cancels`}</text>
|
|
295
|
-
</box>
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const notReady = new Map<Backend, string>(
|
|
300
|
-
backends.filter((b) => !(b.installed && b.loggedIn)).map((b) => [b.backend, b.fix ?? ""]),
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
// --- left pane -----------------------------------------------------------------------------
|
|
304
|
-
const leftText = (r: TopRow): { text: string; dim: boolean; tag: string | null } => {
|
|
305
|
-
switch (r.kind) {
|
|
306
|
-
case "role": {
|
|
307
|
-
const on = profile.roles[r.role].enabled;
|
|
308
|
-
return {
|
|
309
|
-
text: `${dot(on ? "ready" : "missing", ui.plain)} ${r.role}`,
|
|
310
|
-
dim: !on,
|
|
311
|
-
tag: on ? null : "off",
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
case "objective":
|
|
315
|
-
return { text: "objective", dim: false, tag: profile.objective };
|
|
316
|
-
case "lock":
|
|
317
|
-
return { text: "heavy slots", dim: false, tag: String(profile.lock.heavy) };
|
|
318
|
-
case "harness":
|
|
319
|
-
return {
|
|
320
|
-
text: r.backend,
|
|
321
|
-
dim: notReady.has(r.backend),
|
|
322
|
-
tag: profile.harness[r.backend].isolated ? "isolated" : "native",
|
|
323
|
-
};
|
|
324
|
-
case "budget-summary":
|
|
325
|
-
return { text: "budget", dim: false, tag: budgetSummary(profile, ` ${glyph("dot", ui.plain)} `) };
|
|
326
|
-
case "failover-summary":
|
|
327
|
-
return { text: "failover", dim: false, tag: failoverSummary(profile) };
|
|
328
|
-
case "notify": {
|
|
329
|
-
const on = profile.notify.includes(r.moment);
|
|
330
|
-
return { text: `${dot(on ? "ready" : "missing", ui.plain)} ${r.moment}`, dim: !on, tag: null };
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
// Rows left for the panes once the cat, the border, the padding and the hint are drawn.
|
|
336
|
-
const rows = Math.max(12, termHeight || 24);
|
|
337
|
-
const LEFT_HEIGHT = Math.max(4, rows - 18);
|
|
338
|
-
const RIGHT_HEIGHT = Math.max(3, rows - 20);
|
|
339
|
-
const start = Math.max(0, Math.min(leftCursor - Math.floor(LEFT_HEIGHT / 2), top.length - LEFT_HEIGHT));
|
|
340
|
-
const leftWindow = top.slice(start, start + LEFT_HEIGHT);
|
|
341
|
-
let lastSection = "";
|
|
342
|
-
const leftLines = leftWindow.flatMap((r, i) => {
|
|
343
|
-
const idx = start + i;
|
|
344
|
-
const section = sectionOf(r);
|
|
345
|
-
const heading =
|
|
346
|
-
section !== lastSection ? (
|
|
347
|
-
<text key={`h${section}`} fg={tint("pink", ui.depth)} attributes={TextAttributes.BOLD}>
|
|
348
|
-
{section}
|
|
349
|
-
</text>
|
|
350
|
-
) : null;
|
|
351
|
-
lastSection = section;
|
|
352
|
-
const { text, dim, tag } = leftText(r);
|
|
353
|
-
return [
|
|
354
|
-
heading,
|
|
355
|
-
<ListLine
|
|
356
|
-
key={rowId(r)}
|
|
357
|
-
ui={ui}
|
|
358
|
-
selected={idx === leftCursor && focus === "left"}
|
|
359
|
-
dim={dim}
|
|
360
|
-
text={text}
|
|
361
|
-
tag={tag}
|
|
362
|
-
/>,
|
|
363
|
-
];
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// --- right pane ------------------------------------------------------------------------------
|
|
367
|
-
const dstart = Math.max(
|
|
368
|
-
0,
|
|
369
|
-
Math.min(rightAt - Math.floor(RIGHT_HEIGHT / 2), Math.max(0, detail.length - RIGHT_HEIGHT)),
|
|
370
|
-
);
|
|
371
|
-
const detailText = (r: DetailRow): { text: string; dim: boolean; tag: string | null } => {
|
|
372
|
-
switch (r.kind) {
|
|
373
|
-
case "backend": {
|
|
374
|
-
const fix = notReady.get(r.backend as Backend);
|
|
375
|
-
return {
|
|
376
|
-
text: r.backend,
|
|
377
|
-
dim: fix !== undefined,
|
|
378
|
-
tag: fix === undefined ? null : `not ready${dotSep}${fix}`,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
case "model": {
|
|
382
|
-
const efforts = profile.roles[r.role].models[r.model.id] ?? [];
|
|
383
|
-
const on = efforts.length > 0;
|
|
384
|
-
return {
|
|
385
|
-
text: `${glyph(open.has(openKey(r) ?? "") ? "open" : "shut", ui.plain)} ${dot(on ? "ready" : "missing", ui.plain)} ${shortModelName(r.model.id)} ${efforts.join(" ")}`.trimEnd(),
|
|
386
|
-
dim: !ready.has(r.model.backend),
|
|
387
|
-
tag: r.model.id.includes("/") ? (r.model.id.split("/")[0] ?? r.model.id) : r.model.id,
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
case "effort": {
|
|
391
|
-
const rung = rungOf(r.model.id, r.effort);
|
|
392
|
-
const like = catalog.treatLike[rung];
|
|
393
|
-
return {
|
|
394
|
-
text: ` ${dot(isTicked(profile, r) ? "ready" : "missing", ui.plain)} ${r.effort}`,
|
|
395
|
-
dim: !ready.has(r.model.backend),
|
|
396
|
-
tag: like ? `like ${shortRung(like)}` : entryFor(catalog, rung) ? null : "unscored",
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
case "budget": {
|
|
400
|
-
const v = profile.budget?.[r.field];
|
|
401
|
-
return { text: r.field, dim: false, tag: v === undefined ? "no cap" : String(v) };
|
|
402
|
-
}
|
|
403
|
-
case "failover": {
|
|
404
|
-
const to = profile.failover?.[r.rung];
|
|
405
|
-
return {
|
|
406
|
-
text: shortRung(r.rung),
|
|
407
|
-
dim: false,
|
|
408
|
-
tag: to ? `${glyph("arrow", ui.plain)} ${shortRung(to)}` : "no stand-in",
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
const header = (): { title: string; subtitle: string } => {
|
|
415
|
-
switch (selTop.kind) {
|
|
416
|
-
case "role": {
|
|
417
|
-
const on = profile.roles[selTop.role].enabled;
|
|
418
|
-
return { title: selTop.role, subtitle: on ? "capable models and efforts" : "off" };
|
|
419
|
-
}
|
|
420
|
-
case "objective":
|
|
421
|
-
return {
|
|
422
|
-
title: "objective",
|
|
423
|
-
subtitle: profile.objective === "cost" ? "cheapest rung first" : "fastest rung first",
|
|
424
|
-
};
|
|
425
|
-
case "lock":
|
|
426
|
-
return { title: "heavy slots", subtitle: "how many roles catherd's lock treats as heavy" };
|
|
427
|
-
case "harness":
|
|
428
|
-
return { title: selTop.backend, subtitle: "vendor harness: native config or an isolated sandbox" };
|
|
429
|
-
case "budget-summary":
|
|
430
|
-
return { title: "budget", subtitle: "minutes, tokens and dollars this profile allows a run" };
|
|
431
|
-
case "failover-summary":
|
|
432
|
-
return { title: "failover", subtitle: "a stand-in rung for a codex/opencode quota limit" };
|
|
433
|
-
case "notify":
|
|
434
|
-
return { title: "notify", subtitle: "when catherd pings you" };
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
const { title, subtitle } = header();
|
|
439
|
-
const detailBody: ReactNode[] = [];
|
|
440
|
-
if (selTop.kind === "harness") {
|
|
441
|
-
const cost = costs.find((h) => h.backend === selTop.backend);
|
|
442
|
-
detailBody.push(
|
|
443
|
-
<text key="h" wrapMode="word">
|
|
444
|
-
{harnessLine(selTop.backend, profile.harness[selTop.backend].isolated, cost, ui.plain)}
|
|
445
|
-
</text>,
|
|
446
|
-
);
|
|
447
|
-
} else if (selTop.kind === "notify") {
|
|
448
|
-
for (const m of ["milestone", "finish", "blocked"] as const) {
|
|
449
|
-
const on = profile.notify.includes(m);
|
|
450
|
-
detailBody.push(
|
|
451
|
-
<text
|
|
452
|
-
key={m}
|
|
453
|
-
wrapMode="none"
|
|
454
|
-
truncate
|
|
455
|
-
attributes={m === selTop.moment ? TextAttributes.BOLD : TextAttributes.NONE}
|
|
456
|
-
>
|
|
457
|
-
{`${dot(on ? "ready" : "missing", ui.plain)} ${m}`}
|
|
458
|
-
</text>,
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
} else {
|
|
462
|
-
detail.slice(dstart, dstart + RIGHT_HEIGHT).forEach((r, i) => {
|
|
463
|
-
const idx = dstart + i;
|
|
464
|
-
const { text, dim, tag } = detailText(r);
|
|
465
|
-
detailBody.push(
|
|
466
|
-
<ListLine
|
|
467
|
-
key={rowId(r)}
|
|
468
|
-
ui={ui}
|
|
469
|
-
selected={focus === "right" && idx === rightAt}
|
|
470
|
-
dim={dim}
|
|
471
|
-
text={text}
|
|
472
|
-
tag={tag}
|
|
473
|
-
/>,
|
|
474
|
-
);
|
|
475
|
-
});
|
|
476
|
-
if (selTop.kind === "role") {
|
|
477
|
-
const ladder = candidates(profile, catalog, selTop.role).map(
|
|
478
|
-
(r) => `${shortModelName(splitRung(r).model)} ${splitRung(r).effort}`,
|
|
479
|
-
);
|
|
480
|
-
if (ladder.length > 0)
|
|
481
|
-
detailBody.push(
|
|
482
|
-
<text key="ladder" attributes={TextAttributes.DIM} wrapMode="word">
|
|
483
|
-
{`ladder ${dotSep}${ladder.join(` ${glyph("arrow", ui.plain)} `)}`}
|
|
484
|
-
</text>,
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return (
|
|
490
|
-
<box style={{ flexDirection: "row" }}>
|
|
491
|
-
<box style={{ flexDirection: "column", width: 22 }}>{leftLines}</box>
|
|
492
|
-
<DetailPane ui={ui} title={title} subtitle={subtitle}>
|
|
493
|
-
{detailBody}
|
|
494
|
-
{note ? (
|
|
495
|
-
<text fg={tint("pink", ui.depth)} wrapMode="none" truncate>
|
|
496
|
-
{note}
|
|
497
|
-
</text>
|
|
498
|
-
) : null}
|
|
499
|
-
{mode === "filter" ? (
|
|
500
|
-
<text attributes={TextAttributes.DIM} wrapMode="none" truncate>{`filter: ${filter}_`}</text>
|
|
501
|
-
) : filter ? (
|
|
502
|
-
<text attributes={TextAttributes.DIM} wrapMode="none" truncate>{`filter: ${filter}`}</text>
|
|
503
|
-
) : null}
|
|
504
|
-
{errors.map((e) => (
|
|
505
|
-
<text key={e} fg={tint("pink", ui.depth)} wrapMode="none" truncate>
|
|
506
|
-
{e}
|
|
507
|
-
</text>
|
|
508
|
-
))}
|
|
509
|
-
</DetailPane>
|
|
510
|
-
</box>
|
|
511
|
-
);
|
|
512
|
-
}
|
package/src/tui/profiles.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { existsSync, rmSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { readJsonFile } from "../files.ts";
|
|
5
|
-
import { configDir } from "../paths.ts";
|
|
6
|
-
import { saveProfileAndAgents } from "../profile/agents.ts";
|
|
7
|
-
import { activeProfileName, setActiveProfile } from "../profile/profile.ts";
|
|
8
|
-
import type { Catalog, Profile } from "../types.ts";
|
|
9
|
-
|
|
10
|
-
// `saveTreatLike` lives in `src/routing/catalog.ts` (OVERRIDES cross-plan fix 5); this module
|
|
11
|
-
// adds the writers INTERFACES has none for: profile delete, name validation, and save-and-activate.
|
|
12
|
-
const ProjectsSchema = z.record(z.string(), z.string());
|
|
13
|
-
|
|
14
|
-
export function deleteProfile(name: string): void {
|
|
15
|
-
if (name === activeProfileName()) {
|
|
16
|
-
throw new Error(
|
|
17
|
-
`"${name}" is the active profile; save another profile first, which makes that one active`,
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
const projectsFile = join(configDir(), "projects.json");
|
|
21
|
-
const bound = Object.entries(existsSync(projectsFile) ? readJsonFile(ProjectsSchema, projectsFile) : {})
|
|
22
|
-
.filter(([, profile]) => profile === name)
|
|
23
|
-
.map(([repo]) => repo);
|
|
24
|
-
if (bound.length > 0) throw new Error(`"${name}" is bound to ${bound.join(", ")}`);
|
|
25
|
-
rmSync(join(configDir(), "profiles", `${name}.json`), { force: true });
|
|
26
|
-
rmSync(join(configDir(), "agents", name), { recursive: true, force: true });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function nameError(name: string, taken: string[]): string | null {
|
|
30
|
-
if (!/^[a-z0-9][a-z0-9-]{0,31}$/.test(name)) return "Use lowercase letters, digits and dashes, up to 32.";
|
|
31
|
-
if (taken.includes(name)) return `"${name}" already exists.`;
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function saveAndActivate(p: Profile, c: Catalog): void {
|
|
36
|
-
saveProfileAndAgents(p, c);
|
|
37
|
-
setActiveProfile(p.name);
|
|
38
|
-
}
|