@modelstatus/cli 0.1.44 → 0.1.46
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 +1 -1
- package/src/index.js +16 -1
- package/src/tui/app.js +10 -2
- package/src/tui/game/launch.js +9 -1
- package/src/tui/views/alerts.js +13 -0
- package/src/tui/views/whatsnew.js +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelstatus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "Track which AI models you use, where, and never get surprised by a retirement. Free offline model-health for any repo (mm status), browser sign-in for cloud inventory + alerts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
package/src/index.js
CHANGED
|
@@ -98,7 +98,9 @@ function parseArgs(argv) {
|
|
|
98
98
|
const name = a.slice(2);
|
|
99
99
|
if (valueFlags.has(name)) flags[name] = argv[++i];
|
|
100
100
|
else flags[name] = true;
|
|
101
|
-
} else
|
|
101
|
+
} else if (a === "-h") flags.help = true;
|
|
102
|
+
else if (a === "-v") flags.version = true;
|
|
103
|
+
else positional.push(a);
|
|
102
104
|
}
|
|
103
105
|
if (flags.ci) {
|
|
104
106
|
flags.yes = true;
|
|
@@ -556,6 +558,12 @@ function cmdIntegrations(positional, flags) {
|
|
|
556
558
|
const id = positional[2];
|
|
557
559
|
const known = (x) => INTEGRATION_IDS.includes(x);
|
|
558
560
|
|
|
561
|
+
// Missing id → a clear usage line (not `Unknown integration "undefined"`).
|
|
562
|
+
if ((sub === "enable" || sub === "disable" || sub === "env") && !id) {
|
|
563
|
+
console.error(`Usage: mm integrations ${sub} <id>${sub === "env" ? " <prod|staging|dev|unknown>" : ""} (id: ${INTEGRATION_IDS.join(", ")})`);
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
|
|
559
567
|
if (sub === "enable" || sub === "disable") {
|
|
560
568
|
if (!known(id)) {
|
|
561
569
|
console.error(`Unknown integration "${id}". One of: ${INTEGRATION_IDS.join(", ")}`);
|
|
@@ -730,6 +738,13 @@ async function main() {
|
|
|
730
738
|
return;
|
|
731
739
|
}
|
|
732
740
|
|
|
741
|
+
// --help / -h / help: print usage + exit. MUST come before the no-arg → TUI
|
|
742
|
+
// fallthrough below (a bare `mm` launches the TUI, but `mm --help` must not).
|
|
743
|
+
if (cmd === "help" || flags.help || flags.h) {
|
|
744
|
+
console.log(HELP);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
733
748
|
// Anonymous, opt-out usage analytics (one-time disclosure, then a single
|
|
734
749
|
// event per invocation). No-op without a baked key / when opted out.
|
|
735
750
|
maybeAnalyticsNotice();
|
package/src/tui/app.js
CHANGED
|
@@ -86,6 +86,11 @@ export function App({ apiBase, apiKey, dir, initialView, onSignedIn, fresh }) {
|
|
|
86
86
|
const [prompt, setPrompt] = React.useState(null);
|
|
87
87
|
const [capturing, setCapturing] = React.useState(false); // a view is capturing text (e.g. / search)
|
|
88
88
|
const [status, setStatus] = React.useState(null); // { forKey, counts, context }
|
|
89
|
+
// A view can publish a CONTEXTUAL keybar for its current sub-state (a tab, a
|
|
90
|
+
// drill-in) via ui.setKeys(); it overrides the static meta.keys until the view
|
|
91
|
+
// changes (reset below). Keeps the keybar honest for tabbed views (What's New,
|
|
92
|
+
// Alerts) where the active keys depend on which sub-tab you're on.
|
|
93
|
+
const [dynKeys, setDynKeys] = React.useState(null);
|
|
89
94
|
const me = useAsync(async () => (apiKey ? client.me() : null), [apiKey]);
|
|
90
95
|
|
|
91
96
|
const { outer, termRows } = useTermDims();
|
|
@@ -97,6 +102,9 @@ export function App({ apiBase, apiKey, dir, initialView, onSignedIn, fresh }) {
|
|
|
97
102
|
curKeyRef.current = current.key;
|
|
98
103
|
// Anonymous: which tab is being viewed (no content, just the view name).
|
|
99
104
|
React.useEffect(() => { track("tui_view", { view: current.key }); }, [current.key]);
|
|
105
|
+
// Drop any contextual keybar when the top-level view changes (the new view
|
|
106
|
+
// re-publishes its own if it wants one).
|
|
107
|
+
React.useEffect(() => { setDynKeys(null); }, [idx]);
|
|
100
108
|
|
|
101
109
|
const showToast = React.useCallback((msg, color = "#16a34a") => {
|
|
102
110
|
setToast({ msg, color });
|
|
@@ -168,7 +176,7 @@ export function App({ apiBase, apiKey, dir, initialView, onSignedIn, fresh }) {
|
|
|
168
176
|
{ isActive: !!prompt },
|
|
169
177
|
);
|
|
170
178
|
|
|
171
|
-
const ui = { showToast, askPrompt, reportStatus, setCapturing, setHandlesBack, switchTo: (k) => setIdx(VIEWS.findIndex((v) => v.key === k)) };
|
|
179
|
+
const ui = { showToast, askPrompt, reportStatus, setCapturing, setHandlesBack, setKeys: setDynKeys, switchTo: (k) => setIdx(VIEWS.findIndex((v) => v.key === k)) };
|
|
172
180
|
const current2 = current;
|
|
173
181
|
const View = current2.Comp;
|
|
174
182
|
const account = me.data?.account ?? null;
|
|
@@ -195,7 +203,7 @@ export function App({ apiBase, apiKey, dir, initialView, onSignedIn, fresh }) {
|
|
|
195
203
|
keys = GATE_KEYS;
|
|
196
204
|
} else {
|
|
197
205
|
body = h(View, { client, me: account, refreshMe: me.reload, dir, apiBase, ui, width: W, height: bodyRows, active: !prompt, fresh });
|
|
198
|
-
keys = (current2.meta && current2.meta.keys) || [];
|
|
206
|
+
keys = dynKeys || (current2.meta && current2.meta.keys) || [];
|
|
199
207
|
}
|
|
200
208
|
|
|
201
209
|
// Status bar segments.
|
package/src/tui/game/launch.js
CHANGED
|
@@ -73,7 +73,15 @@ export async function playGameInTui({ dir, width, height, initialView = "scan",
|
|
|
73
73
|
try { if (cacheFile) fs.unlinkSync(cacheFile); } catch { /* ignore */ }
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// (4)
|
|
76
|
+
// (4) Reset the terminal to a clean full screen BEFORE remounting. The game
|
|
77
|
+
// left the alternate screen (\x1b[?1049l), which restores the PRE-game main
|
|
78
|
+
// buffer + cursor — i.e. wherever Ink sat when it unmounted (below the old
|
|
79
|
+
// frame). Ink renders its first frame from that cursor, so without this the
|
|
80
|
+
// TUI comes back shifted down / not full-height until the next resize. Reset
|
|
81
|
+
// the scroll region (\x1b[r), clear the screen, and home the cursor so the
|
|
82
|
+
// remounted tree fills the whole terminal from the top (same as Ctrl-L).
|
|
83
|
+
try { process.stdout.write("\x1b[r\x1b[2J\x1b[3J\x1b[H"); } catch { /* ignore */ }
|
|
84
|
+
await new Promise((r) => setImmediate(r)); // let the reset flush before Ink mounts
|
|
77
85
|
appController.remount({ initialView, fresh: false });
|
|
78
86
|
} catch (e) {
|
|
79
87
|
if (onError) onError(e); else throw e;
|
package/src/tui/views/alerts.js
CHANGED
|
@@ -23,10 +23,19 @@ const KIND_COLOR = {
|
|
|
23
23
|
webhook: C.FG_DIM,
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
// Per-section keybars — Rules and Channels have different actions, so the keybar
|
|
27
|
+
// follows the active sub-tab (published via ui.setKeys) rather than advertising
|
|
28
|
+
// Rules keys while you're on Channels.
|
|
29
|
+
const ALERTS_TAB_KEYS = [
|
|
30
|
+
[{ k: "←→", label: "section" }, { k: "↑↓", label: "nav" }, { k: "n", label: "new rule" }, { k: "space", label: "enable" }, { k: "c", label: "cadence" }, { k: "d", label: "delete" }, { k: "g", label: "refresh" }],
|
|
31
|
+
[{ k: "←→", label: "section" }, { k: "↑↓", label: "nav" }, { k: "n", label: "add channel" }, { k: "t", label: "test" }, { k: "g", label: "refresh" }],
|
|
32
|
+
];
|
|
33
|
+
|
|
26
34
|
export function AlertsView({ client, ui, active, width = 78, height = 14 }) {
|
|
27
35
|
const ROWS = Math.max(3, height - 4); // subtabs + blank + footer overhead
|
|
28
36
|
const [tab, setTab] = React.useState(0); // 0 rules, 1 channels
|
|
29
37
|
const [cursor, setCursor] = React.useState(0);
|
|
38
|
+
React.useEffect(() => { ui?.setKeys?.(ALERTS_TAB_KEYS[tab]); }, [tab, ui]);
|
|
30
39
|
const rules = useAsync(async () => (await client.listRules()).data || [], []);
|
|
31
40
|
const channels = useAsync(async () => (await client.listChannels()).data || [], []);
|
|
32
41
|
|
|
@@ -36,6 +45,7 @@ export function AlertsView({ client, ui, active, width = 78, height = 14 }) {
|
|
|
36
45
|
const cur = list[clampCursor(cursor, list.length)] || null;
|
|
37
46
|
|
|
38
47
|
const loading = tab === 0 ? rules.loading : channels.loading;
|
|
48
|
+
const err = tab === 0 ? rules.error : channels.error;
|
|
39
49
|
const tick = useTick(80, loading);
|
|
40
50
|
|
|
41
51
|
React.useEffect(
|
|
@@ -133,6 +143,9 @@ export function AlertsView({ client, ui, active, width = 78, height = 14 }) {
|
|
|
133
143
|
let body;
|
|
134
144
|
if (loading) {
|
|
135
145
|
body = h(StateLine, { kind: "loading", spin: SPINNER[tick % SPINNER.length], text: tab === 0 ? "loading rules…" : "loading channels…" });
|
|
146
|
+
} else if (err) {
|
|
147
|
+
// Don't let a network/API error masquerade as "no rules" (a false empty).
|
|
148
|
+
body = h(StateLine, { kind: "error", text: `couldn't load ${tab === 0 ? "rules" : "channels"} — ${err}. Press g to retry.` });
|
|
136
149
|
} else if (tab === 0) {
|
|
137
150
|
if (!ruleList.length) {
|
|
138
151
|
body = h(EmptyCard, { title: "No alert rules yet", lines: ["Stay ahead of your model timeline — a heads-up 90, 30, 7, and 1 day before anything you use is deprecated or retired.", "Press n to set the sensible default (your models · in-app + email · those lead times)."], width });
|
|
@@ -22,10 +22,20 @@ export const meta = {
|
|
|
22
22
|
],
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
// Per-section keybars — the active actions differ by tab (Registry: mark seen ·
|
|
26
|
+
// Notifications: mark read · Drift: rescan + archive). Published via ui.setKeys()
|
|
27
|
+
// so the keybar never advertises a key that's dead on the current section.
|
|
28
|
+
const TAB_KEYS = [
|
|
29
|
+
[{ k: "←→", label: "section" }, { k: "↑↓", label: "scroll" }, { k: "m", label: "mark all seen" }],
|
|
30
|
+
[{ k: "←→", label: "section" }, { k: "↑↓", label: "scroll" }, { k: "o", label: "mark read" }],
|
|
31
|
+
[{ k: "←→", label: "section" }, { k: "↑↓", label: "scroll" }, { k: "r", label: "rescan" }, { k: "a", label: "archive" }],
|
|
32
|
+
];
|
|
33
|
+
|
|
25
34
|
export function WhatsNewView({ client, dir, ui, active, width = 78, height = 14 }) {
|
|
26
35
|
const [tab, setTab] = React.useState(0);
|
|
27
36
|
const [cursor, setCursor] = React.useState(0);
|
|
28
37
|
const lastSeen = loadConfig().lastEventsSeenAt || null;
|
|
38
|
+
React.useEffect(() => { ui?.setKeys?.(TAB_KEYS[tab]); }, [tab, ui]);
|
|
29
39
|
|
|
30
40
|
const reg = useAsync(async () => {
|
|
31
41
|
const [ev, m, p] = await Promise.all([
|