@modelstatus/cli 0.1.44 → 0.1.45

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelstatus/cli",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
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 positional.push(a);
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.
@@ -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([