@modelstatus/cli 0.1.87 → 0.1.89
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 +16 -0
- package/package.json +3 -2
- package/src/changelog-data.js +11 -0
- package/src/clipboard.js +42 -0
- package/src/fix-prompt.js +121 -0
- package/src/index.js +88 -2
- package/src/telemetry.js +2 -2
- package/src/tui/views/inventory.js +57 -0
- package/src/tui/views/local.js +37 -1
package/README.md
CHANGED
|
@@ -75,6 +75,20 @@ place. Boundary-safe (`gpt-4` never rewrites inside `gpt-4o`), style-preserving
|
|
|
75
75
|
it follows the chain to the first live model. In the TUI, press `f` for a
|
|
76
76
|
red/green diff preview; nothing is written until you confirm.
|
|
77
77
|
|
|
78
|
+
### Free: or hand the fixes to your AI coding agent
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
mm prompt [dir] # print a fix-it prompt for Claude Code, Cursor, …
|
|
82
|
+
mm prompt [dir] | pbcopy # straight to your clipboard
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Prints one self-contained prompt: every retired/retiring reference (file:line +
|
|
86
|
+
the exact string in code), the chain-resolved replacement, and the rewrite
|
|
87
|
+
rules. Paste it into your agent and let it do the work — unlike `mm fix`, an
|
|
88
|
+
agent can also re-pin dated ids (`-20250514`), follow config indirection, and
|
|
89
|
+
adjust parameters the new model needs. In the TUI, press `p` on the Here or
|
|
90
|
+
Inventory tab to copy the same prompt to your clipboard.
|
|
91
|
+
|
|
78
92
|
### Sign in for cloud features
|
|
79
93
|
|
|
80
94
|
```bash
|
|
@@ -92,6 +106,7 @@ You get two binaries — `mm` (short) and `llmstatus` (descriptive). Same binary
|
|
|
92
106
|
|---|---|
|
|
93
107
|
| `mm status [dir]` | Free offline model-health check — no account |
|
|
94
108
|
| `mm fix [dir]` | Rewrite dying model ids to their replacement (`--dry-run` to preview) |
|
|
109
|
+
| `mm prompt [dir]` | Print a fix-it prompt for your AI coding agent (`mm prompt \| pbcopy`) |
|
|
95
110
|
| `mm [dir]` | Launch the TUI on a folder (defaults to the current one) — runs locally |
|
|
96
111
|
| `mm update` | Update the binary in place (Homebrew installs: `brew upgrade`) |
|
|
97
112
|
| `mm login [api_key]` | Browser sign-in with polling (or paste a key) |
|
|
@@ -125,6 +140,7 @@ Secret sources shell out to your already-authenticated CLIs, run **read-only**,
|
|
|
125
140
|
| Resolve + health locally, on-device | ✓ | ✓ |
|
|
126
141
|
| Secret-source aware (`env`, `aws-secrets`, `k8s`, `helm`, `sql`) | ✓ | ✓ |
|
|
127
142
|
| `mm fix` — rewrite dying ids to replacements | ✓ | ✓ |
|
|
143
|
+
| `mm prompt` — fix-it prompt for your AI agent | ✓ | ✓ |
|
|
128
144
|
| Cloud inventory across projects/teams | — | ✓ |
|
|
129
145
|
| GitHub App: PR checks + one-click fix PRs | — | ✓ |
|
|
130
146
|
| Alerts on deprecations/retirements (email/Slack/SMS) | — | ✓ |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelstatus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.89",
|
|
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",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"LICENSE"
|
|
39
39
|
],
|
|
40
40
|
"publishConfig": {
|
|
41
|
-
"access": "public"
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": false
|
|
42
43
|
},
|
|
43
44
|
"engines": {
|
|
44
45
|
"node": ">=18"
|
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.88",
|
|
6
|
+
"date": "2026-08-22",
|
|
7
|
+
"title": "Hand the fixes to your AI coding agent",
|
|
8
|
+
"items": [
|
|
9
|
+
"New `mm prompt [dir]`: prints one self-contained prompt describing every retired/retiring model reference in your repo (file:line, the exact string in code, the chain-resolved replacement) plus rewrite rules. Paste it into Claude Code, Cursor, or any coding agent — or pipe it straight to your clipboard: `mm prompt | pbcopy`.",
|
|
10
|
+
"Unlike `mm fix` (a mechanical in-place rewrite), an agent can also re-pin dated ids like `-20250514`, follow config indirection, and adjust parameters the new model needs — the prompt spells out those rules for it.",
|
|
11
|
+
"TUI: press `p` on the Here tab or the Inventory tab to copy the same prompt to your clipboard (with a file fallback when no clipboard tool exists). On the Inventory tab the `/` filter scopes it — search `prod`, press `p`, get a prod-only prompt.",
|
|
12
|
+
"`mm status` and `mm fix` now point at `mm prompt` when there's something an auto-rewrite can't safely touch (like version-pinned ids)."
|
|
13
|
+
]
|
|
14
|
+
},
|
|
4
15
|
{
|
|
5
16
|
"version": "0.1.87",
|
|
6
17
|
"date": "2026-07-07",
|
package/src/clipboard.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Deliver generated text from inside the TUI, where stdout belongs to ink:
|
|
2
|
+
* copy to the system clipboard via the platform's native tool, or — when no
|
|
3
|
+
* clipboard tool exists (headless, bare containers) — write a file and hand
|
|
4
|
+
* back its path. Best-effort and synchronous (the payloads are a few KB).
|
|
5
|
+
* MM_NO_CLIPBOARD=1 skips the copy (tests + users who hate surprise copies);
|
|
6
|
+
* MM_PROMPT_DIR redirects the fallback file (tests). */
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
const CANDIDATES = process.platform === "darwin"
|
|
13
|
+
? [["pbcopy", []]]
|
|
14
|
+
: process.platform === "win32"
|
|
15
|
+
? [["clip", []]]
|
|
16
|
+
: [["wl-copy", []], ["xclip", ["-selection", "clipboard"]], ["xsel", ["--clipboard", "--input"]]];
|
|
17
|
+
|
|
18
|
+
/** True when the text landed on the system clipboard. */
|
|
19
|
+
export function copyToClipboard(text) {
|
|
20
|
+
if (process.env.MM_NO_CLIPBOARD === "1") return false;
|
|
21
|
+
for (const [cmd, args] of CANDIDATES) {
|
|
22
|
+
try {
|
|
23
|
+
const r = spawnSync(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"], timeout: 3000 });
|
|
24
|
+
if (r.status === 0) return true;
|
|
25
|
+
} catch { /* tool missing — try the next */ }
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Clipboard first, file fallback. Returns { method: "clipboard" } or
|
|
31
|
+
* { method: "file", path } — never throws (a failed file write returns
|
|
32
|
+
* { method: "none" } so the caller can say so instead of crashing the TUI). */
|
|
33
|
+
export function deliverText(text, { filename = "mm-fix-prompt.md" } = {}) {
|
|
34
|
+
if (copyToClipboard(text)) return { method: "clipboard" };
|
|
35
|
+
try {
|
|
36
|
+
const p = path.join(process.env.MM_PROMPT_DIR || os.tmpdir(), filename);
|
|
37
|
+
fs.writeFileSync(p, text);
|
|
38
|
+
return { method: "file", path: p };
|
|
39
|
+
} catch {
|
|
40
|
+
return { method: "none" };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* Build a copy-paste prompt for an AI coding agent (Claude Code, Cursor, …) to
|
|
2
|
+
* fix dying model references. `mm fix` is the mechanical path — boundary-safe
|
|
3
|
+
* string swaps at known file:lines — but it deliberately skips what a dumb
|
|
4
|
+
* rewrite can't do safely: version-pinned ids, config indirection, parameter
|
|
5
|
+
* changes the new model needs. An agent CAN do those, so the prompt hands it
|
|
6
|
+
* the full picture: every dying reference (file:line + the exact string), the
|
|
7
|
+
* chain-resolved replacement, and the rewrite rules fix.js enforces.
|
|
8
|
+
*
|
|
9
|
+
* Used by `mm prompt` (stdout), the Here tab (`p`) and the Inv tab (`p`).
|
|
10
|
+
* Pure text-building — no I/O, no registry access — so every surface renders
|
|
11
|
+
* the identical prompt and tests stay hermetic.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Same severity order the TUI/status sort by: already-dead first, then aging.
|
|
15
|
+
const TIER = { retired: 0, withdrawn: 0, retiring: 1, deprecating: 2 };
|
|
16
|
+
|
|
17
|
+
const REFS_CAP = 100; // per model — an agent doesn't need ref #101 to get it
|
|
18
|
+
|
|
19
|
+
/** One reference line: "- path:line — currently `gpt-4o`" (repo-prefixed when
|
|
20
|
+
* the ref came from another checkout, so a cross-repo inventory stays honest). */
|
|
21
|
+
function refLine(r) {
|
|
22
|
+
const loc = `${r.repo ? r.repo + " · " : ""}${r.file}${r.line ? ":" + r.line : ""}`;
|
|
23
|
+
return `- ${loc}${r.matched ? ` — currently \`${r.matched}\`` : ""}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Dedupe a model's refs (same file:line:string appears once; at one file:line
|
|
27
|
+
* a matched string that's a substring of a sibling's is dropped — the detector
|
|
28
|
+
* can emit both "gpt-4o" and "openai/gpt-4o" for the same spot). */
|
|
29
|
+
function dedupeRefs(refs) {
|
|
30
|
+
const seen = new Map();
|
|
31
|
+
for (const r of refs || []) {
|
|
32
|
+
if (!r?.file) continue;
|
|
33
|
+
const key = `${r.repo || ""}|${r.file}:${r.line ?? ""}:${(r.matched || "").toLowerCase()}`;
|
|
34
|
+
if (!seen.has(key)) seen.set(key, r);
|
|
35
|
+
}
|
|
36
|
+
const list = [...seen.values()];
|
|
37
|
+
return list.filter(
|
|
38
|
+
(r) => !list.some(
|
|
39
|
+
(q) => q !== r && q.file === r.file && q.line === r.line && (q.repo || "") === (r.repo || "")
|
|
40
|
+
&& q.matched && r.matched && q.matched.toLowerCase() !== r.matched.toLowerCase()
|
|
41
|
+
&& q.matched.toLowerCase().includes(r.matched.toLowerCase()),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Human health phrase for a section header. */
|
|
47
|
+
function healthPhrase(f) {
|
|
48
|
+
if (f.health === "retired") return f.retires_date ? `retired ${f.retires_date}` : "retired";
|
|
49
|
+
if (f.health === "withdrawn") return "withdrawn by the provider";
|
|
50
|
+
return `${f.health}${f.retires_date ? `, retires ${f.retires_date}` : ""}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Dedupe + drop empty findings + sort — the rows the prompt actually renders. */
|
|
54
|
+
function buildRows(findings) {
|
|
55
|
+
return (findings || [])
|
|
56
|
+
.map((f) => ({ ...f, refs: dedupeRefs(f.refs) }))
|
|
57
|
+
.filter((f) => f.refs.length)
|
|
58
|
+
.sort((a, b) => (TIER[a.health] ?? 3) - (TIER[b.health] ?? 3)
|
|
59
|
+
|| String(a.retires_date || "9999").localeCompare(String(b.retires_date || "9999"))
|
|
60
|
+
|| String(a.slug || a.display).localeCompare(String(b.slug || b.display)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The model/reference counts the rendered prompt will actually contain (after
|
|
64
|
+
* dedupe and empty-finding drops) — use these for any summary line or toast so
|
|
65
|
+
* it never disagrees with the prompt body. */
|
|
66
|
+
export function promptCounts(findings) {
|
|
67
|
+
const rows = buildRows(findings);
|
|
68
|
+
return { models: rows.length, refs: rows.reduce((n, f) => n + f.refs.length, 0) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Render the prompt. `findings` = [{ slug, display, health, retires_date,
|
|
73
|
+
* replacement, refs: [{ file, line, matched, repo? }] }] where `replacement`
|
|
74
|
+
* is the write-form target ("provider/canonical_id", terminalReplacement's
|
|
75
|
+
* output) or null when the registry has no successor yet. Findings with zero
|
|
76
|
+
* usable refs are dropped; returns null when nothing remains.
|
|
77
|
+
*/
|
|
78
|
+
export function buildFixPrompt(findings, { date = new Date() } = {}) {
|
|
79
|
+
const rows = buildRows(findings);
|
|
80
|
+
if (!rows.length) return null;
|
|
81
|
+
|
|
82
|
+
// Local calendar date — toISOString() is UTC and stamps "tomorrow" for
|
|
83
|
+
// anyone west of Greenwich in the evening.
|
|
84
|
+
const day = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
85
|
+
const nRefs = rows.reduce((n, f) => n + f.refs.length, 0);
|
|
86
|
+
const out = [];
|
|
87
|
+
out.push("# Task: update retired / retiring AI model references");
|
|
88
|
+
out.push("");
|
|
89
|
+
out.push(`This codebase references ${rows.length === 1 ? "an AI model" : `${rows.length} AI models`} that ${rows.length === 1 ? "is" : "are"} retired or scheduled for retirement (checked against the LLM Status registry on ${day}). Update every reference listed below — ${nRefs} in total.`);
|
|
90
|
+
out.push("");
|
|
91
|
+
out.push("## Models to replace");
|
|
92
|
+
|
|
93
|
+
for (const f of rows) {
|
|
94
|
+
const name = f.display && f.display !== f.slug ? `${f.display} (\`${f.slug}\`)` : `\`${f.slug || f.display}\``;
|
|
95
|
+
out.push("");
|
|
96
|
+
out.push(`### ${name} — ${healthPhrase(f)}`);
|
|
97
|
+
if (f.replacement) {
|
|
98
|
+
const i = f.replacement.indexOf("/");
|
|
99
|
+
const bare = i >= 0 ? f.replacement.slice(i + 1) : f.replacement;
|
|
100
|
+
out.push(bare !== f.replacement
|
|
101
|
+
? `Replace with: \`${bare}\` (provider-prefixed form: \`${f.replacement}\`).`
|
|
102
|
+
: `Replace with: \`${f.replacement}\`.`);
|
|
103
|
+
} else {
|
|
104
|
+
out.push("No registry replacement is listed yet — pick the provider's closest current successor, apply it consistently, and note the choice in your summary.");
|
|
105
|
+
}
|
|
106
|
+
out.push("References:");
|
|
107
|
+
for (const r of f.refs.slice(0, REFS_CAP)) out.push(refLine(r));
|
|
108
|
+
if (f.refs.length > REFS_CAP) out.push(`- …and ${f.refs.length - REFS_CAP} more (rewrite every occurrence of the old id, not just the lines listed)`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
out.push("");
|
|
112
|
+
out.push("## Rules");
|
|
113
|
+
out.push("");
|
|
114
|
+
out.push("1. Preserve each call site's existing style: keep a bare id bare (`claude-sonnet-4-5`) and a provider-prefixed id prefixed (`anthropic/claude-sonnet-4-5`); don't change quoting or surrounding formatting.");
|
|
115
|
+
out.push("2. Version-pinned ids (dated like `-20250514`, `@`-pins, Bedrock `-v1:0`, fine-tune `:suffix`): switch to the replacement model's own current id — pinned only if the platform requires a pin. Never graft the old model's version pin onto the new id.");
|
|
116
|
+
out.push("3. If a model id comes from a config file, env var, constant, or database row, update it at the source — not just the call sites listed above.");
|
|
117
|
+
out.push("4. Review adjacent parameters that may need adjusting for the new model (max output tokens, temperature constraints, flags the old model needed).");
|
|
118
|
+
out.push("5. When you're done, run the project's tests, then run `mm status` (the LLM Status CLI, https://llmstatus.ai) to confirm no retired or retiring references remain.");
|
|
119
|
+
out.push("");
|
|
120
|
+
return out.join("\n");
|
|
121
|
+
}
|
package/src/index.js
CHANGED
|
@@ -774,9 +774,79 @@ async function cmdFix(positional, flags) {
|
|
|
774
774
|
console.log(`\n✓ rewrote ${res.applied.length} reference(s) in ${new Set(res.applied.map((p) => p.file)).size} file(s).`);
|
|
775
775
|
for (const s of res.stale) console.log(` ! skipped ${s.file}:${s.line} — ${s.error}`);
|
|
776
776
|
for (const f of res.failed) console.log(` × failed ${f.file}:${f.line} — ${f.error}`);
|
|
777
|
+
if (res.stale.some((s) => /pinned variant/.test(s.error || ""))) {
|
|
778
|
+
console.log("\n→ pinned ids need a human (or an agent): `mm prompt` prints a fix-it prompt for your AI coding agent.");
|
|
779
|
+
}
|
|
777
780
|
if (res.applied.length) console.log("\nRe-run your tests, then `mm status` to confirm everything reads current.");
|
|
778
781
|
}
|
|
779
782
|
|
|
783
|
+
/** `mm prompt [dir]` — print a copy-paste prompt for an AI coding agent to fix
|
|
784
|
+
* dying model refs. Where `mm fix` mechanically rewrites what it can prove safe,
|
|
785
|
+
* the prompt hands EVERYTHING to an agent — including version-pinned ids, models
|
|
786
|
+
* with no registry replacement yet, and the config/parameter follow-ups a string
|
|
787
|
+
* swap can't do. stdout is the prompt and nothing else (pipe it: `mm prompt |
|
|
788
|
+
* pbcopy`); progress + the summary line go to stderr. */
|
|
789
|
+
async function cmdPrompt(positional, flags) {
|
|
790
|
+
const dir = path.resolve(positional[1] || flags.dir || ".");
|
|
791
|
+
requireDirectory(dir);
|
|
792
|
+
const { getRegistry } = await import("./registry/fetch.js");
|
|
793
|
+
const { resolveLocal, computeHealth, dropResolvedFragments } = await import("./registry/local.js");
|
|
794
|
+
const { terminalReplacement } = await import("./fix.js");
|
|
795
|
+
const { buildFixPrompt, promptCounts } = await import("./fix-prompt.js");
|
|
796
|
+
const prog = startProgress(true, "fetching the model registry…");
|
|
797
|
+
const snapshot = await getRegistry({
|
|
798
|
+
offline: !!flags.offline || process.env.LLMSTATUS_REGISTRY_OFFLINE === "1",
|
|
799
|
+
cacheFile: process.env.LLMSTATUS_REGISTRY_CACHE || undefined,
|
|
800
|
+
log: (m) => prog.log(m),
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
prog.update("scanning for model references…");
|
|
804
|
+
const onProgress = ({ filesScanned, candidates: c }) => prog.update(`scanning… ${filesScanned} files, ${c} reference(s)`);
|
|
805
|
+
let candidates = await collectFrom(["filesystem"], { root: dir }, snapshot.detection, new Set(), onProgress);
|
|
806
|
+
prog.stop();
|
|
807
|
+
const resolved = resolveLocal(snapshot, [...new Set(candidates.map((c) => c.model_string))]);
|
|
808
|
+
const byStr = new Map(resolved.map((r) => [r.input.toLowerCase(), r]));
|
|
809
|
+
candidates = dropResolvedFragments(candidates, (c) => !!byStr.get(c.model_string.toLowerCase())?.model_slug);
|
|
810
|
+
const today = new Date();
|
|
811
|
+
|
|
812
|
+
// Group EVERY dying model's refs — unlike cmdFix, keep models with no
|
|
813
|
+
// replacement (the agent picks a successor) and pinned refs (it re-pins).
|
|
814
|
+
const byModel = new Map(); // slug -> { model, health, refs }
|
|
815
|
+
for (const c of candidates) {
|
|
816
|
+
const r = byStr.get(c.model_string.toLowerCase());
|
|
817
|
+
if (!r?.model_slug || !r.model) continue;
|
|
818
|
+
const health = computeHealth(r.model, 90, today);
|
|
819
|
+
if (health === "ok") continue;
|
|
820
|
+
if (flags.model && r.model_slug !== flags.model) continue;
|
|
821
|
+
const e = byModel.get(r.model_slug) || { model: r.model, health, refs: [] };
|
|
822
|
+
e.refs.push(c);
|
|
823
|
+
byModel.set(r.model_slug, e);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const bySlug = new Map(snapshot.models.map((m) => [m.slug, m]));
|
|
827
|
+
const isCurrent = (m) => computeHealth(m, 90, today) === "ok";
|
|
828
|
+
const findings = [...byModel.values()].map(({ model, health, refs }) => ({
|
|
829
|
+
slug: model.slug,
|
|
830
|
+
display: model.display,
|
|
831
|
+
health,
|
|
832
|
+
retires_date: model.retires_date,
|
|
833
|
+
replacement: model.replacement_slug
|
|
834
|
+
? terminalReplacement(model.replacement_slug, (slug) => bySlug.get(slug) ?? null, isCurrent)
|
|
835
|
+
: null,
|
|
836
|
+
refs: refs.map((c) => ({ file: c.source_path || c.location_label, line: c.source_line, matched: c.model_string })),
|
|
837
|
+
}));
|
|
838
|
+
|
|
839
|
+
const text = buildFixPrompt(findings, { date: today });
|
|
840
|
+
if (!text) {
|
|
841
|
+
process.stderr.write("Nothing to fix — every recognized model here is current. No prompt to generate.\n");
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
console.log(text);
|
|
845
|
+
const counts = promptCounts(findings);
|
|
846
|
+
const copyTool = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
|
|
847
|
+
process.stderr.write(`\n(${counts.models} model(s), ${counts.refs} reference(s). Copy it straight to your clipboard: mm prompt | ${copyTool})\n`);
|
|
848
|
+
}
|
|
849
|
+
|
|
780
850
|
/** List detection sources and whether each can run right now. Live integrations
|
|
781
851
|
* also show their on/off toggle (the `int` column) so toggled state is visible
|
|
782
852
|
* here too. */
|
|
@@ -791,7 +861,7 @@ async function cmdSources(_positional, flags) {
|
|
|
791
861
|
}
|
|
792
862
|
console.log("\nScan with: mm scan --sources env,aws-secrets,k8s,helm,sql (or --sources all)");
|
|
793
863
|
console.log("Integrations (toggle with `mm integrations enable <id>`): " + INTEGRATION_IDS.join(", "));
|
|
794
|
-
console.log("Options: --region <r> · --namespace <ns> · --kube-context <c> · --db <pg-dsn> --sql-table <t>");
|
|
864
|
+
console.log("Options: --region <r> · --namespace <ns> · --kube-context <c> · --db <pg-dsn> · --sql-table <t>");
|
|
795
865
|
console.log(" --vercel-project <p> · --vercel-team <t> · --gh-repo <owner/name> · --supabase-ref <ref>");
|
|
796
866
|
console.log("Safety: secret VALUES never leave your machine — only model ids upload. Use --dry-run to preview.");
|
|
797
867
|
}
|
|
@@ -973,6 +1043,7 @@ async function cmdStatus(positional, flags) {
|
|
|
973
1043
|
const fixable = attention.filter((r) => r.model.replacement_slug).length;
|
|
974
1044
|
const tips = [];
|
|
975
1045
|
if (fixable) tips.push(`\`mm fix\` rewrites the ${fixable} with a known replacement`);
|
|
1046
|
+
tips.push("`mm prompt` prints a fix-it prompt for your AI coding agent (Claude Code, Cursor, …)");
|
|
976
1047
|
if (!loadConfig().apiKey) tips.push("`mm login` to get alerted before these dates (Pro)");
|
|
977
1048
|
if (tips.length) console.log("\n" + tips.map((t) => "→ " + t).join("\n"));
|
|
978
1049
|
}
|
|
@@ -989,6 +1060,7 @@ Usage:
|
|
|
989
1060
|
mm config View or change settings (analytics, …)
|
|
990
1061
|
mm scan [dir] Scan for model usage; interactive TUI, or --ci/--json for pipelines
|
|
991
1062
|
mm fix [dir] Rewrite dying model ids to their replacement, in place (--dry-run previews; --model <slug> limits; --yes skips the confirm)
|
|
1063
|
+
mm prompt [dir] Print a fix-it prompt for your AI coding agent — paste it into Claude Code, Cursor, … and let it do the rewrites (mm prompt | pbcopy)
|
|
992
1064
|
mm ci [dir] CI gate: fail the build on deprecated/retiring models (GitHub annotations)
|
|
993
1065
|
(--fail-on <none|deprecating|retiring|retired> sets the threshold, default retired;
|
|
994
1066
|
--json-out <file> writes findings JSON; --diff <base> limits to changed files, auto on PRs)
|
|
@@ -1011,7 +1083,7 @@ Scan sources (--sources; default = filesystem + enabled integrations; "all" for
|
|
|
1011
1083
|
and only ever upload model ids — secret VALUES never leave your machine.
|
|
1012
1084
|
|
|
1013
1085
|
Flags: --update · --api <url> · --key <key> · --project <id|name> · --dir <path> · --yes · --json · --ci · --dry-run
|
|
1014
|
-
--offline (use the cached registry) · --model <slug> (fix) · --fail-on <t> · --json-out <file> (ci) · --all · --force (clear)
|
|
1086
|
+
--offline (use the cached registry) · --model <slug> (fix, prompt) · --fail-on <t> · --json-out <file> (ci) · --all · --force (clear)
|
|
1015
1087
|
--sources <list> · --region <r> · --namespace <ns> · --kube-context <c> · --db <dsn> · --sql-table <t>
|
|
1016
1088
|
--vercel-project <p> · --vercel-team <t> · --gh-repo <owner/name> · --supabase-ref <ref>
|
|
1017
1089
|
|
|
@@ -1042,6 +1114,19 @@ const COMMAND_HELP = {
|
|
|
1042
1114
|
--yes skip the confirmation prompt
|
|
1043
1115
|
--model <slug> only fix this one model (full provider/slug)
|
|
1044
1116
|
--offline use the cached registry only`,
|
|
1117
|
+
prompt: `mm prompt [dir] Print a copy-paste prompt for an AI coding agent to fix dying model refs.
|
|
1118
|
+
|
|
1119
|
+
Scans like \`mm status\`, then prints one self-contained prompt to stdout: every
|
|
1120
|
+
retired/retiring model reference (file:line + the exact string in code), the
|
|
1121
|
+
chain-resolved replacement, and the rewrite rules. Paste it into Claude Code,
|
|
1122
|
+
Cursor, or any coding agent. Unlike \`mm fix\` (a mechanical in-place rewrite),
|
|
1123
|
+
an agent can also re-pin dated ids, follow config indirection, and adjust
|
|
1124
|
+
parameters the new model needs. stdout is ONLY the prompt — pipe it:
|
|
1125
|
+
mm prompt | pbcopy
|
|
1126
|
+
|
|
1127
|
+
--model <slug> only include this one model (full provider/slug)
|
|
1128
|
+
--offline use the cached registry only
|
|
1129
|
+
--dir <path> directory to scan (alternative to the positional arg)`,
|
|
1045
1130
|
ci: `mm ci [dir] CI gate: fail the build on deprecated/retiring models.
|
|
1046
1131
|
|
|
1047
1132
|
Exits non-zero when a finding is at/above --fail-on. Emits GitHub annotations +
|
|
@@ -1217,6 +1302,7 @@ async function main() {
|
|
|
1217
1302
|
}
|
|
1218
1303
|
else if (cmd === "scan") await cmdScan(positional, flags);
|
|
1219
1304
|
else if (cmd === "fix") await cmdFix(positional, flags);
|
|
1305
|
+
else if (cmd === "prompt") await cmdPrompt(positional, flags);
|
|
1220
1306
|
else if (cmd === "ci") await cmdCi(positional, flags);
|
|
1221
1307
|
else if (cmd === "status") await cmdStatus(positional, flags);
|
|
1222
1308
|
else if (cmd === "sources") await cmdSources(positional, flags);
|
package/src/telemetry.js
CHANGED
|
@@ -75,8 +75,8 @@ export function analyticsState() {
|
|
|
75
75
|
* paths") includes the path the user points mm at. Keep in sync with the
|
|
76
76
|
* dispatch in index.js main(). */
|
|
77
77
|
const KNOWN_COMMANDS = new Set([
|
|
78
|
-
"login", "signup", "logout", "config", "analytics", "scan", "fix", "
|
|
79
|
-
"status", "sources", "integrations", "clear", "play", "upgrade", "tui",
|
|
78
|
+
"login", "signup", "logout", "config", "analytics", "scan", "fix", "prompt",
|
|
79
|
+
"ci", "status", "sources", "integrations", "clear", "play", "upgrade", "tui",
|
|
80
80
|
"update", "version", "help",
|
|
81
81
|
]);
|
|
82
82
|
|
|
@@ -24,6 +24,7 @@ export const meta = {
|
|
|
24
24
|
{ k: "g", label: "refresh" },
|
|
25
25
|
{ k: "r", label: "rescan" },
|
|
26
26
|
{ k: "n", label: "new" },
|
|
27
|
+
{ k: "p", label: "llm prompt" },
|
|
27
28
|
{ k: "e", label: "env" },
|
|
28
29
|
{ k: "t", label: "tag" },
|
|
29
30
|
{ k: "c", label: "critical" },
|
|
@@ -100,6 +101,61 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
|
|
|
100
101
|
ui?.reportStatus?.({ counts, context: query ? `${filtered.length} of ${usages.length}` : `${usages.length} tracked` });
|
|
101
102
|
}, [usages, ui, query, filtered.length, q.error, q.loading]);
|
|
102
103
|
|
|
104
|
+
/** p: build the LLM fix prompt for every dying usage in the CURRENT view
|
|
105
|
+
* (the / filter scopes it — search "prod", press p, get a prod-only prompt)
|
|
106
|
+
* and put it on the clipboard (file fallback). Inventory rows can point at
|
|
107
|
+
* code on other machines, which is exactly why the prompt exists: paste it
|
|
108
|
+
* into the agent that lives WITH that code. Registry lookup is best-effort
|
|
109
|
+
* (cached snapshot) to turn replacement slugs into real API ids; without a
|
|
110
|
+
* cache the slug is still an unambiguous instruction for an agent. */
|
|
111
|
+
async function makeLlmPrompt() {
|
|
112
|
+
const dying = filtered.filter((u) => u.health && u.health !== "ok" && u.health !== "custom");
|
|
113
|
+
if (!dying.length) return ui.showToast(query ? "no matching usages need fixing" : "all current — nothing to fix", "#16a34a");
|
|
114
|
+
try {
|
|
115
|
+
const [{ terminalReplacement }, { buildFixPrompt, promptCounts }, { deliverText }, { computeHealth }] = await Promise.all([
|
|
116
|
+
import("../../fix.js"), import("../../fix-prompt.js"), import("../../clipboard.js"), import("../../registry/local.js"),
|
|
117
|
+
]);
|
|
118
|
+
const today = new Date();
|
|
119
|
+
let bySlug = new Map();
|
|
120
|
+
try {
|
|
121
|
+
const { getRegistry } = await import("../../registry/fetch.js");
|
|
122
|
+
const snap = await getRegistry({ offline: true, cacheFile: process.env.LLMSTATUS_REGISTRY_CACHE || undefined });
|
|
123
|
+
bySlug = new Map((snap.models || []).map((m) => [m.slug, m]));
|
|
124
|
+
} catch { /* no cached registry — replacement slugs still name the target */ }
|
|
125
|
+
const isCurrent = (m) => computeHealth(m, 90, today) === "ok";
|
|
126
|
+
const byModel = new Map(); // one section per model, refs merged across projects
|
|
127
|
+
for (const u of dying) {
|
|
128
|
+
const key = u.model_display || u.custom_model_name || u.canonical_id || "?";
|
|
129
|
+
const e = byModel.get(key) || {
|
|
130
|
+
slug: u.canonical_id || key,
|
|
131
|
+
display: u.model_display || key,
|
|
132
|
+
health: u.health,
|
|
133
|
+
retires_date: u.retires_date ? String(u.retires_date).slice(0, 10) : null,
|
|
134
|
+
replacement: u.replacement_slug
|
|
135
|
+
? terminalReplacement(u.replacement_slug, (slug) => bySlug.get(slug) ?? null, isCurrent)
|
|
136
|
+
: null,
|
|
137
|
+
refs: [],
|
|
138
|
+
};
|
|
139
|
+
e.refs.push({
|
|
140
|
+
file: u.source_path || u.location_label || "(added manually — find its usages)",
|
|
141
|
+
line: u.source_line,
|
|
142
|
+
matched: u.custom_model_name || u.canonical_id,
|
|
143
|
+
repo: u.source_repo || undefined,
|
|
144
|
+
});
|
|
145
|
+
byModel.set(key, e);
|
|
146
|
+
}
|
|
147
|
+
const text = buildFixPrompt([...byModel.values()], { date: today });
|
|
148
|
+
if (!text) return ui.showToast("no locatable references to hand off", "#d97706");
|
|
149
|
+
const counts = promptCounts([...byModel.values()]);
|
|
150
|
+
const res = deliverText(text);
|
|
151
|
+
if (res.method === "clipboard") ui.showToast(`${GLYPH.check} LLM fix prompt copied (${counts.models} model${counts.models === 1 ? "" : "s"} · ${counts.refs} refs) — paste into your AI agent`);
|
|
152
|
+
else if (res.method === "file") ui.showToast(`${GLYPH.check} couldn't copy — prompt saved to ${res.path}`);
|
|
153
|
+
else ui.showToast("couldn't copy or save the prompt", "#dc2626");
|
|
154
|
+
} catch (e) {
|
|
155
|
+
ui.showToast(e.message, "red");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
103
159
|
async function patch(u, body, label) {
|
|
104
160
|
try {
|
|
105
161
|
await client.patchUsage(u.id, body);
|
|
@@ -128,6 +184,7 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
|
|
|
128
184
|
if (input === "g") return q.reload();
|
|
129
185
|
if (input === "r") return ui.switchTo("scan");
|
|
130
186
|
if (input === "n") return ui.switchTo("add");
|
|
187
|
+
if (input === "p") return makeLlmPrompt();
|
|
131
188
|
if (input === "t") {
|
|
132
189
|
const untagged = usages.filter(isUntagged);
|
|
133
190
|
if (!untagged.length) return ui.showToast("nothing untagged");
|
package/src/tui/views/local.js
CHANGED
|
@@ -29,6 +29,7 @@ export const meta = {
|
|
|
29
29
|
{ k: "↑↓", label: "nav" },
|
|
30
30
|
{ k: "↵", label: "refs" },
|
|
31
31
|
{ k: "f", label: "fix all" },
|
|
32
|
+
{ k: "p", label: "llm prompt" },
|
|
32
33
|
{ k: "u", label: "push → Inv" },
|
|
33
34
|
{ k: "g", label: "rescan" },
|
|
34
35
|
{ k: "/", label: "search" },
|
|
@@ -134,6 +135,39 @@ export function LocalView({ client, me, dir, ui, width = 78, height = 14, active
|
|
|
134
135
|
});
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
/** p (while idle): build the LLM fix prompt for EVERY dying model found here
|
|
139
|
+
* and put it on the clipboard (file fallback) — for the refs `mm fix` can't
|
|
140
|
+
* safely touch (pinned ids, config indirection) hand the whole job to the
|
|
141
|
+
* user's AI coding agent instead. Same builder as `mm prompt`. */
|
|
142
|
+
function makeLlmPrompt() {
|
|
143
|
+
const dying = items.filter((it) => it.model && it.health !== "ok" && it.health !== "custom");
|
|
144
|
+
if (!dying.length) return ui?.showToast?.("all current — nothing to fix", "#16a34a");
|
|
145
|
+
Promise.all([import("../../fix.js"), import("../../fix-prompt.js"), import("../../clipboard.js")]).then(
|
|
146
|
+
([{ terminalReplacement }, { buildFixPrompt, promptCounts }, { deliverText }]) => {
|
|
147
|
+
const bySlug = new Map((scan.snapshot?.models || []).map((m) => [m.slug, m]));
|
|
148
|
+
const today = new Date();
|
|
149
|
+
const isCurrent = (m) => computeHealth(m, 90, today) === "ok";
|
|
150
|
+
const findings = dying.map((it) => ({
|
|
151
|
+
slug: it.model.slug,
|
|
152
|
+
display: it.model.display,
|
|
153
|
+
health: it.health,
|
|
154
|
+
retires_date: it.model.retires_date,
|
|
155
|
+
replacement: it.model.replacement_slug
|
|
156
|
+
? terminalReplacement(it.model.replacement_slug, (slug) => bySlug.get(slug) ?? null, isCurrent)
|
|
157
|
+
: null,
|
|
158
|
+
refs: distinctRefs(it.refs).map((r) => ({ file: r.source_path || r.location_label, line: r.source_line, matched: r.model_string })),
|
|
159
|
+
}));
|
|
160
|
+
const text = buildFixPrompt(findings, { date: today });
|
|
161
|
+
if (!text) return ui?.showToast?.("no file references to hand off", "#d97706");
|
|
162
|
+
const counts = promptCounts(findings);
|
|
163
|
+
const res = deliverText(text);
|
|
164
|
+
if (res.method === "clipboard") ui?.showToast?.(`${GLYPH.check} LLM fix prompt copied (${counts.models} model${counts.models === 1 ? "" : "s"} · ${counts.refs} refs) — paste into your AI agent`);
|
|
165
|
+
else if (res.method === "file") ui?.showToast?.(`${GLYPH.check} couldn't copy — prompt saved to ${res.path}`);
|
|
166
|
+
else ui?.showToast?.("couldn't copy or save the prompt", "#dc2626");
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
137
171
|
const tick = useTick(80, running || pushing);
|
|
138
172
|
const spin = SPINNER[tick % SPINNER.length];
|
|
139
173
|
const search = useSearch();
|
|
@@ -281,7 +315,9 @@ export function LocalView({ client, me, dir, ui, width = 78, height = 14, active
|
|
|
281
315
|
if (key.upArrow || input === "k") return nav.up();
|
|
282
316
|
if (input === "e") return excludeRef(drefs[0]); // exclude the highlighted model's location (editable)
|
|
283
317
|
if (input === "f") return fixRefs(cur?.refs || [], `all ${cur?.count ?? 0} references`);
|
|
284
|
-
|
|
318
|
+
// p is overloaded by state: pausing only means anything mid-scan, and the
|
|
319
|
+
// prompt is only trustworthy once the scan is done — no key collision.
|
|
320
|
+
if (input === "p") return running ? scan.togglePause() : makeLlmPrompt();
|
|
285
321
|
if (input === "g") { justReloadedRef.current = true; return scan.reload(); }
|
|
286
322
|
if (input === "u" && !pushing) return pushToInventory();
|
|
287
323
|
},
|