@modelstatus/cli 0.1.74 → 0.1.76

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 CHANGED
@@ -8,16 +8,19 @@ The free CLI + TUI for [LLM Status](https://llmstatus.ai) — scans your repo fo
8
8
  npx @modelstatus/cli status
9
9
  ```
10
10
 
11
- That's it. No sign-in, no account, no telemetryjust a snapshot of every model in your repo plus health badges and replacement suggestions.
11
+ That's it. No sign-in, no account, and the scan runs entirely on your machine you get a snapshot of every model in your repo plus health badges and replacement suggestions. (The CLI sends anonymous usage analytics — event names + counts only, never code, model names, or paths; a one-time notice says so and `MM_NO_ANALYTICS=1` turns it off.)
12
12
 
13
13
  ## Install
14
14
 
15
15
  Pick whichever fits your stack:
16
16
 
17
17
  ```bash
18
- # Self-contained binary, no Node required:
18
+ # Self-contained binary, no Node required (sha256 + signature verified by the installer):
19
19
  curl -fsSL https://llmstatus.ai/install.sh | bash
20
20
 
21
+ # Homebrew (macOS + Linux):
22
+ brew install randomartifact/tap/modelstatus-cli
23
+
21
24
  # Via npm (needs Node ≥18):
22
25
  npm i -g @modelstatus/cli
23
26
  ```
@@ -49,6 +52,19 @@ Models in use:
49
52
 
50
53
  Works **fully offline** after the first run (cached snapshot at `~/.config/llmstatus/registry-cache.json`).
51
54
 
55
+ ### Free: fix the dying ones
56
+
57
+ ```bash
58
+ mm fix [dir] --dry-run # preview the rewrites
59
+ mm fix [dir] # apply (asks first)
60
+ ```
61
+
62
+ Rewrites deprecated/retiring model ids to their current registry replacement, in
63
+ place. Boundary-safe (`gpt-4` never rewrites inside `gpt-4o`), style-preserving
64
+ (prefixed stays prefixed), and chain-aware — if the replacement is itself dying,
65
+ it follows the chain to the first live model. In the TUI, press `f` for a
66
+ red/green diff preview; nothing is written until you confirm.
67
+
52
68
  ### Sign in for cloud features
53
69
 
54
70
  ```bash
@@ -65,7 +81,9 @@ You get two binaries — `mm` (short) and `llmstatus` (descriptive). Same binary
65
81
  | Command | What it does |
66
82
  |---|---|
67
83
  | `mm status [dir]` | Free offline model-health check — no account |
68
- | `mm` | Launch the TUI (inventory, scan, what's-new, alerts, account) |
84
+ | `mm fix [dir]` | Rewrite dying model ids to their replacement (`--dry-run` to preview) |
85
+ | `mm [dir]` | Launch the TUI on a folder (defaults to the current one) — runs locally |
86
+ | `mm update` | Update the binary in place (Homebrew installs: `brew upgrade`) |
69
87
  | `mm login [api_key]` | Browser sign-in with polling (or paste a key) |
70
88
  | `mm signup` | Create an account in the browser |
71
89
  | `mm scan [dir]` | Scan for model usage; interactive TUI, or `--ci`/`--json` for pipelines |
@@ -96,7 +114,9 @@ Secret sources shell out to your already-authenticated CLIs, run **read-only**,
96
114
  | Signed registry snapshot, offline cache | ✓ | ✓ |
97
115
  | Resolve + health locally, on-device | ✓ | ✓ |
98
116
  | Secret-source aware (`env`, `aws-secrets`, `k8s`, `helm`, `sql`) | ✓ | ✓ |
117
+ | `mm fix` — rewrite dying ids to replacements | ✓ | ✓ |
99
118
  | Cloud inventory across projects/teams | — | ✓ |
119
+ | GitHub App: PR checks + one-click fix PRs | — | ✓ |
100
120
  | Alerts on deprecations/retirements (email/Slack/SMS) | — | ✓ |
101
121
  | CI integrations + web dashboard | — | ✓ |
102
122
 
@@ -113,6 +133,11 @@ pinned root key (in the CLI binary)
113
133
 
114
134
  The CLI verifies every byte before trusting the snapshot, refuses any rollback to an older version, and falls back to its local cache when the network's down. The signing key can be rotated without shipping a new CLI release.
115
135
 
136
+ The binaries get the same treatment: macOS builds are Developer ID signed and
137
+ Apple-notarized, every release manifest is Ed25519-signed and verified — against
138
+ a public key embedded in the installer and the self-updater, not fetched from the
139
+ CDN — and both refuse to proceed if any check fails.
140
+
116
141
  ## Links
117
142
 
118
143
  - Website: [llmstatus.ai](https://llmstatus.ai)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelstatus/cli",
3
- "version": "0.1.74",
3
+ "version": "0.1.76",
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
@@ -373,9 +373,11 @@ async function ciReport(dir, flags, res) {
373
373
  const uniq = [...new Set(res.candidates.map((c) => c.model_string))];
374
374
  const resolved = uniq.length ? (await client.resolve(uniq)).data || [] : [];
375
375
  const byStr = new Map(resolved.map((r) => [r.input.toLowerCase(), r]));
376
+ const { dropResolvedFragments } = await import("./registry/local.js");
377
+ const uploadable = dropResolvedFragments(res.candidates, (c) => !!byStr.get(c.model_string.toLowerCase())?.model_id);
376
378
  const seen = new Set();
377
379
  const usages = [];
378
- for (const c of res.candidates) {
380
+ for (const c of uploadable) {
379
381
  const r = byStr.get(c.model_string.toLowerCase());
380
382
  const k = `${r?.model_id ?? "custom:" + c.model_string}|${c.location_label}`;
381
383
  if (seen.has(k)) continue;
@@ -685,12 +687,14 @@ function cmdIntegrations(positional, flags) {
685
687
  async function cmdStatus(positional, flags) {
686
688
  const dir = path.resolve(positional[1] || flags.dir || ".");
687
689
  const { getRegistry } = await import("./registry/fetch.js");
688
- const { resolveLocal, computeHealth } = await import("./registry/local.js");
690
+ const { resolveLocal, computeHealth, dropResolvedFragments } = await import("./registry/local.js");
689
691
  const snapshot = await getRegistry({ offline: !!flags.offline, log: (m) => process.stderr.write(`! ${m}\n`) });
690
692
 
691
- const candidates = await collectFrom(parseSources(flags), scanOpts(flags, dir), snapshot.detection, explicitSources(flags));
693
+ let candidates = await collectFrom(parseSources(flags), scanOpts(flags, dir), snapshot.detection, explicitSources(flags));
692
694
  const resolved = resolveLocal(snapshot, [...new Set(candidates.map((c) => c.model_string))]);
693
695
  const byStr = new Map(resolved.map((r) => [r.input.toLowerCase(), r]));
696
+ // Suppress detector fragments of resolved aliases (see dropResolvedFragments).
697
+ candidates = dropResolvedFragments(candidates, (c) => !!byStr.get(c.model_string.toLowerCase())?.model_slug);
694
698
 
695
699
  // Aggregate locations per known model / per custom string.
696
700
  const known = new Map(); // slug -> { model, count }
@@ -51,3 +51,34 @@ export function needsAttention(snapshot, retiringWindowDays = 90, today = new Da
51
51
  .filter((m) => m.health !== "ok")
52
52
  .sort((a, b) => String(a.retires_date || "9999-99-99").localeCompare(String(b.retires_date || "9999-99-99")));
53
53
  }
54
+
55
+ /**
56
+ * Drop unresolved (custom) candidates that NEST with a resolved match at the
57
+ * same location (either direction of containment). The detector often emits
58
+ * overlapping spans for one id — e.g. from the Bedrock ARN
59
+ * "anthropic.claude-3-sonnet-20240229-v1:0" it emits both
60
+ * "claude-3-sonnet-20240229" (resolves → claude-3-sonnet) and the longer
61
+ * fragment "claude-3-sonnet-20240229-v1" (doesn't resolve) — which would
62
+ * double-count the line as a known model AND a phantom custom id. Both
63
+ * directions are intentional: for variants like "ft:gpt-4:acme", the resolved
64
+ * base model carries the lifecycle signal (a fine-tune dies with its base), so
65
+ * the custom row is noise there too. Customs that share a line with a resolved
66
+ * model WITHOUT string overlap (two different ids on one line) are kept.
67
+ */
68
+ export function dropResolvedFragments(candidates, isResolved) {
69
+ const locOf = (c) => (c.source_path || c.location_label || "") + ":" + (c.source_line || "");
70
+ const resolvedByLoc = new Map(); // loc → [lowercased resolved strings]
71
+ for (const c of candidates) {
72
+ if (!isResolved(c)) continue;
73
+ const k = locOf(c);
74
+ if (!resolvedByLoc.has(k)) resolvedByLoc.set(k, []);
75
+ resolvedByLoc.get(k).push(c.model_string.toLowerCase());
76
+ }
77
+ return candidates.filter((c) => {
78
+ if (isResolved(c)) return true;
79
+ const here = resolvedByLoc.get(locOf(c));
80
+ if (!here) return true;
81
+ const s = c.model_string.toLowerCase();
82
+ return !here.some((rs) => rs !== s && (rs.includes(s) || s.includes(rs)));
83
+ });
84
+ }
@@ -11,7 +11,7 @@ import fs from "node:fs";
11
11
  import os from "node:os";
12
12
  import path from "node:path";
13
13
  import { getRegistry } from "../registry/fetch.js";
14
- import { resolveLocal, computeHealth } from "../registry/local.js";
14
+ import { resolveLocal, computeHealth, dropResolvedFragments } from "../registry/local.js";
15
15
  import { scanFilesystemStreaming } from "../sources/filesystem.js";
16
16
  import { compilePatterns } from "../detect/core.js";
17
17
 
@@ -76,6 +76,9 @@ export function loadRegistry(log = () => {}) {
76
76
  export function summarize(snapshot, candidates) {
77
77
  const resolved = resolveLocal(snapshot, [...new Set(candidates.map((c) => c.model_string))]);
78
78
  const byStr = new Map(resolved.map((r) => [r.input.toLowerCase(), r]));
79
+ // A custom id that's a fragment of a resolved alias on the same line is a
80
+ // detector artifact, not a finding (e.g. the inner piece of a Bedrock ARN).
81
+ candidates = dropResolvedFragments(candidates, (c) => !!byStr.get(c.model_string.toLowerCase())?.model_slug);
79
82
  const known = new Map(); // slug → { model, count }
80
83
  const custom = new Map(); // string → count
81
84
  const refsBySlug = new Map(); // slug → [candidate]