@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 +28 -3
- package/package.json +1 -1
- package/src/index.js +7 -3
- package/src/registry/local.js +31 -0
- package/src/tui/scan-stream.js +4 -1
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,
|
|
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` |
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 }
|
package/src/registry/local.js
CHANGED
|
@@ -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
|
+
}
|
package/src/tui/scan-stream.js
CHANGED
|
@@ -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]
|