claudeup 6.7.1 → 6.8.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/package.json +4 -4
- package/src/__tests__/mate-availability.test.ts +156 -0
- package/src/__tests__/mate-catalog.test.ts +295 -0
- package/src/__tests__/model-visuals.test.tsx +1698 -25
- package/src/__tests__/models-adapter.test.ts +21 -6
- package/src/__tests__/models-cli.test.ts +100 -0
- package/src/__tests__/models-core.test.ts +273 -111
- package/src/__tests__/models-manager.test.ts +15 -12
- package/src/__tests__/models-presets-marketplace.test.ts +29 -2
- package/src/__tests__/models-screen-state.test.ts +57 -1
- package/src/cli/doctor.ts +8 -13
- package/src/cli/models.ts +97 -13
- package/src/data/models-presets.ts +62 -2
- package/src/services/mate-availability.ts +133 -0
- package/src/services/mate-catalog.ts +265 -0
- package/src/services/models-core.ts +371 -30
- package/src/ui/adapters/modelsAdapter.ts +58 -15
- package/src/ui/components/layout/ScreenLayout.tsx +6 -1
- package/src/ui/renderers/modelRenderers.tsx +413 -108
- package/src/ui/renderers/modelVisuals.tsx +694 -145
- package/src/ui/screens/ModelsScreen.tsx +74 -10
- package/src/ui/state/reducer.ts +20 -0
- package/src/ui/state/types.ts +31 -0
- package/src/ui/theme-mode.ts +128 -14
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The LIVE external-model catalogue, read from claudish.
|
|
3
|
+
*
|
|
4
|
+
* A slot is bound to a bare catalogue id (`grok-4.6`). This module answers one question about
|
|
5
|
+
* such an id — is it still in the catalogue — and does nothing else with the answer.
|
|
6
|
+
*
|
|
7
|
+
* ## Why there is no table of model ids in this repo
|
|
8
|
+
*
|
|
9
|
+
* There was one once, elsewhere, and it went four months stale and silently resolved dead
|
|
10
|
+
* ids. A committed snapshot of a catalogue that turns over weekly is not a cache, it is a
|
|
11
|
+
* liability with a plausible face: it keeps answering after it stops being right. So the ids
|
|
12
|
+
* come from the catalogue at call time or they do not come at all.
|
|
13
|
+
*
|
|
14
|
+
* ## Why `claudish --models` and nothing else
|
|
15
|
+
*
|
|
16
|
+
* It is a READ-ONLY DIAGNOSTIC. The rule in this project is that work goes through claudish's
|
|
17
|
+
* MCP tools, never its CLI — with exactly three exceptions, `--help`, `--version` and
|
|
18
|
+
* `--models`, none of which may run a task. This module uses one of the three and runs no
|
|
19
|
+
* task: it prints a table and we read it. claudeup is a TUI with no MCP client in it, so the
|
|
20
|
+
* MCP route is not available here at all; if it were, this would still be the right call,
|
|
21
|
+
* because listing a catalogue IS the diagnostic those three exist for.
|
|
22
|
+
*
|
|
23
|
+
* ## The contract this module holds to
|
|
24
|
+
*
|
|
25
|
+
* FAILS SILENT, ALWAYS. claudish absent, a non-zero exit, unparseable output, a timeout —
|
|
26
|
+
* every one of them returns an empty catalogue, which reads downstream as "unknown". Never
|
|
27
|
+
* throws, never blocks, never empties the Model column because a lookup failed. The screen
|
|
28
|
+
* with no catalogue is the screen showing every bound id exactly as written, unannotated,
|
|
29
|
+
* which is what it showed before this module existed.
|
|
30
|
+
*
|
|
31
|
+
* ADVISORY ONLY. It marks a bound id the catalogue does not list. It never rebinds a slot,
|
|
32
|
+
* never substitutes a model, never fails a validation. This is deliberately the same contract
|
|
33
|
+
* as the image-model staleness check in the repo's CLAUDE.md, and for the same reason: a
|
|
34
|
+
* check that can act on a catalogue it may have failed to read can act on nothing.
|
|
35
|
+
*
|
|
36
|
+
* ## Shape
|
|
37
|
+
*
|
|
38
|
+
* Mirrors `mate-availability.ts`: a pure decision, plus an injected seam for the thing that
|
|
39
|
+
* touches the machine. The PARSING and the DECISIONS are what is worth testing — a format
|
|
40
|
+
* change, a truncated table, an empty result — and none of them can be tested through a
|
|
41
|
+
* function that shells out. The default runner is the real one, so no caller needs to know
|
|
42
|
+
* the seam is there.
|
|
43
|
+
*
|
|
44
|
+
* This is NOT in `models-core.ts`. That module is pure — no fs, no process, no network —
|
|
45
|
+
* because Claude Code runs the hook that imports it on every single Agent call.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { spawn } from "node:child_process";
|
|
49
|
+
|
|
50
|
+
/** One row of `claudish --models`, as printed. */
|
|
51
|
+
export interface CatalogModel {
|
|
52
|
+
/** The bare catalogue id — the only field a binding stores. */
|
|
53
|
+
id: string;
|
|
54
|
+
provider: string;
|
|
55
|
+
/** As printed: `$4.00/1M`, or `FREE`. Kept as text; nothing here does arithmetic on it. */
|
|
56
|
+
pricing: string;
|
|
57
|
+
/** As printed: `1M`, `500K`, `N/A`. */
|
|
58
|
+
context: string;
|
|
59
|
+
/** As printed: `TRV`, `TR`, `·`. */
|
|
60
|
+
caps: string;
|
|
61
|
+
/** ISO date, or "" when the table printed an em dash for an unknown release. */
|
|
62
|
+
released: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** How the catalogue is fetched. A seam — see the header. */
|
|
66
|
+
export type CatalogFetcher = () => Promise<string | null>;
|
|
67
|
+
|
|
68
|
+
/** Milliseconds before a wedged claudish is abandoned. It is drawing three rows, not a page. */
|
|
69
|
+
export const CATALOG_TIMEOUT_MS = 8000;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A table row: rank, id, provider, pricing, context, caps, released.
|
|
73
|
+
*
|
|
74
|
+
* Every field claudish prints is whitespace-free — providers are slugs (`thinking-machines`),
|
|
75
|
+
* pricing has no spaces (`$4.00/1M`, `FREE`), caps is a letter run or `·`, and an unknown
|
|
76
|
+
* release date is a single em dash. So a row is EXACTLY seven tokens with a number first, and
|
|
77
|
+
* that is the whole rule.
|
|
78
|
+
*
|
|
79
|
+
* Requiring exactly seven is the defensive choice, not a brittle one. If claudish adds a
|
|
80
|
+
* column, every row stops matching, the catalogue comes back empty, and empty means
|
|
81
|
+
* "unknown" — which suppresses the advisory note and changes nothing else on screen. The
|
|
82
|
+
* alternative, accepting `>= 7` and taking the first seven, would keep matching while
|
|
83
|
+
* silently reading the wrong field into `id` and marking live models as retired.
|
|
84
|
+
*/
|
|
85
|
+
const ROW = /^\s*\d+\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$/;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Strip SGR sequences.
|
|
89
|
+
*
|
|
90
|
+
* MEASURED: `claudish --models | cat` emits none, so on the path this module actually takes
|
|
91
|
+
* (a pipe, never a TTY) there is nothing to strip. Kept anyway, because claudish is free to
|
|
92
|
+
* start colouring a piped table, and an ESC left in the string would ride into an `id` and
|
|
93
|
+
* make a live model look retired — the exact false positive this module must never produce.
|
|
94
|
+
*
|
|
95
|
+
* The ESC is written as an escape rather than as the raw byte it first was: a literal control
|
|
96
|
+
* character in source survives no reformat and no copy-paste, and losing it silently turns
|
|
97
|
+
* this into a function that strips square brackets out of model ids.
|
|
98
|
+
*/
|
|
99
|
+
function stripAnsi(text: string): string {
|
|
100
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: an SGR sequence starts with ESC
|
|
101
|
+
return text.replace(/\u001b\[[0-9;]*m/g, "");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse the table `claudish --models` prints. Never throws.
|
|
106
|
+
*
|
|
107
|
+
* Non-row lines fall out on their own rather than needing a section parser: the header starts
|
|
108
|
+
* with `#`, the rule is one long dash run, the `Caps:` legend and the local-provider block
|
|
109
|
+
* have the wrong token count, and the footer lines start with words. A section-aware parser
|
|
110
|
+
* would have to be taught each of those and would break when one moved.
|
|
111
|
+
*/
|
|
112
|
+
export function parseModelsTable(text: string): CatalogModel[] {
|
|
113
|
+
const out: CatalogModel[] = [];
|
|
114
|
+
const seen = new Set<string>();
|
|
115
|
+
for (const line of stripAnsi(text).split("\n")) {
|
|
116
|
+
const m = ROW.exec(line);
|
|
117
|
+
if (!m) continue;
|
|
118
|
+
const [, id, provider, pricing, context, caps, released] = m;
|
|
119
|
+
if (!id || seen.has(id)) continue;
|
|
120
|
+
seen.add(id);
|
|
121
|
+
out.push({
|
|
122
|
+
id,
|
|
123
|
+
provider: provider ?? "",
|
|
124
|
+
pricing: pricing ?? "",
|
|
125
|
+
context: context ?? "",
|
|
126
|
+
caps: caps ?? "",
|
|
127
|
+
// An em dash means claudish does not know the date. "" says that; carrying the
|
|
128
|
+
// dash through would put a glyph in a field every reader treats as a date.
|
|
129
|
+
released: released === "—" ? "" : (released ?? ""),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Run `claudish --models` and hand back its stdout, or null.
|
|
137
|
+
*
|
|
138
|
+
* Null for every failure mode there is: no claudish on PATH (`error`), a non-zero exit, or a
|
|
139
|
+
* process still running after the timeout. The caller cannot tell them apart and does not
|
|
140
|
+
* need to — all three mean the catalogue is unknown, and unknown has exactly one behaviour.
|
|
141
|
+
*/
|
|
142
|
+
const runClaudishModels: CatalogFetcher = () =>
|
|
143
|
+
new Promise((resolve) => {
|
|
144
|
+
let settled = false;
|
|
145
|
+
const done = (value: string | null) => {
|
|
146
|
+
if (settled) return;
|
|
147
|
+
settled = true;
|
|
148
|
+
resolve(value);
|
|
149
|
+
};
|
|
150
|
+
let child: ReturnType<typeof spawn>;
|
|
151
|
+
try {
|
|
152
|
+
child = spawn("claudish", ["--models"], {
|
|
153
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
// `spawn` throws rather than emitting `error` for some argv/permission failures.
|
|
157
|
+
done(null);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
let out = "";
|
|
161
|
+
child.stdout?.on("data", (chunk) => {
|
|
162
|
+
out += String(chunk);
|
|
163
|
+
});
|
|
164
|
+
child.on("error", () => done(null));
|
|
165
|
+
child.on("close", (code) => done(code === 0 ? out : null));
|
|
166
|
+
// Never let a wedged claudish hold the TUI. `unref` so a pending timer cannot keep
|
|
167
|
+
// the CLI's process alive after it has printed everything it means to print.
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
child.kill();
|
|
170
|
+
done(null);
|
|
171
|
+
}, CATALOG_TIMEOUT_MS).unref?.();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Session cache, including the empty result.
|
|
176
|
+
*
|
|
177
|
+
* Caching a FAILURE is deliberate. The catalogue cannot change while claudeup is open, and a
|
|
178
|
+
* machine with no claudish would otherwise pay the spawn — and its timeout — once per screen
|
|
179
|
+
* that asks. One failed lookup answers for the session, which is the same reasoning
|
|
180
|
+
* `content-drift.ts` uses for its own session cache.
|
|
181
|
+
*/
|
|
182
|
+
let cached: CatalogModel[] | null = null;
|
|
183
|
+
let inFlight: Promise<CatalogModel[]> | null = null;
|
|
184
|
+
|
|
185
|
+
/** Drop the cached catalogue. For tests, and for an explicit reload. */
|
|
186
|
+
export function clearMateCatalogCache(): void {
|
|
187
|
+
cached = null;
|
|
188
|
+
inFlight = null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The live catalogue, or [] when it could not be read.
|
|
193
|
+
*
|
|
194
|
+
* [] is not distinguished from "claudish returned nothing", by design: both mean the same
|
|
195
|
+
* thing to every caller, and a tri-state would invite someone to branch on it and then have
|
|
196
|
+
* to decide what an error should do — which is the decision this module exists to refuse.
|
|
197
|
+
*
|
|
198
|
+
* Concurrent callers share one in-flight promise. Both screens on this tab ask at mount, and
|
|
199
|
+
* two spawns of the same read-only diagnostic is pure waste.
|
|
200
|
+
*/
|
|
201
|
+
export async function loadMateCatalog(
|
|
202
|
+
fetch: CatalogFetcher = runClaudishModels,
|
|
203
|
+
): Promise<CatalogModel[]> {
|
|
204
|
+
if (cached !== null) return cached;
|
|
205
|
+
if (inFlight) return inFlight;
|
|
206
|
+
inFlight = (async () => {
|
|
207
|
+
let models: CatalogModel[] = [];
|
|
208
|
+
try {
|
|
209
|
+
const text = await fetch();
|
|
210
|
+
if (text) models = parseModelsTable(text);
|
|
211
|
+
} catch {
|
|
212
|
+
// A thrown fetcher is a failed lookup like any other. Nothing above this line is
|
|
213
|
+
// allowed to see an exception from a catalogue read.
|
|
214
|
+
models = [];
|
|
215
|
+
}
|
|
216
|
+
cached = models;
|
|
217
|
+
inFlight = null;
|
|
218
|
+
return models;
|
|
219
|
+
})();
|
|
220
|
+
return inFlight;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Every id the catalogue lists. The shape the staleness check and the UI actually want. */
|
|
224
|
+
export function catalogIds(models: CatalogModel[]): Set<string> {
|
|
225
|
+
return new Set(models.map((model) => model.id));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Which of these bound ids the catalogue does not list.
|
|
230
|
+
*
|
|
231
|
+
* RETURNS NOTHING WHEN THE CATALOGUE IS EMPTY. That is the whole contract in one line: an
|
|
232
|
+
* unread catalogue lists nothing, so a naive "not in the set" test would mark every binding
|
|
233
|
+
* on the machine as retired the moment claudish was missing — an advisory that fires hardest
|
|
234
|
+
* exactly when it knows least. Same rule as the image-model staleness check: return nothing
|
|
235
|
+
* when you cannot reach the catalogue.
|
|
236
|
+
*
|
|
237
|
+
* Exact match only. A near-match would be the failure this is meant to catch, wearing a
|
|
238
|
+
* helpful face: `grok-4.5` is not a stand-in for a retired `grok-4.6`.
|
|
239
|
+
*/
|
|
240
|
+
export function unknownBindings(
|
|
241
|
+
ids: Iterable<string>,
|
|
242
|
+
catalog: CatalogModel[],
|
|
243
|
+
): string[] {
|
|
244
|
+
if (catalog.length === 0) return [];
|
|
245
|
+
const known = catalogIds(catalog);
|
|
246
|
+
const out: string[] = [];
|
|
247
|
+
for (const id of ids) {
|
|
248
|
+
if (!known.has(id) && !out.includes(id)) out.push(id);
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The advisory sentence, or null when there is nothing to say.
|
|
255
|
+
*
|
|
256
|
+
* Worded as an observation, never as an instruction: claudeup has not checked whether the
|
|
257
|
+
* model was renamed, retired, or simply sits outside the top 100 rows this diagnostic prints
|
|
258
|
+
* — and `--models` is a TOP-N view, so absence is genuinely weaker evidence than it looks.
|
|
259
|
+
* "may no longer exist" is the strongest claim the evidence supports.
|
|
260
|
+
*/
|
|
261
|
+
export function stalenessNote(unknown: string[]): string | null {
|
|
262
|
+
if (unknown.length === 0) return null;
|
|
263
|
+
const list = unknown.join(", ");
|
|
264
|
+
return `${list} ${unknown.length === 1 ? "is" : "are"} not in claudish's live catalogue — the id may have changed, or it may sit outside the models it lists. Check with \`claudish --models\`.`;
|
|
265
|
+
}
|