@modelstatus/cli 0.1.83 → 0.1.84
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 +19 -0
- package/package.json +1 -1
- package/src/changelog-data.js +11 -0
- package/src/detect/core.js +45 -4
- package/src/index.js +97 -24
- package/src/registry/local.js +15 -3
- package/src/sources/filesystem.js +0 -0
- package/src/sources/index.js +4 -2
package/README.md
CHANGED
|
@@ -148,6 +148,25 @@ Apple-notarized, every release manifest is Ed25519-signed and verified — again
|
|
|
148
148
|
a public key embedded in the installer and the self-updater, not fetched from the
|
|
149
149
|
CDN — and both refuse to proceed if any check fails.
|
|
150
150
|
|
|
151
|
+
### Verify a download yourself
|
|
152
|
+
|
|
153
|
+
The installer already checks everything, but you can verify any binary
|
|
154
|
+
independently with [minisign](https://jedisct1.github.io/minisign/) (Ed25519).
|
|
155
|
+
Each binary ships a detached `.minisig` next to it on the CDN:
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
# e.g. Linux x64 (use latest/ or a pinned cli/v0.1.83/)
|
|
159
|
+
curl -fLO https://cdn.llmstatus.ai/cli/latest/modelstatus-cli-linux-x64
|
|
160
|
+
curl -fLO https://cdn.llmstatus.ai/cli/latest/modelstatus-cli-linux-x64.minisig
|
|
161
|
+
minisign -Vm modelstatus-cli-linux-x64 \
|
|
162
|
+
-P RWQlKGcm5lXiIiTvGv/cdc0joBn1sm9vOFr9WqR6CI15PXzoHNHVBY6S
|
|
163
|
+
# → "Signature and comment signature verified"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The public key is also committed at `packages/cli/release-minisign.pub`. (On
|
|
167
|
+
macOS you additionally get the Developer ID signature + Apple notarization; on
|
|
168
|
+
Linux/Windows the minisign signature is the independent check.)
|
|
169
|
+
|
|
151
170
|
## Links
|
|
152
171
|
|
|
153
172
|
- 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.84",
|
|
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/changelog-data.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/* GENERATED by scripts/gen-changelog.mjs from apps/web/lib/changelog.json — do not edit.
|
|
2
2
|
* Release notes baked into the binary (in-TUI + the on-load what's-new card). */
|
|
3
3
|
export const CHANGELOG = [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.84",
|
|
6
|
+
"date": "2026-06-14",
|
|
7
|
+
"title": "Clearer mm status, faster scans, safer CI",
|
|
8
|
+
"items": [
|
|
9
|
+
"`mm status` now leads with a one-line verdict, orders models by what dies first, and shows a `≈ suggestion?` for ids written without their provider prefix — so a model the registry knows isn't just labelled \"unrecognized\".",
|
|
10
|
+
"Scans are ~9x faster on large trees, with a live file counter while they run; random config keys and generated tokens no longer get mistaken for model ids.",
|
|
11
|
+
"`mm ci --diff` no longer silently passes on a symlinked checkout — it was dropping every finding, hiding retiring models in changed files behind a green check.",
|
|
12
|
+
"`mm scan --dry-run` always previews (it never launches the uploader), `mm scan --json` always emits JSON, and running the dashboard without an interactive terminal prints a clear message instead of crashing."
|
|
13
|
+
]
|
|
14
|
+
},
|
|
4
15
|
{
|
|
5
16
|
"version": "0.1.83",
|
|
6
17
|
"date": "2026-06-14",
|
package/src/detect/core.js
CHANGED
|
@@ -50,11 +50,31 @@ function matchesAtBoundary(haystack, term) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** Reject a generic match that carries a long, high-entropy alnum run — a config
|
|
54
|
+
* key / generated id / random token (e.g. "claude-config-8f4k2m9x7p3q",
|
|
55
|
+
* "qwen1ah0aqumtapuychbwd2maghkvcltn") rather than a real model id. A model id —
|
|
56
|
+
* even a brand-new one not yet in the registry — has short, separated, mostly
|
|
57
|
+
* pronounceable segments; a generated key has one long mixed-case/digit blob.
|
|
58
|
+
* Only applied to GENERIC matches (exact registry strings bypass looksLikeModel),
|
|
59
|
+
* so it can never drop a known model. Pure-alpha segments (davinci) and pure-digit
|
|
60
|
+
* snapshot dates (20250514) are never flagged — only MIXED long segments are. */
|
|
61
|
+
function hasRandomSegment(s) {
|
|
62
|
+
for (const seg of s.split(/[-._/:]+/)) {
|
|
63
|
+
if (seg.length < 12) continue;
|
|
64
|
+
if (!/[a-z]/i.test(seg) || !/[0-9]/.test(seg)) continue; // need mixed letters+digits
|
|
65
|
+
const distinct = new Set(seg).size;
|
|
66
|
+
const vowels = (seg.match(/[aeiou]/gi) || []).length;
|
|
67
|
+
if (distinct / seg.length > 0.6 || vowels / seg.length < 0.18) return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
53
72
|
/** Family globs catch brand-NEW versioned models before they're in the
|
|
54
73
|
* registry, so a real hit virtually always carries a version digit. Requiring
|
|
55
|
-
* one (and rejecting filename/domain tails) kills the bulk of
|
|
74
|
+
* one (and rejecting filename/domain tails + random-key blobs) kills the bulk of
|
|
75
|
+
* false positives. */
|
|
56
76
|
function looksLikeModel(s) {
|
|
57
|
-
return s.length >= 5 && /[0-9]/.test(s) && !BANNED_TAIL.test(s);
|
|
77
|
+
return s.length >= 5 && /[0-9]/.test(s) && !BANNED_TAIL.test(s) && !hasRandomSegment(s);
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
// Detection strings that are ALSO everyday code/English words. Cohere's bare
|
|
@@ -78,8 +98,20 @@ export function compilePatterns(patterns) {
|
|
|
78
98
|
// (The old >=4 floor silently dropped the entire OpenAI o-series.)
|
|
79
99
|
if (ms.match && ms.match.length >= 2 && !STOPWORDS.has(ms.match.toLowerCase())) exact.push(ms.match.toLowerCase());
|
|
80
100
|
}
|
|
101
|
+
// Bucket the exact strings by their first 2 chars. detectInLine then runs the
|
|
102
|
+
// boundary matcher ONLY on buckets whose 2-char head appears in the line (a
|
|
103
|
+
// cheap indexOf prefilter). With 1500+ registry strings this is ~9x faster on a
|
|
104
|
+
// large tree and byte-identical: a string always contains its own first 2 chars,
|
|
105
|
+
// so a present string's bucket is never skipped. (~47 distinct heads today.)
|
|
106
|
+
const exactByHead = new Map();
|
|
107
|
+
for (const s of exact) {
|
|
108
|
+
const head = s.slice(0, 2);
|
|
109
|
+
let bucket = exactByHead.get(head);
|
|
110
|
+
if (!bucket) exactByHead.set(head, (bucket = []));
|
|
111
|
+
bucket.push(s);
|
|
112
|
+
}
|
|
81
113
|
const generic = (patterns.generic_model_regexes || []).map((r) => new RegExp(r, "gi"));
|
|
82
|
-
return { exact, generic };
|
|
114
|
+
return { exact, exactByHead, generic };
|
|
83
115
|
}
|
|
84
116
|
|
|
85
117
|
/** Find model strings on a single line. Returns a Set of lowercased strings. */
|
|
@@ -89,7 +121,16 @@ export function detectInLine(line, compiled) {
|
|
|
89
121
|
// Exact registry strings must match at a token boundary, not as a raw substring
|
|
90
122
|
// — otherwise a short alias ("gpt-4") resolves inside a longer id ("gpt-4o-mini")
|
|
91
123
|
// to the wrong model. Matches the server PR scanner's boundary semantics.
|
|
92
|
-
|
|
124
|
+
// Use the 2-char-head bucket index when available (only scan buckets whose head
|
|
125
|
+
// appears in the line); fall back to the flat list for any older compiled shape.
|
|
126
|
+
if (compiled.exactByHead) {
|
|
127
|
+
for (const [head, bucket] of compiled.exactByHead) {
|
|
128
|
+
if (lower.indexOf(head) < 0) continue;
|
|
129
|
+
for (const s of bucket) if (matchesAtBoundary(lower, s)) found.add(s);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
for (const s of compiled.exact) if (matchesAtBoundary(lower, s)) found.add(s);
|
|
133
|
+
}
|
|
93
134
|
for (const re of compiled.generic) {
|
|
94
135
|
re.lastIndex = 0;
|
|
95
136
|
let m;
|
package/src/index.js
CHANGED
|
@@ -244,10 +244,18 @@ async function cmdPlay(positional, flags) {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
async function launchTui(initialView, flags) {
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
247
|
+
// The Ink TUI needs raw-mode stdin. Without a TTY (piped, redirected, CI logs)
|
|
248
|
+
// Ink throws an unhandled "Raw mode is not supported" deep in a passive effect
|
|
249
|
+
// — it escapes main()'s try/catch and dumps a React/runtime stack. Guard like
|
|
250
|
+
// `mm play` does and point at the non-interactive command instead.
|
|
251
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
252
|
+
console.error("The mm dashboard needs an interactive terminal (a TTY).");
|
|
253
|
+
console.error("For a non-interactive check, run: mm status [dir] (add --json for scripts)");
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
// Pass apiKey straight through (may be null). The TUI lands on the public
|
|
257
|
+
// "Here" tab (no account needed); sign-in is lazy — the Account tab (press 7)
|
|
258
|
+
// or any auth-gated tab prompts when you get there.
|
|
251
259
|
const { apiBase, apiKey } = resolveAuth(flags);
|
|
252
260
|
const dir = path.resolve(flags.dir || ".");
|
|
253
261
|
const { runApp } = await import("./tui/app.js");
|
|
@@ -257,13 +265,15 @@ async function launchTui(initialView, flags) {
|
|
|
257
265
|
}
|
|
258
266
|
|
|
259
267
|
async function cmdScan(positional, flags) {
|
|
260
|
-
const dir = path.resolve(positional[1] || ".");
|
|
268
|
+
const dir = path.resolve(positional[1] || flags.dir || ".");
|
|
261
269
|
const { apiBase, apiKey } = resolveAuth(flags);
|
|
262
270
|
if (!apiKey) {
|
|
263
271
|
console.error("No API key. Run `mm login` or pass --key.");
|
|
264
272
|
process.exit(1);
|
|
265
273
|
}
|
|
266
|
-
|
|
274
|
+
// --dry-run must NOT launch the interactive TUI (which can upload) — it's a
|
|
275
|
+
// no-upload preview, so force the non-interactive path even on a TTY.
|
|
276
|
+
const interactive = !flags.yes && !flags.json && !flags["dry-run"] && process.stdout.isTTY;
|
|
267
277
|
if (interactive) {
|
|
268
278
|
const { runApp } = await import("./tui/app.js");
|
|
269
279
|
await runApp({ apiBase, apiKey, dir, initialView: "scan" });
|
|
@@ -290,7 +300,19 @@ async function cmdScan(positional, flags) {
|
|
|
290
300
|
const patterns = await client.detectionPatterns();
|
|
291
301
|
const candidates = await collectFrom(sources, opts, patterns, explicit);
|
|
292
302
|
if (candidates.length === 0) {
|
|
293
|
-
|
|
303
|
+
// Respect --json/--ci even on the empty path, so `mm scan --ci | jq` never
|
|
304
|
+
// chokes on a bare text line. Mirror the shape of the matching non-empty path.
|
|
305
|
+
if (flags.json) {
|
|
306
|
+
const srcRows = avail.map((a) => ({ id: a.id, available: a.available }));
|
|
307
|
+
console.log(JSON.stringify(
|
|
308
|
+
flags["dry-run"]
|
|
309
|
+
? { scanned: 0, would_upload: [], sources: avail }
|
|
310
|
+
: { scanned: 0, uploaded: 0, sources: srcRows, created: 0, updated: 0, failed: 0 },
|
|
311
|
+
null, 2,
|
|
312
|
+
));
|
|
313
|
+
} else {
|
|
314
|
+
console.log("No model usage found.");
|
|
315
|
+
}
|
|
294
316
|
return;
|
|
295
317
|
}
|
|
296
318
|
|
|
@@ -436,8 +458,18 @@ async function postCiRun(dir, flags, res, failOn) {
|
|
|
436
458
|
* annotations + a step summary under GITHUB_ACTIONS; exits non-zero per --fail-on
|
|
437
459
|
* so it gates merges. Offline (public registry) by default; --report syncs (Pro). */
|
|
438
460
|
async function cmdCi(positional, flags) {
|
|
439
|
-
|
|
461
|
+
// realpathSync so `dir` matches git's `--show-toplevel` (which resolves
|
|
462
|
+
// symlinks). Without this, on a symlinked checkout (macOS /tmp, symlinked CI
|
|
463
|
+
// workspaces) the --diff path math mismatches and ALL findings are dropped →
|
|
464
|
+
// a silent GREEN check while retired models in changed files slip through.
|
|
465
|
+
let dir = path.resolve(positional[1] || flags.dir || ".");
|
|
466
|
+
try { dir = fs.realpathSync(dir); } catch { /* keep resolved path if it doesn't exist */ }
|
|
467
|
+
const VALID_FAIL_ON = new Set(["none", "deprecating", "retiring", "retired"]);
|
|
440
468
|
const failOn = String(flags["fail-on"] || "retired").toLowerCase();
|
|
469
|
+
if (flags["fail-on"] !== undefined && !VALID_FAIL_ON.has(failOn)) {
|
|
470
|
+
console.error(`Invalid --fail-on "${flags["fail-on"]}". One of: ${[...VALID_FAIL_ON].join(", ")}.`);
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
441
473
|
const { evaluateCi, annotationLines, summaryMarkdown, filterToChangedFiles, getChangedFiles, HEALTH_RANK } = await import("./ci.js");
|
|
442
474
|
const res = await evaluateCi({
|
|
443
475
|
dir,
|
|
@@ -551,7 +583,8 @@ async function cmdFix(positional, flags) {
|
|
|
551
583
|
const snapshot = await getRegistry({ offline: !!flags.offline, log: (m) => prog.log(m) });
|
|
552
584
|
|
|
553
585
|
prog.update("scanning for model references…");
|
|
554
|
-
const
|
|
586
|
+
const onProgress = ({ filesScanned, candidates: c }) => prog.update(`scanning… ${filesScanned} files, ${c} reference(s)`);
|
|
587
|
+
const candidates = await collectFrom(["filesystem"], { root: dir }, snapshot.detection, new Set(), onProgress);
|
|
555
588
|
prog.stop();
|
|
556
589
|
const resolved = resolveLocal(snapshot, [...new Set(candidates.map((c) => c.model_string))]);
|
|
557
590
|
const byStr = new Map(resolved.map((r) => [r.input.toLowerCase(), r]));
|
|
@@ -697,11 +730,15 @@ async function cmdStatus(positional, flags) {
|
|
|
697
730
|
// A cold `mm status` downloads the ~225 kB registry snapshot then walks the
|
|
698
731
|
// repo with zero output — it reads as frozen. Show live stderr progress
|
|
699
732
|
// (auto-off for --json / pipes so machine output stays byte-identical).
|
|
733
|
+
// A cold `mm status` downloads the registry snapshot then walks the repo with
|
|
734
|
+
// zero output — it reads as frozen. Show live stderr progress with a MOVING
|
|
735
|
+
// file counter (auto-off for --json / pipes so machine output is byte-identical).
|
|
700
736
|
const prog = startProgress(!flags.json, "fetching the model registry…");
|
|
701
737
|
const snapshot = await getRegistry({ offline: !!flags.offline, log: (m) => prog.log(m) });
|
|
702
738
|
|
|
703
739
|
prog.update("scanning for model references…");
|
|
704
|
-
|
|
740
|
+
const onProgress = ({ filesScanned, candidates: c }) => prog.update(`scanning… ${filesScanned} files, ${c} reference(s)`);
|
|
741
|
+
let candidates = await collectFrom(parseSources(flags), scanOpts(flags, dir), snapshot.detection, explicitSources(flags), onProgress);
|
|
705
742
|
prog.stop();
|
|
706
743
|
const resolved = resolveLocal(snapshot, [...new Set(candidates.map((c) => c.model_string))]);
|
|
707
744
|
const byStr = new Map(resolved.map((r) => [r.input.toLowerCase(), r]));
|
|
@@ -719,22 +756,36 @@ async function cmdStatus(positional, flags) {
|
|
|
719
756
|
known.set(r.model_slug, e);
|
|
720
757
|
} else custom.set(c.model_string, (custom.get(c.model_string) || 0) + 1);
|
|
721
758
|
}
|
|
759
|
+
const fileCount = new Set(candidates.map((c) => c.source_path || c.location_label)).size;
|
|
722
760
|
|
|
723
761
|
const today = new Date();
|
|
724
762
|
const ICON = { ok: "🟢", deprecating: "🟡", retiring: "🟠", retired: "🔴", withdrawn: "⛔", custom: "⚪" };
|
|
725
|
-
|
|
763
|
+
// Order by WHAT DIES FIRST: already-dead (retired/withdrawn) first, then aging
|
|
764
|
+
// by soonest retirement date regardless of bucket (a deprecating model that
|
|
765
|
+
// retires sooner outranks a retiring one that retires later), then ok last.
|
|
766
|
+
const tier = (h) => (h === "retired" || h === "withdrawn") ? 0 : h === "ok" ? 2 : 1;
|
|
726
767
|
const rows = [...known.values()]
|
|
727
768
|
.map(({ model, count }) => ({ model, count, health: computeHealth(model, 90, today) }))
|
|
728
|
-
.sort((a, b) =>
|
|
769
|
+
.sort((a, b) => tier(a.health) - tier(b.health) || String(a.model.retires_date || "9999").localeCompare(String(b.model.retires_date || "9999")));
|
|
729
770
|
const attention = rows.filter((r) => r.health !== "ok");
|
|
771
|
+
const nBy = (h) => rows.filter((r) => r.health === h).length;
|
|
772
|
+
const nDead = nBy("retired") + nBy("withdrawn");
|
|
773
|
+
const nRetiring = nBy("retiring");
|
|
774
|
+
const nDeprecating = nBy("deprecating");
|
|
775
|
+
|
|
776
|
+
// Custom strings: surface the resolver's near-miss suggestion + sort by frequency.
|
|
777
|
+
const customList = [...custom.entries()]
|
|
778
|
+
.map(([name, places]) => ({ name, places, suggestion: byStr.get(name.toLowerCase())?.suggestion || null }))
|
|
779
|
+
.sort((a, b) => b.places - a.places || a.name.localeCompare(b.name));
|
|
730
780
|
|
|
731
781
|
if (flags.json) {
|
|
732
782
|
console.log(JSON.stringify({
|
|
733
783
|
registry: { version: snapshot.version, generated_at: snapshot.generated_at, models: snapshot.models.length },
|
|
734
784
|
scanned: dir,
|
|
735
785
|
references: candidates.length,
|
|
786
|
+
files: fileCount,
|
|
736
787
|
models: rows.map((r) => ({ slug: r.model.slug, display: r.model.display, health: r.health, retires_date: r.model.retires_date, replacement_slug: r.model.replacement_slug, places: r.count })),
|
|
737
|
-
custom:
|
|
788
|
+
custom: customList,
|
|
738
789
|
needs_attention: attention.length,
|
|
739
790
|
}, null, 2));
|
|
740
791
|
return;
|
|
@@ -742,9 +793,21 @@ async function cmdStatus(positional, flags) {
|
|
|
742
793
|
|
|
743
794
|
const ageDays = Math.round((today - new Date(snapshot.generated_at)) / 86_400_000);
|
|
744
795
|
console.log(`LLM Status — registry ${snapshot.version} (${ageDays <= 0 ? "today" : ageDays + "d old"}), ${snapshot.models.length} models`);
|
|
745
|
-
console.log(`Scanned ${dir}: ${candidates.length} reference(s) → ${known.size} model(s), ${custom.size} custom
|
|
796
|
+
console.log(`Scanned ${dir}: ${candidates.length} reference(s) in ${fileCount} file(s) → ${known.size} model(s), ${custom.size} custom`);
|
|
797
|
+
// Lead with the verdict so the result reads at a glance.
|
|
798
|
+
if (attention.length) {
|
|
799
|
+
const parts = [];
|
|
800
|
+
if (nDead) parts.push(`${nDead} retired`);
|
|
801
|
+
if (nRetiring) parts.push(`${nRetiring} retiring soon`);
|
|
802
|
+
if (nDeprecating) parts.push(`${nDeprecating} deprecating`);
|
|
803
|
+
console.log(`${attention.length} of ${known.size} model(s) need attention: ${parts.join(" · ")}`);
|
|
804
|
+
} else if (rows.length) {
|
|
805
|
+
console.log(`All ${known.size} model(s) you use are current.`);
|
|
806
|
+
}
|
|
807
|
+
console.log("");
|
|
808
|
+
|
|
746
809
|
if (!rows.length && !custom.size) {
|
|
747
|
-
console.log("No model usage found here.
|
|
810
|
+
console.log("No model usage found here. Point `mm status` at a repo, or run `mm scan` to map a whole project.");
|
|
748
811
|
return;
|
|
749
812
|
}
|
|
750
813
|
if (rows.length) {
|
|
@@ -755,15 +818,23 @@ async function cmdStatus(positional, flags) {
|
|
|
755
818
|
console.log(` ${ICON[r.health]} ${r.health.padEnd(11)} ${r.model.slug.padEnd(32)} ${tail.padEnd(20)}${repl} (${r.count})`);
|
|
756
819
|
}
|
|
757
820
|
}
|
|
758
|
-
if (
|
|
821
|
+
if (customList.length) {
|
|
759
822
|
console.log("\nCustom / unrecognized:");
|
|
760
|
-
|
|
823
|
+
const CAP = 12;
|
|
824
|
+
for (const c of customList.slice(0, CAP)) {
|
|
825
|
+
const hint = c.suggestion ? ` ≈ ${c.suggestion}?` : "";
|
|
826
|
+
console.log(` ⚪ ${c.name} (${c.places})${hint}`);
|
|
827
|
+
}
|
|
828
|
+
if (customList.length > CAP) console.log(` … +${customList.length - CAP} more (mm status --json for the full list)`);
|
|
761
829
|
}
|
|
830
|
+
// Functional next steps only (no marketing voice): `mm fix` when something's
|
|
831
|
+
// auto-rewritable; the alerts pointer only when not signed in.
|
|
762
832
|
if (attention.length) {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
833
|
+
const fixable = attention.filter((r) => r.model.replacement_slug).length;
|
|
834
|
+
const tips = [];
|
|
835
|
+
if (fixable) tips.push(`\`mm fix\` rewrites the ${fixable} with a known replacement`);
|
|
836
|
+
if (!loadConfig().apiKey) tips.push("`mm login` to get alerted before these dates (Pro)");
|
|
837
|
+
if (tips.length) console.log("\n" + tips.map((t) => "→ " + t).join("\n"));
|
|
767
838
|
}
|
|
768
839
|
}
|
|
769
840
|
|
|
@@ -779,14 +850,15 @@ Usage:
|
|
|
779
850
|
mm scan [dir] Scan for model usage; interactive TUI, or --ci/--json for pipelines
|
|
780
851
|
mm fix [dir] Rewrite dying model ids to their replacement, in place (--dry-run previews; --model <slug> limits; --yes skips the confirm)
|
|
781
852
|
mm ci [dir] CI gate: fail the build on deprecated/retiring models (GitHub annotations)
|
|
782
|
-
(--
|
|
853
|
+
(--fail-on <none|deprecating|retiring|retired> sets the threshold, default retired;
|
|
854
|
+
--json-out <file> writes findings JSON; --diff <base> limits to changed files, auto on PRs)
|
|
783
855
|
mm sources List detection sources and whether each can run here
|
|
784
856
|
mm integrations Manage live integrations (list | enable <id> | disable <id> | env <id> <tag>)
|
|
785
857
|
mm clear Delete all tracked usages from your inventory (--all also wipes projects/rules; --yes to skip the prompt)
|
|
786
858
|
mm update Update to the latest version now and relaunch (or add --update to any command)
|
|
787
859
|
mm upgrade Open Stripe checkout and poll until Pro is active (the paid plan, not the binary)
|
|
788
860
|
mm play [dir] Play Donkey Kong while a background scan walks the dir (just for fun)
|
|
789
|
-
mm tui Force-launch the TUI (
|
|
861
|
+
mm tui Force-launch the TUI (needs an interactive terminal)
|
|
790
862
|
|
|
791
863
|
Scan sources (--sources; default = filesystem + enabled integrations; "all" for everything):
|
|
792
864
|
filesystem repo files aws-secrets AWS Secrets Manager + SSM
|
|
@@ -798,7 +870,8 @@ Scan sources (--sources; default = filesystem + enabled integrations; "all" for
|
|
|
798
870
|
Secret sources shell out to your already-authenticated CLIs, run read-only,
|
|
799
871
|
and only ever upload model ids — secret VALUES never leave your machine.
|
|
800
872
|
|
|
801
|
-
Flags: --update · --api <url> · --key <key> · --project <id|name> · --yes · --json · --ci · --dry-run
|
|
873
|
+
Flags: --update · --api <url> · --key <key> · --project <id|name> · --dir <path> · --yes · --json · --ci · --dry-run
|
|
874
|
+
--offline (use the cached registry) · --model <slug> (fix) · --fail-on <t> · --json-out <file> (ci) · --all · --force (clear)
|
|
802
875
|
--sources <list> · --region <r> · --namespace <ns> · --kube-context <c> · --db <dsn> · --sql-table <t>
|
|
803
876
|
--vercel-project <p> · --vercel-team <t> · --gh-repo <owner/name> · --supabase-ref <ref>
|
|
804
877
|
|
package/src/registry/local.js
CHANGED
|
@@ -21,10 +21,22 @@ export function resolveLocal(snapshot, strings) {
|
|
|
21
21
|
const model = bySlug.get(slug) || null;
|
|
22
22
|
return { input: s, model_slug: slug, display: model?.display ?? s, model, confidence: 1, suggestion: null };
|
|
23
23
|
}
|
|
24
|
-
// No exact hit — find the nearest known id (
|
|
25
|
-
|
|
24
|
+
// No exact hit — find the nearest known id. Prefer (1) a key the input is a
|
|
25
|
+
// PREFIX of (input = base id, key = a versioned form, e.g. "gpt-3" →
|
|
26
|
+
// "gpt-3.5-turbo"), taking the SHORTEST such (closest minor version); else
|
|
27
|
+
// (2) the longest known id the input CONTAINS (input is a longer variant);
|
|
28
|
+
// else (3) the longest known id that contains the input. Tiering avoids
|
|
29
|
+
// latching onto a long dated/ARN variant (the old longest-overlap rule
|
|
30
|
+
// suggested e.g. an Amazon Bedrock ARN slug for a bare "claude-sonnet-4").
|
|
31
|
+
let near = null, bestScore = -1, bestTie = -Infinity;
|
|
26
32
|
for (const k of keys) {
|
|
27
|
-
if (k.length
|
|
33
|
+
if (k.length < 4) continue;
|
|
34
|
+
let score, tie;
|
|
35
|
+
if (k.startsWith(n)) { score = 3; tie = -k.length; } // versioned form of the base — shortest wins
|
|
36
|
+
else if (n.includes(k)) { score = 2; tie = k.length; } // input is a longer variant of a known id
|
|
37
|
+
else if (k.includes(n)) { score = 1; tie = -k.length; } // input embedded in a longer id — weakest
|
|
38
|
+
else continue;
|
|
39
|
+
if (score > bestScore || (score === bestScore && tie > bestTie)) { near = k; bestScore = score; bestTie = tie; }
|
|
28
40
|
}
|
|
29
41
|
const suggestion = near ? exact.get(near) || null : null;
|
|
30
42
|
return { input: s, model_slug: null, display: s, model: null, confidence: suggestion ? 0.6 : 0, suggestion };
|
|
Binary file
|
package/src/sources/index.js
CHANGED
|
@@ -94,7 +94,7 @@ export async function availability(sourceIds, opts = {}, explicit = new Set()) {
|
|
|
94
94
|
* the cheap path: uses available() (PATH check), never authState() (spawn).
|
|
95
95
|
* `explicit` is the set of ids the caller named verbatim — naming a live
|
|
96
96
|
* integration there overrides the enabled-gate. */
|
|
97
|
-
export async function collectFrom(sourceIds, opts, patterns, explicit = new Set()) {
|
|
97
|
+
export async function collectFrom(sourceIds, opts, patterns, explicit = new Set(), onProgress = null) {
|
|
98
98
|
const compiled = compilePatterns(patterns);
|
|
99
99
|
const ids = sourceIds && sourceIds.length ? sourceIds : ["filesystem"];
|
|
100
100
|
const seen = new Set();
|
|
@@ -108,7 +108,9 @@ export async function collectFrom(sourceIds, opts, patterns, explicit = new Set(
|
|
|
108
108
|
// Fold the per-source declared envTag into opts.env so the integration's env
|
|
109
109
|
// overrides guessEnvFrom — but an explicit --env flag (opts.env) still wins,
|
|
110
110
|
// and Vercel's authoritative deploy target still wins inside its own collect.
|
|
111
|
-
|
|
111
|
+
// onProgress (optional) lets a caller render a live file counter — only the
|
|
112
|
+
// filesystem source emits progress; the rest ignore the extra opt.
|
|
113
|
+
const srcOpts = src.integration ? { ...opts, env: opts.env || getEnvTag(id), onProgress } : { ...opts, onProgress };
|
|
112
114
|
for (const c of await src.collect(srcOpts, compiled)) {
|
|
113
115
|
const key = `${c.model_string}|${c.location_label}`;
|
|
114
116
|
if (seen.has(key)) continue;
|