@modelstatus/cli 0.1.77 → 0.1.78

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,7 +8,7 @@ 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, 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.)
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. (Anonymous usage analytics — event names + counts only, never code, model names, or paths can be turned off anytime: `mm analytics off`, or `MM_NO_ANALYTICS=1`.)
12
12
 
13
13
  ## Install
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelstatus/cli",
3
- "version": "0.1.77",
3
+ "version": "0.1.78",
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
@@ -14,7 +14,7 @@ import { redactValue } from "./redact.js";
14
14
  import { assignProjects, buildUsages } from "./upload.js";
15
15
  import { loginViaBrowser } from "./auth.js";
16
16
  import { maybeCheckForUpdate, forceUpdate } from "./updater.js";
17
- import { track, maybeAnalyticsNotice } from "./telemetry.js";
17
+ import { track, analyticsState } from "./telemetry.js";
18
18
  import { BUILD_VERSION } from "./version.js";
19
19
 
20
20
  // TRUE BACKGROUND SCAN — hidden worker dispatch. MUST be the first executable
@@ -763,6 +763,7 @@ Usage:
763
763
  mm login [api_key] Browser sign-in with polling (or paste a key)
764
764
  mm signup Create an account in the browser, then poll
765
765
  mm logout Forget the saved API key
766
+ mm analytics [on|off] Anonymous usage stats (event counts only — never code or paths)
766
767
  mm scan [dir] Scan for model usage; interactive TUI, or --ci/--json for pipelines
767
768
  mm fix [dir] Rewrite dying model ids to their replacement, in place (--dry-run previews; --model <slug> limits; --yes skips the confirm)
768
769
  mm ci [dir] CI gate: fail the build on deprecated/retiring models (GitHub annotations)
@@ -853,7 +854,6 @@ async function main() {
853
854
 
854
855
  // Anonymous, opt-out usage analytics (one-time disclosure, then a single
855
856
  // event per invocation). No-op without a baked key / when opted out.
856
- maybeAnalyticsNotice();
857
857
  track("cli_command", { command: cmd || "tui" });
858
858
 
859
859
  // Explicit self-update: `mm update` (command) or `--update` (flag on any
@@ -878,6 +878,19 @@ async function main() {
878
878
  if (cmd === "login") await cmdLogin(positional, flags);
879
879
  else if (cmd === "signup") await cmdSignup(positional, flags);
880
880
  else if (cmd === "logout") cmdLogout();
881
+ else if (cmd === "analytics") {
882
+ const arg = (positional[1] || "").toLowerCase();
883
+ if (arg === "on" || arg === "off") {
884
+ const { setConfigValue } = await import("./config.js");
885
+ setConfigValue("analyticsOptOut", arg === "off");
886
+ console.log(`Anonymous usage analytics ${arg}.`);
887
+ } else {
888
+ const st = analyticsState();
889
+ console.log(`Anonymous usage analytics: ${st.on ? "on" : `off${st.reason ? ` — ${st.reason}` : ""}`}`);
890
+ console.log(" When on, mm sends event names + counts only — never code, model names, or paths.");
891
+ console.log(" Toggle: mm analytics on|off · also honored: MM_NO_ANALYTICS=1, DO_NOT_TRACK=1, CI=1");
892
+ }
893
+ }
881
894
  else if (cmd === "scan") await cmdScan(positional, flags);
882
895
  else if (cmd === "fix") await cmdFix(positional, flags);
883
896
  else if (cmd === "ci") await cmdCi(positional, flags);
package/src/telemetry.js CHANGED
@@ -14,7 +14,10 @@ import { BUILD_VERSION, UPDATE_CHANNEL } from "./version.js";
14
14
  const POSTHOG_KEY = typeof __POSTHOG_KEY__ !== "undefined" ? __POSTHOG_KEY__ : (process.env.MM_POSTHOG_KEY || "");
15
15
  const POSTHOG_HOST = (process.env.MM_POSTHOG_HOST || "https://us.i.posthog.com").replace(/\/$/, "");
16
16
 
17
- const optedOut = () => !!(process.env.MM_NO_ANALYTICS || process.env.DO_NOT_TRACK || process.env.CI);
17
+ const optedOut = () => {
18
+ if (process.env.MM_NO_ANALYTICS || process.env.DO_NOT_TRACK || process.env.CI) return true;
19
+ try { return !!loadConfig().analyticsOptOut; } catch { return false; }
20
+ };
18
21
  const enabled = () => !!POSTHOG_KEY && !optedOut();
19
22
 
20
23
  let cachedId = null;
@@ -27,13 +30,15 @@ function distinctId() {
27
30
  }
28
31
 
29
32
  /** One-time, pre-TUI stderr disclosure. Honors opt-out + only shows once. */
30
- export function maybeAnalyticsNotice() {
31
- if (!enabled()) return;
32
- if (loadConfig().analyticsNoticeShown) return;
33
- try { setConfigValue("analyticsNoticeShown", true); } catch { /* best effort */ }
34
- process.stderr.write(
35
- "ℹ llmstatus sends anonymous usage analytics (event names + counts only — never code, model names, or paths). Opt out: MM_NO_ANALYTICS=1\n",
36
- );
33
+ /** Current analytics state + why — powers `mm analytics` and the Account tab.
34
+ * Disclosure lives there + README/docs/--help (not as a banner interrupting
35
+ * scan output). */
36
+ export function analyticsState() {
37
+ if (process.env.MM_NO_ANALYTICS || process.env.DO_NOT_TRACK || process.env.CI) {
38
+ return { on: false, reason: "env (MM_NO_ANALYTICS / DO_NOT_TRACK / CI)" };
39
+ }
40
+ try { if (loadConfig().analyticsOptOut) return { on: false, reason: "mm analytics off" }; } catch { /* ignore */ }
41
+ return { on: !!POSTHOG_KEY, reason: POSTHOG_KEY ? "" : "no key baked in" };
37
42
  }
38
43
 
39
44
  /** Fire-and-forget capture. Never throws, never blocks the CLI meaningfully. */
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { h, C, GLYPH } from "../ui.js";
4
4
  import { loadConfig, configFilePath } from "../../config.js";
5
+ import { analyticsState } from "../../telemetry.js";
5
6
  import { openUrl } from "../../openUrl.js";
6
7
 
7
8
  const FREE_LIMITS = { projects: 1, usages: 15, channels: 1 };
@@ -15,7 +16,7 @@ function Row({ label, value, color = C.FG, bold = false }) {
15
16
  return h(
16
17
  Text,
17
18
  {},
18
- h(Text, { color: C.ACCENT }, label.padEnd(9)),
19
+ h(Text, { color: C.ACCENT }, label.padEnd(10)),
19
20
  h(Text, { color, bold }, value),
20
21
  );
21
22
  }
@@ -98,6 +99,7 @@ export function AccountView({ client, me, refreshMe, apiBase, ui, active }) {
98
99
  h(Row, { label: "retiring", value: `${me?.retiring_window_days ?? 90} day window` }),
99
100
  h(Row, { label: "endpoint", value: endpoint }),
100
101
  h(Row, { label: "key", value: keyPrefix }),
102
+ h(Row, { label: "analytics", value: analyticsState().on ? "on · anonymous event counts only · mm analytics off" : "off", color: C.FG_FAINT }),
101
103
  h(Row, { label: "config", value: configFilePath, color: C.FG_FAINT }),
102
104
  ),
103
105
  h(Text, {}, ""),