@modelstatus/cli 0.1.0
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/LICENSE +21 -0
- package/README.md +124 -0
- package/package.json +55 -0
- package/src/api.js +85 -0
- package/src/auth.js +44 -0
- package/src/config.js +63 -0
- package/src/detect/core.js +58 -0
- package/src/index.js +337 -0
- package/src/openUrl.js +29 -0
- package/src/redact.js +28 -0
- package/src/registry/fetch.js +93 -0
- package/src/registry/local.js +34 -0
- package/src/registry/root-keys.js +12 -0
- package/src/registry/sign.js +17 -0
- package/src/registry/verify.js +67 -0
- package/src/scan.js +15 -0
- package/src/sources/aws.js +63 -0
- package/src/sources/configscan.js +88 -0
- package/src/sources/env.js +21 -0
- package/src/sources/filesystem.js +95 -0
- package/src/sources/helm.js +42 -0
- package/src/sources/index.js +63 -0
- package/src/sources/k8s.js +51 -0
- package/src/sources/shell.js +35 -0
- package/src/sources/sql.js +47 -0
- package/src/tui/app.js +139 -0
- package/src/tui/ui.js +47 -0
- package/src/tui/views/account.js +76 -0
- package/src/tui/views/add.js +84 -0
- package/src/tui/views/alerts.js +160 -0
- package/src/tui/views/inventory.js +102 -0
- package/src/tui/views/scan.js +125 -0
- package/src/tui/views/whatsnew.js +177 -0
- package/src/upgrade.js +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LLM Status
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# @modelstatus/cli
|
|
2
|
+
|
|
3
|
+
**Track which AI models you use, where, and never get surprised by a retirement.**
|
|
4
|
+
|
|
5
|
+
The free CLI + TUI for [LLM Status](https://llmstatus.ai) — scans your repo for AI model usage (OpenAI, Anthropic, Google, Mistral, DeepSeek, xAI, Moonshot, Cohere, and dozens more) and joins it to a constantly-updated lifecycle registry so you find out *before* `gpt-4` retires, not after.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @modelstatus/cli status
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That's it. No sign-in, no account, no telemetry — just a snapshot of every model in your repo plus health badges and replacement suggestions.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Pick whichever fits your stack:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Self-contained binary, no Node required:
|
|
19
|
+
curl -fsSL https://llmstatus.ai/install.sh | bash
|
|
20
|
+
|
|
21
|
+
# Via npm (needs Node ≥18):
|
|
22
|
+
npm i -g @modelstatus/cli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or skip install entirely and one-shot it: `npx @modelstatus/cli status`.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
### Free: offline health check
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
mm status [dir] # if installed
|
|
33
|
+
npx @modelstatus/cli status [dir] # zero install (needs Node)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Pulls the **signed** registry snapshot from `cdn.llmstatus.ai` (Ed25519-signed, anti-rollback, ~225 kB), scans the directory, resolves every model id, and prints what you use along with its health:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
LLM Status — registry 20260528T013239Z (today), 380 models
|
|
40
|
+
Scanned ./apps/web: 12 reference(s) → 6 model(s), 1 custom
|
|
41
|
+
|
|
42
|
+
Models in use:
|
|
43
|
+
🔴 retired openai/gpt-4-0314 retires 2024-06-13 → openai/gpt-4-1 (2)
|
|
44
|
+
🟠 retiring anthropic/claude-opus-4 retires 2026-06-15 → anthropic/claude-opus-4-7 (1)
|
|
45
|
+
🟢 ok openai/gpt-5 (3)
|
|
46
|
+
|
|
47
|
+
⚠ 2 model(s) need attention before they retire.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Works **fully offline** after the first run (cached snapshot at `~/.config/llmstatus/registry-cache.json`).
|
|
51
|
+
|
|
52
|
+
### Sign in for cloud features
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
mm login # browser sign-in, polls for completion
|
|
56
|
+
mm scan # scans + uploads to your account's inventory
|
|
57
|
+
mm # launches the TUI: inventory, scan, what's-new, alerts
|
|
58
|
+
mm upgrade # Stripe checkout for Pro (alerting)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
You get two binaries — `mm` (short) and `llmstatus` (descriptive). Same binary, take your pick.
|
|
62
|
+
|
|
63
|
+
## Commands
|
|
64
|
+
|
|
65
|
+
| Command | What it does |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `mm status [dir]` | Free offline model-health check — no account |
|
|
68
|
+
| `mm` | Launch the TUI (inventory, scan, what's-new, alerts, account) |
|
|
69
|
+
| `mm login [api_key]` | Browser sign-in with polling (or paste a key) |
|
|
70
|
+
| `mm signup` | Create an account in the browser |
|
|
71
|
+
| `mm scan [dir]` | Scan for model usage; interactive TUI, or `--ci`/`--json` for pipelines |
|
|
72
|
+
| `mm sources` | List detection sources and whether each can run here |
|
|
73
|
+
| `mm upgrade` | Open Stripe checkout, poll until Pro is active |
|
|
74
|
+
| `mm logout` | Forget the saved API key |
|
|
75
|
+
|
|
76
|
+
**Scan sources** (`--sources`, default `filesystem`; `all` for everything):
|
|
77
|
+
|
|
78
|
+
| Source | Reads from |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `filesystem` | repo files |
|
|
81
|
+
| `env` | live process env vars (`OPENAI_API_KEY`, …) |
|
|
82
|
+
| `aws-secrets` | AWS Secrets Manager + SSM |
|
|
83
|
+
| `k8s` | kubectl secrets + configmaps |
|
|
84
|
+
| `helm` | helm release values |
|
|
85
|
+
| `sql` | psql, via `--db <dsn>` |
|
|
86
|
+
|
|
87
|
+
Secret sources shell out to your already-authenticated CLIs, run **read-only**, REDACT every snippet, and only ever upload model ids — secret *values* never leave your machine. Use `--dry-run` to preview.
|
|
88
|
+
|
|
89
|
+
**Common flags:** `--api <url>` · `--key <key>` · `--project <id|name>` · `--yes` · `--json` · `--ci` · `--dry-run` · `--sources <list>` · `--region <r>` · `--namespace <ns>` · `--kube-context <c>` · `--db <dsn>` · `--sql-table <t>`
|
|
90
|
+
|
|
91
|
+
## Free vs paid
|
|
92
|
+
|
|
93
|
+
| | Free (this CLI) | Pro (signed in) |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `mm status` on any repo | ✓ unlimited | ✓ |
|
|
96
|
+
| Signed registry snapshot, offline cache | ✓ | ✓ |
|
|
97
|
+
| Resolve + health locally, on-device | ✓ | ✓ |
|
|
98
|
+
| Secret-source aware (`env`, `aws-secrets`, `k8s`, `helm`, `sql`) | ✓ | ✓ |
|
|
99
|
+
| Cloud inventory across projects/teams | — | ✓ |
|
|
100
|
+
| Alerts on deprecations/retirements (email/Slack/SMS) | — | ✓ |
|
|
101
|
+
| CI integrations + web dashboard | — | ✓ |
|
|
102
|
+
|
|
103
|
+
## How the registry distribution works
|
|
104
|
+
|
|
105
|
+
The registry is published as a **date-versioned, signed snapshot** on Cloudflare R2 at `cdn.llmstatus.ai`, with a mini-TUF trust chain:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
pinned root key (in the CLI binary)
|
|
109
|
+
→ root-signs ─→ keys.json (names the current signing key)
|
|
110
|
+
→ signing-key-signs ─→ latest.json (pointer, with sha256)
|
|
111
|
+
→ blob: <version>.json (immutable)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
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
|
+
|
|
116
|
+
## Links
|
|
117
|
+
|
|
118
|
+
- Website: [llmstatus.ai](https://llmstatus.ai)
|
|
119
|
+
- Pricing: [llmstatus.ai/pricing](https://llmstatus.ai/pricing)
|
|
120
|
+
- Contact / bugs: [it@llmstatus.ai](mailto:it@llmstatus.ai)
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
[MIT](LICENSE) © LLM Status
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@modelstatus/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"llm",
|
|
7
|
+
"ai",
|
|
8
|
+
"models",
|
|
9
|
+
"deprecation",
|
|
10
|
+
"retirement",
|
|
11
|
+
"registry",
|
|
12
|
+
"openai",
|
|
13
|
+
"anthropic",
|
|
14
|
+
"claude",
|
|
15
|
+
"gpt",
|
|
16
|
+
"gemini",
|
|
17
|
+
"mistral",
|
|
18
|
+
"cli",
|
|
19
|
+
"tui",
|
|
20
|
+
"monitoring",
|
|
21
|
+
"lifecycle"
|
|
22
|
+
],
|
|
23
|
+
"homepage": "https://llmstatus.ai",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"email": "it@llmstatus.ai"
|
|
26
|
+
},
|
|
27
|
+
"author": "LLM Status <it@llmstatus.ai>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"type": "module",
|
|
30
|
+
"bin": {
|
|
31
|
+
"mm": "src/index.js",
|
|
32
|
+
"llmstatus": "src/index.js"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "node --test"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"ink": "^5.0.1",
|
|
50
|
+
"react": "^18.3.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"ink-testing-library": "^4.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Thin client for the LLM Status / Model Manager public API (/api/v1). */
|
|
2
|
+
export function createClient({ apiBase, apiKey }) {
|
|
3
|
+
async function req(method, pathname, body) {
|
|
4
|
+
const res = await fetch(`${apiBase}/api/v1${pathname}`, {
|
|
5
|
+
method,
|
|
6
|
+
headers: {
|
|
7
|
+
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
8
|
+
...(body ? { "content-type": "application/json" } : {}),
|
|
9
|
+
},
|
|
10
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
11
|
+
});
|
|
12
|
+
const text = await res.text();
|
|
13
|
+
let data;
|
|
14
|
+
try {
|
|
15
|
+
data = text ? JSON.parse(text) : {};
|
|
16
|
+
} catch {
|
|
17
|
+
data = { raw: text };
|
|
18
|
+
}
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const msg = data?.error?.message || data?.message || `HTTP ${res.status}`;
|
|
21
|
+
const err = new Error(`${method} ${pathname} → ${msg}`);
|
|
22
|
+
err.status = res.status;
|
|
23
|
+
err.code = data?.error?.code;
|
|
24
|
+
err.body = data;
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const qs = (params = {}) => {
|
|
31
|
+
const u = new URLSearchParams();
|
|
32
|
+
for (const [k, v] of Object.entries(params)) if (v != null && v !== "") u.set(k, String(v));
|
|
33
|
+
const s = u.toString();
|
|
34
|
+
return s ? `?${s}` : "";
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
raw: req,
|
|
39
|
+
|
|
40
|
+
// account / auth
|
|
41
|
+
me: () => req("GET", "/me"),
|
|
42
|
+
|
|
43
|
+
// device auth (no key required)
|
|
44
|
+
authStart: (body) => req("POST", "/cli-auth/start", body),
|
|
45
|
+
authPoll: (deviceCode) => req("POST", "/cli-auth/poll", { device_code: deviceCode }),
|
|
46
|
+
|
|
47
|
+
// registry (shared, read-only)
|
|
48
|
+
detectionPatterns: () => req("GET", "/registry/detection-patterns"),
|
|
49
|
+
resolve: (strings) => req("POST", "/registry/resolve", { strings }),
|
|
50
|
+
registryEvents: (since) => req("GET", `/registry/events${qs({ since })}`),
|
|
51
|
+
registryModels: (params) => req("GET", `/registry/models${qs(params)}`),
|
|
52
|
+
|
|
53
|
+
// projects
|
|
54
|
+
listProjects: () => req("GET", "/projects"),
|
|
55
|
+
createProject: (name) => req("POST", "/projects", { name }),
|
|
56
|
+
patchProject: (id, body) => req("PATCH", `/projects/${id}`, body),
|
|
57
|
+
|
|
58
|
+
// usages
|
|
59
|
+
listUsages: (params) => req("GET", `/usages${qs(params)}`),
|
|
60
|
+
getUsage: (id) => req("GET", `/usages/${id}`),
|
|
61
|
+
createUsage: (body) => req("POST", "/usages", body),
|
|
62
|
+
patchUsage: (id, body) => req("PATCH", `/usages/${id}`, body),
|
|
63
|
+
deleteUsage: (id) => req("DELETE", `/usages/${id}`),
|
|
64
|
+
linkUsage: (id, modelId) => req("POST", `/usages/${id}/link`, { model_id: modelId }),
|
|
65
|
+
bulkUpload: (projectId, usages) => req("POST", "/usages/bulk", { project_id: projectId, usages }),
|
|
66
|
+
|
|
67
|
+
// notification rules
|
|
68
|
+
listRules: () => req("GET", "/notification-rules"),
|
|
69
|
+
createRule: (body) => req("POST", "/notification-rules", body),
|
|
70
|
+
patchRule: (id, body) => req("PATCH", `/notification-rules/${id}`, body),
|
|
71
|
+
deleteRule: (id) => req("DELETE", `/notification-rules/${id}`),
|
|
72
|
+
|
|
73
|
+
// channels
|
|
74
|
+
listChannels: () => req("GET", "/channels"),
|
|
75
|
+
createChannel: (body) => req("POST", "/channels", { action: "connect", ...body }),
|
|
76
|
+
testChannel: (body) => req("POST", "/channels", { action: "test", ...body }),
|
|
77
|
+
|
|
78
|
+
// notifications feed
|
|
79
|
+
listNotifications: (params) => req("GET", `/notifications${qs(params)}`),
|
|
80
|
+
readNotification: (id) => req("POST", `/notifications/${id}/read`),
|
|
81
|
+
|
|
82
|
+
// billing
|
|
83
|
+
checkout: () => req("POST", "/billing/checkout"),
|
|
84
|
+
};
|
|
85
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createClient } from "./api.js";
|
|
2
|
+
import { loadConfig, saveConfig, configFilePath } from "./config.js";
|
|
3
|
+
import { openUrl } from "./openUrl.js";
|
|
4
|
+
|
|
5
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6
|
+
|
|
7
|
+
/** Browser login with polling, à la Claude Code: start a device request, open
|
|
8
|
+
* the browser, then poll until the user authorizes (or signs up). Returns
|
|
9
|
+
* { apiKey, account } and saves it to the config file. */
|
|
10
|
+
export async function loginViaBrowser({ apiBase, signup = false, log = console.error }) {
|
|
11
|
+
const client = createClient({ apiBase });
|
|
12
|
+
const start = await client.authStart({ client_name: "mm CLI", signup });
|
|
13
|
+
|
|
14
|
+
log(`\n Opening your browser to authorize this device…`);
|
|
15
|
+
log(` If it doesn't open, visit:\n ${start.verification_url}`);
|
|
16
|
+
log(` Verification code: ${start.user_code}\n`);
|
|
17
|
+
openUrl(start.verification_url);
|
|
18
|
+
|
|
19
|
+
const interval = Math.max(1, start.interval || 3) * 1000;
|
|
20
|
+
const deadline = Date.now() + (start.expires_in || 600) * 1000;
|
|
21
|
+
|
|
22
|
+
log(` Waiting for authorization`);
|
|
23
|
+
while (Date.now() < deadline) {
|
|
24
|
+
await sleep(interval);
|
|
25
|
+
let res;
|
|
26
|
+
try {
|
|
27
|
+
res = await client.authPoll(start.device_code);
|
|
28
|
+
} catch {
|
|
29
|
+
continue; // transient network error — keep polling
|
|
30
|
+
}
|
|
31
|
+
if (res.status === "approved") {
|
|
32
|
+
const cfg = loadConfig();
|
|
33
|
+
cfg.apiKey = res.api_key;
|
|
34
|
+
cfg.apiBase = apiBase;
|
|
35
|
+
saveConfig(cfg);
|
|
36
|
+
log(`\n ✓ Authorized${res.account?.name ? ` as "${res.account.name}"` : ""}. Saved to ${configFilePath}\n`);
|
|
37
|
+
return { apiKey: res.api_key, account: res.account ?? null };
|
|
38
|
+
}
|
|
39
|
+
if (res.status === "denied") throw new Error("Authorization was denied.");
|
|
40
|
+
if (res.status === "expired") throw new Error("The login request expired. Run `mm login` again.");
|
|
41
|
+
process.stderr.write(".");
|
|
42
|
+
}
|
|
43
|
+
throw new Error("Timed out waiting for authorization. Run `mm login` again.");
|
|
44
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const dir = path.join(os.homedir(), ".config", "llmstatus");
|
|
6
|
+
const file = path.join(dir, "config.json");
|
|
7
|
+
// Legacy location (pre-rebrand) — read as a fallback so existing logins keep working.
|
|
8
|
+
const legacyFile = path.join(os.homedir(), ".config", "modelmanager", "config.json");
|
|
9
|
+
|
|
10
|
+
export function loadConfig() {
|
|
11
|
+
for (const f of [file, legacyFile]) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(fs.readFileSync(f, "utf8"));
|
|
14
|
+
} catch {
|
|
15
|
+
/* try next */
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function saveConfig(next) {
|
|
22
|
+
// The API key lives here in plaintext, so keep it owner-only (like gh/aws creds)
|
|
23
|
+
// — other local users must not be able to read it. chmod after write also fixes
|
|
24
|
+
// a pre-existing loose-perm file (writeFileSync's mode only applies on create).
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
26
|
+
fs.writeFileSync(file, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
27
|
+
try {
|
|
28
|
+
fs.chmodSync(file, 0o600);
|
|
29
|
+
} catch {
|
|
30
|
+
/* best-effort (e.g. Windows) */
|
|
31
|
+
}
|
|
32
|
+
return file;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolve {apiBase, apiKey} from flags > env > config file. Defaults to prod. */
|
|
36
|
+
export function resolveAuth(flags = {}) {
|
|
37
|
+
const cfg = loadConfig();
|
|
38
|
+
const apiBase =
|
|
39
|
+
flags.api ||
|
|
40
|
+
process.env.LLMSTATUS_API_BASE ||
|
|
41
|
+
process.env.MM_API_BASE ||
|
|
42
|
+
cfg.apiBase ||
|
|
43
|
+
"https://llmstatus.ai";
|
|
44
|
+
const apiKey =
|
|
45
|
+
flags.key || process.env.LLMSTATUS_API_KEY || process.env.MM_API_KEY || cfg.apiKey || "";
|
|
46
|
+
return { apiBase: apiBase.replace(/\/$/, ""), apiKey };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Forget the saved API key (keeps apiBase). */
|
|
50
|
+
export function clearAuth() {
|
|
51
|
+
const cfg = loadConfig();
|
|
52
|
+
delete cfg.apiKey;
|
|
53
|
+
saveConfig(cfg);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Persist a single key (e.g. lastEventsSeenAt for the "what's new" feed). */
|
|
57
|
+
export function setConfigValue(key, value) {
|
|
58
|
+
const cfg = loadConfig();
|
|
59
|
+
cfg[key] = value;
|
|
60
|
+
saveConfig(cfg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const configFilePath = file;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Pure model-string detector shared by every scan source (filesystem now,
|
|
2
|
+
* secrets/k8s/helm/sql later). Given text + the registry's detection patterns,
|
|
3
|
+
* returns the model strings found per line. No I/O. */
|
|
4
|
+
|
|
5
|
+
// File extensions / TLDs the family globs accidentally swallow
|
|
6
|
+
// (e.g. "command-2.0.0.tgz", "grok-free.app"). Used to reject generic matches.
|
|
7
|
+
const BANNED_TAIL =
|
|
8
|
+
/\.(tgz|tar|gz|zip|js|ts|tsx|jsx|mjs|py|go|rb|json|md|lock|sh|css|html|txt|log|yaml|yml|toml|ini|conf|cfg|env|pem|crt|key|csv|xml|pdf|sql|app|com|net|io|dev|org|ai|co)\b/;
|
|
9
|
+
|
|
10
|
+
/** Trim leading/trailing separators a greedy family glob can capture. */
|
|
11
|
+
function cleanGeneric(s) {
|
|
12
|
+
return s.replace(/^[.\-_]+/, "").replace(/[.\-_]+$/, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Family globs catch brand-NEW versioned models before they're in the
|
|
16
|
+
* registry, so a real hit virtually always carries a version digit. Requiring
|
|
17
|
+
* one (and rejecting filename/domain tails) kills the bulk of false positives. */
|
|
18
|
+
function looksLikeModel(s) {
|
|
19
|
+
return s.length >= 5 && /[0-9]/.test(s) && !BANNED_TAIL.test(s);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Compile detection patterns into reusable matchers (do this once per scan). */
|
|
23
|
+
export function compilePatterns(patterns) {
|
|
24
|
+
const exact = [];
|
|
25
|
+
for (const ms of patterns.model_strings || []) {
|
|
26
|
+
if (ms.match && ms.match.length >= 4) exact.push(ms.match.toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
const generic = (patterns.generic_model_regexes || []).map((r) => new RegExp(r, "gi"));
|
|
29
|
+
return { exact, generic };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Find model strings on a single line. Returns a Set of lowercased strings. */
|
|
33
|
+
export function detectInLine(line, compiled) {
|
|
34
|
+
const lower = line.toLowerCase();
|
|
35
|
+
const found = new Set();
|
|
36
|
+
for (const s of compiled.exact) if (lower.includes(s)) found.add(s);
|
|
37
|
+
for (const re of compiled.generic) {
|
|
38
|
+
re.lastIndex = 0;
|
|
39
|
+
let m;
|
|
40
|
+
while ((m = re.exec(line))) {
|
|
41
|
+
const cand = cleanGeneric(m[0].toLowerCase());
|
|
42
|
+
if (looksLikeModel(cand)) found.add(cand);
|
|
43
|
+
if (re.lastIndex === m.index) re.lastIndex++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Detect across multi-line text → [{ model_string, line, snippet }]. */
|
|
50
|
+
export function detectInText(text, compiled) {
|
|
51
|
+
const out = [];
|
|
52
|
+
text.split(/\r?\n/).forEach((line, idx) => {
|
|
53
|
+
for (const modelStr of detectInLine(line, compiled)) {
|
|
54
|
+
out.push({ model_string: modelStr, line: idx + 1, snippet: line.trim().slice(0, 160) });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return out;
|
|
58
|
+
}
|