@modelstatus/cli 0.1.73 → 0.1.75

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.73",
3
+ "version": "0.1.75",
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/updater.js CHANGED
@@ -88,6 +88,33 @@ async function fetchJson(url) {
88
88
  return res.json();
89
89
  }
90
90
 
91
+ // Ed25519 RELEASE public key (committed: packages/cli/release-pubkey.pem). The
92
+ // manifest is signed with the matching private key; verifying that signature is
93
+ // what makes the manifest's sha256 trustworthy on every platform, independent of
94
+ // the CDN. An auto-update that can't verify this signature does not proceed.
95
+ const RELEASE_PUBKEY = `-----BEGIN PUBLIC KEY-----
96
+ MCowBQYDK2VwAyEApu4VP7vB6iwxsPcK5KaosjK7SIp9HrWMt5IvcUwjUOM=
97
+ -----END PUBLIC KEY-----`;
98
+
99
+ /** Fetch the manifest + its detached Ed25519 signature and verify before
100
+ * parsing. Returns the parsed manifest, or throws if the signature is missing or
101
+ * invalid (fail closed — never self-update against an unverifiable manifest). */
102
+ async function fetchVerifiedManifest(base) {
103
+ const [mRes, sRes] = await Promise.all([
104
+ fetch(`${base}/version.json`, { cache: "no-store" }),
105
+ fetch(`${base}/version.json.sig`, { cache: "no-store" }),
106
+ ]);
107
+ if (!mRes.ok) throw new Error(`GET version.json -> ${mRes.status}`);
108
+ if (!sRes.ok) throw new Error("manifest signature unavailable — refusing to self-update");
109
+ const manifestBytes = Buffer.from(await mRes.arrayBuffer());
110
+ const sig = Buffer.from((await sRes.text()).trim(), "base64");
111
+ const key = crypto.createPublicKey(RELEASE_PUBKEY);
112
+ if (!crypto.verify(null, manifestBytes, key, sig)) {
113
+ throw new Error("manifest signature INVALID — refusing to self-update");
114
+ }
115
+ return JSON.parse(manifestBytes.toString("utf8"));
116
+ }
117
+
91
118
  /** True if we can create/rename files in `dir` (needed for an atomic swap). */
92
119
  function dirWritable(dir) {
93
120
  try {
@@ -189,7 +216,7 @@ export async function forceUpdate() {
189
216
  const key = platformKey();
190
217
  if (!key) return { status: "unsupported" };
191
218
 
192
- const manifest = await fetchJson(`${CDN}/cli/${CHANNEL_PATH}/version.json`);
219
+ const manifest = await fetchVerifiedManifest(`${CDN}/cli/${CHANNEL_PATH}`);
193
220
  writeCache({ ...(readCache() || {}), last_check: Date.now(), latest_known: manifest.version });
194
221
  if (!manifest.version) return { status: "error", message: "manifest has no version" };
195
222
  if (compareVer(manifest.version, BUILD_VERSION) <= 0) return { status: "latest", version: dispVer(BUILD_VERSION) };
@@ -223,7 +250,7 @@ export async function maybeCheckForUpdate(flags = {}) {
223
250
  const key = platformKey();
224
251
  if (!key) return null;
225
252
 
226
- const manifest = await fetchJson(`${CDN}/cli/${CHANNEL_PATH}/version.json`);
253
+ const manifest = await fetchVerifiedManifest(`${CDN}/cli/${CHANNEL_PATH}`);
227
254
  // Always update the cache so we don't re-check for 24 h.
228
255
  writeCache({ ...cache, last_check: Date.now(), latest_known: manifest.version });
229
256