@modelstatus/cli 0.1.42 → 0.1.44

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.42",
3
+ "version": "0.1.44",
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",
@@ -60,14 +60,18 @@ export const SOURCE_META = {
60
60
  env: { label: "Env var", badge: "env", glyph: "=", color: C.FG_DIM },
61
61
  vercel: { label: "Vercel env", badge: "vercel", glyph: GLYPH.retiring, color: C.FG_STRONG },
62
62
  "supabase-edge": { label: "Supabase Edge", badge: "supabase", glyph: "~", color: "#3ecf8e" },
63
+ // DISTINCT single-width glyph per source so the tight 1-char column is never
64
+ // ambiguous (the AWS family used to all show "λ" and k8s/helm both "⎈"). All
65
+ // glyphs + ascii are single-width + pairwise-distinct; the badge stays the
66
+ // readable wide form. (Glyphs are CLI-only; web parity is keys/labels/badges.)
63
67
  "aws-lambda": { label: "AWS Lambda", badge: "aws", glyph: "λ", ascii: "L", color: "#ff9900" },
64
- "aws-bedrock": { label: "AWS Bedrock", badge: "bedrock", glyph: "λ", ascii: "L", color: "#ff9900" },
65
- "aws-secrets": { label: "AWS Secrets", badge: "secrets", glyph: "λ", ascii: "L", color: "#ff9900" },
66
- "aws-ssm": { label: "AWS SSM", badge: "ssm", glyph: "λ", ascii: "L", color: "#ff9900" },
67
- "github-actions": { label: "GitHub Actions", badge: "github", glyph: "⎇", ascii: "gh", color: "#a78bfa" },
68
- k8s: { label: "Kubernetes", badge: "k8s", glyph: "⎈", ascii: "k8s", color: "#326ce5" },
69
- helm: { label: "Helm", badge: "helm", glyph: "", ascii: "helm", color: "#0f6fd1" },
70
- sql: { label: "SQL/config", badge: "sql", glyph: "▦", ascii: "sql", color: C.FG_DIM },
68
+ "aws-bedrock": { label: "AWS Bedrock", badge: "bedrock", glyph: "β", ascii: "B", color: "#ff9900" },
69
+ "aws-secrets": { label: "AWS Secrets", badge: "secrets", glyph: "*", ascii: "*", color: "#ff9900" },
70
+ "aws-ssm": { label: "AWS SSM", badge: "ssm", glyph: "", ascii: "P", color: "#ff9900" },
71
+ "github-actions": { label: "GitHub Actions", badge: "github", glyph: "⎇", ascii: "g", color: "#a78bfa" },
72
+ k8s: { label: "Kubernetes", badge: "k8s", glyph: "⎈", ascii: "K", color: "#326ce5" },
73
+ helm: { label: "Helm", badge: "helm", glyph: "", ascii: "H", color: "#0f6fd1" },
74
+ sql: { label: "SQL/config", badge: "sql", glyph: "▦", ascii: "Q", color: C.FG_DIM },
71
75
  manual: { label: "Manual", badge: "manual", glyph: GLYPH.repl === "->" ? "+" : "✎", ascii: "+", color: C.FG_FAINT },
72
76
  };
73
77
 
@@ -6,8 +6,8 @@ import React from "react";
6
6
  import path from "node:path";
7
7
  import { Box, Text, useInput } from "ink";
8
8
  import {
9
- h, C, GLYPH, ListRow, StateLine, EmptyCard, healthColor, healthGlyph,
10
- relativeTime, envTag, cell, cellE, snippetLines, SPINNER, useTick, useAsync, clampCursor,
9
+ h, C, GLYPH, ListRow, StateLine, EmptyCard, SearchBar, healthColor, healthGlyph,
10
+ relativeTime, envTag, cell, cellE, snippetLines, SPINNER, useTick, useAsync, useSearch, clampCursor,
11
11
  } from "../ui.js";
12
12
  import { readSnippet, findSourceFile } from "../snippet.js";
13
13
  import { sourceOf, SOURCE_META, sourceGlyph } from "../source-meta.js";
@@ -17,6 +17,7 @@ const ENV_ORDER = ["prod", "staging", "dev", "unknown"];
17
17
  export const meta = {
18
18
  keys: [
19
19
  { k: "↑↓", label: "nav" },
20
+ { k: "/", label: "search" },
20
21
  { k: "e", label: "env" },
21
22
  { k: "t", label: "tag untagged" },
22
23
  { k: "c", label: "critical" },
@@ -44,7 +45,27 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
44
45
  const usages = q.data?.usages || [];
45
46
  const projects = q.data?.projects || [];
46
47
  const projName = (id) => projects.find((p) => p.id === id)?.name || "—";
47
- const cur = usages[clampCursor(cursor, usages.length)] || null;
48
+
49
+ // Filter (press / ) — matches the model, the SOURCE (badge + label + scheme),
50
+ // the project + env. So "vercel"/"supabase"/"aws"/"github"/"repo"/"manual"
51
+ // search BY SOURCE, while "gpt" / a project name still work. Reuses the Scan
52
+ // tab's search hook so the / + esc + type flow is identical everywhere.
53
+ const search = useSearch();
54
+ const query = search.query.trim().toLowerCase();
55
+ const haystack = (u) => {
56
+ const st = sourceOf(u.location_label, u.discovered_by);
57
+ const m = SOURCE_META[st] || {};
58
+ return [u.model_display, u.custom_model_name, u.canonical_id, m.badge, m.label, st, projName(u.project_id), u.environment]
59
+ .map((x) => String(x || "").toLowerCase())
60
+ .join(" ");
61
+ };
62
+ const filtered = query ? usages.filter((u) => haystack(u).includes(query)) : usages;
63
+ const cur = filtered[clampCursor(cursor, filtered.length)] || null;
64
+
65
+ // Keep the cursor in range as the filter narrows; let the shell route backspace
66
+ // to "clear filter" (not "previous tab") while a query is active.
67
+ React.useEffect(() => { setCursor(0); }, [query]);
68
+ React.useEffect(() => { ui?.setHandlesBack?.(!!search.query); }, [search.query, ui]);
48
69
 
49
70
  // Drill-down code preview for the highlighted usage's source location — same
50
71
  // syntax-highlighted snippet as the Here tab, shown below the list (the side
@@ -67,8 +88,8 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
67
88
  React.useEffect(() => {
68
89
  const counts = { ok: 0, deprecating: 0, retiring: 0, retired: 0, custom: 0 };
69
90
  for (const u of usages) if (counts[u.health] != null) counts[u.health] += 1;
70
- ui?.reportStatus?.({ counts, context: `${usages.length} tracked` });
71
- }, [usages, ui]);
91
+ ui?.reportStatus?.({ counts, context: query ? `${filtered.length} of ${usages.length}` : `${usages.length} tracked` });
92
+ }, [usages, ui, query, filtered.length]);
72
93
 
73
94
  async function patch(u, body, label) {
74
95
  try {
@@ -83,8 +104,18 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
83
104
  useInput(
84
105
  (input, key) => {
85
106
  if (!active) return;
86
- if (key.downArrow || input === "j") return setCursor((c) => clampCursor(c + 1, usages.length));
87
- if (key.upArrow || input === "k") return setCursor((c) => clampCursor(c - 1, usages.length));
107
+ // --- filter mode: type to search (matches model + SOURCE + project + env) ---
108
+ if (search.isSearchingNow()) {
109
+ if (key.escape) { search.clear(); setCursor(0); ui?.setCapturing?.(false); return; }
110
+ if (key.return) { search.confirm(); ui?.setCapturing?.(false); return; }
111
+ if (key.backspace || key.delete) return search.backspace();
112
+ if (input && !key.ctrl && !key.meta) return search.type(input);
113
+ return;
114
+ }
115
+ if (input === "/") { search.open(); ui?.setCapturing?.(true); return; }
116
+ if ((key.escape || key.backspace || key.delete) && search.query) { search.clear(); setCursor(0); return; }
117
+ if (key.downArrow || input === "j") return setCursor((c) => clampCursor(c + 1, filtered.length));
118
+ if (key.upArrow || input === "k") return setCursor((c) => clampCursor(c - 1, filtered.length));
88
119
  if (input === "g") return q.reload();
89
120
  if (input === "r") return ui.switchTo("scan");
90
121
  if (input === "n") return ui.switchTo("add");
@@ -135,7 +166,7 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
135
166
  .deleteUsage(cur.id)
136
167
  .then(() => {
137
168
  ui.showToast("deleted");
138
- setCursor((c) => clampCursor(c, usages.length - 1));
169
+ setCursor((c) => clampCursor(c, filtered.length - 1));
139
170
  q.reload();
140
171
  })
141
172
  .catch((e) => ui.showToast(e.message, "red"));
@@ -184,10 +215,11 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
184
215
  const showNote = !showSnip && !!srcRef && height >= 14; // has a path but no local file
185
216
  const reserve = showSnip ? SNIP : showNote ? 2 : 0;
186
217
  const topH = height - reserve;
187
- const VISIBLE = Math.max(3, topH - 2); // header + "…and N more" overhead
188
- const c = clampCursor(cursor, usages.length);
189
- const start = Math.max(0, Math.min(c - Math.floor(VISIBLE / 2), Math.max(0, usages.length - VISIBLE)));
190
- const view = usages.slice(start, start + VISIBLE);
218
+ const searchRow = search.active ? 1 : 0; // the / search bar costs a row when shown
219
+ const VISIBLE = Math.max(3, topH - 2 - searchRow); // header + "…and N more" overhead
220
+ const c = clampCursor(cursor, filtered.length);
221
+ const start = Math.max(0, Math.min(c - Math.floor(VISIBLE / 2), Math.max(0, filtered.length - VISIBLE)));
222
+ const view = filtered.slice(start, start + VISIBLE);
191
223
 
192
224
  const header = h(
193
225
  Text,
@@ -215,13 +247,15 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
215
247
  return h(ListRow, { key: u.id, active: isCur, cells, width: leftWidth });
216
248
  });
217
249
 
250
+ const noMatches = query && filtered.length === 0;
218
251
  const list = h(
219
252
  Box,
220
253
  { flexDirection: "column", width: leftWidth },
254
+ search.active ? h(SearchBar, { searching: search.searching, query: search.query, count: filtered.length }) : null,
221
255
  header,
222
- ...rowNodes,
223
- usages.length > VISIBLE
224
- ? h(Text, { color: C.FG_FAINT }, ` ${start + 1}-${Math.min(start + VISIBLE, usages.length)} of ${usages.length}`)
256
+ ...(noMatches ? [h(Text, { color: C.FG_DIM }, ` no matches for "${search.query}"`)] : rowNodes),
257
+ filtered.length > VISIBLE
258
+ ? h(Text, { color: C.FG_FAINT }, ` ${start + 1}-${Math.min(start + VISIBLE, filtered.length)} of ${filtered.length}`)
225
259
  : null,
226
260
  );
227
261