claudeup 6.6.0 → 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__/conventions-manager.test.ts +45 -45
- package/src/__tests__/enabled-not-installed.test.ts +1 -1
- 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 +168 -0
- package/src/__tests__/models-screen-state.test.ts +57 -1
- package/src/__tests__/plugin-manager-fallback.test.ts +3 -5
- package/src/__tests__/resolver.test.ts +3 -3
- package/src/cli/doctor.ts +8 -13
- package/src/cli/models.ts +97 -13
- package/src/data/models-presets.ts +72 -13
- package/src/data/predefined-profiles.ts +7 -7
- package/src/services/claude-settings.ts +1 -1
- package/src/services/community-styles.ts +1 -1
- 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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading claudish's live model catalogue, and the one rule that matters most about it.
|
|
3
|
+
*
|
|
4
|
+
* This module's whole job is to be ADVISORY and to FAIL SILENT. So the tests that earn their
|
|
5
|
+
* place are the negative ones: what happens when claudish is missing, when it exits non-zero,
|
|
6
|
+
* when it prints a table this build has never seen. Every one of those has to end in "the
|
|
7
|
+
* catalogue is unknown" and nothing else — an advisory that fires hardest when it knows least
|
|
8
|
+
* is worse than no advisory at all.
|
|
9
|
+
*
|
|
10
|
+
* The fixture below is REAL output, captured from `claudish --models` (v9.3.0) rather than
|
|
11
|
+
* written from memory of the format. A parser tested against its author's idea of the format
|
|
12
|
+
* passes forever and tells you nothing about the format.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, test } from "bun:test";
|
|
15
|
+
import {
|
|
16
|
+
type CatalogModel,
|
|
17
|
+
catalogIds,
|
|
18
|
+
clearMateCatalogCache,
|
|
19
|
+
loadMateCatalog,
|
|
20
|
+
parseModelsTable,
|
|
21
|
+
stalenessNote,
|
|
22
|
+
unknownBindings,
|
|
23
|
+
} from "../services/mate-catalog.js";
|
|
24
|
+
|
|
25
|
+
/** Verbatim `claudish --models`, trimmed in the middle. Header, rule, rows, legend, footer. */
|
|
26
|
+
const REAL_OUTPUT = `
|
|
27
|
+
Top 100 models from Firebase (pool: 384 eligible)
|
|
28
|
+
|
|
29
|
+
# Model Provider Pricing Context Caps Released
|
|
30
|
+
──────────────────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
4 deepseek-v4.1-flash deepseek $0.75/1M 1M TRV 2026-09-10
|
|
32
|
+
1 gpt-6-astra openai $30.00/1M 1M TRV 2026-09-03
|
|
33
|
+
3 gemini-3.8-flash google $2.25/1M 1M TRV 2026-09-02
|
|
34
|
+
57 grok-4.6 x-ai $4.00/1M 500K TRV 2026-08-12
|
|
35
|
+
15 kimi-k3 moonshotai $9.00/1M 1M TRV 2026-07-16
|
|
36
|
+
8 glm-5.3-fp8 z-ai FREE 1M TR 2026-08-14
|
|
37
|
+
55 inkling-small thinking-machines $0.85/1M 1M TRV 2026-07-30
|
|
38
|
+
68 mimo-v2.5 xiaomi FREE 1M TRV —
|
|
39
|
+
99 codex-mini-latest openai $3.75/1M 200K TRV —
|
|
40
|
+
|
|
41
|
+
Caps: T = tools R = reasoning V = vision
|
|
42
|
+
|
|
43
|
+
Local providers
|
|
44
|
+
──────────────────────────────────────────────────────────────────────
|
|
45
|
+
Ollama: not running
|
|
46
|
+
LiteLLM: not configured (set LITELLM_BASE_URL + LITELLM_API_KEY)
|
|
47
|
+
|
|
48
|
+
Filter by provider: claudish --models --provider <slug>
|
|
49
|
+
All providers: claudish --providers
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const idsOf = (models: CatalogModel[]) => models.map((m) => m.id);
|
|
53
|
+
|
|
54
|
+
describe("parseModelsTable", () => {
|
|
55
|
+
test("reads every model row out of real claudish output", () => {
|
|
56
|
+
expect(idsOf(parseModelsTable(REAL_OUTPUT))).toEqual([
|
|
57
|
+
"deepseek-v4.1-flash",
|
|
58
|
+
"gpt-6-astra",
|
|
59
|
+
"gemini-3.8-flash",
|
|
60
|
+
"grok-4.6",
|
|
61
|
+
"kimi-k3",
|
|
62
|
+
"glm-5.3-fp8",
|
|
63
|
+
"inkling-small",
|
|
64
|
+
"mimo-v2.5",
|
|
65
|
+
"codex-mini-latest",
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("keeps the fields a binding and a picker need", () => {
|
|
70
|
+
const grok = parseModelsTable(REAL_OUTPUT).find((m) => m.id === "grok-4.6");
|
|
71
|
+
expect(grok).toEqual({
|
|
72
|
+
id: "grok-4.6",
|
|
73
|
+
provider: "x-ai",
|
|
74
|
+
pricing: "$4.00/1M",
|
|
75
|
+
context: "500K",
|
|
76
|
+
caps: "TRV",
|
|
77
|
+
released: "2026-08-12",
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("`FREE` is a pricing string like any other, not a missing field", () => {
|
|
82
|
+
const free = parseModelsTable(REAL_OUTPUT).find(
|
|
83
|
+
(m) => m.id === "glm-5.3-fp8",
|
|
84
|
+
);
|
|
85
|
+
expect(free?.pricing).toBe("FREE");
|
|
86
|
+
expect(free?.caps).toBe("TR");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("an em-dash release date becomes empty, not a dash in a date field", () => {
|
|
90
|
+
// Carrying `—` through would put a glyph in a field every reader treats as a date,
|
|
91
|
+
// and anything that formats or sorts it would have to special-case the glyph.
|
|
92
|
+
const undated = parseModelsTable(REAL_OUTPUT).find(
|
|
93
|
+
(m) => m.id === "mimo-v2.5",
|
|
94
|
+
);
|
|
95
|
+
expect(undated?.released).toBe("");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("the legend, the local-provider block and the footer are not models", () => {
|
|
99
|
+
const ids = idsOf(parseModelsTable(REAL_OUTPUT));
|
|
100
|
+
for (const notAModel of [
|
|
101
|
+
"Caps:",
|
|
102
|
+
"Ollama:",
|
|
103
|
+
"LiteLLM:",
|
|
104
|
+
"Local",
|
|
105
|
+
"Filter",
|
|
106
|
+
"Top",
|
|
107
|
+
]) {
|
|
108
|
+
expect(ids).not.toContain(notAModel);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The defensive half of the seven-token rule.
|
|
114
|
+
*
|
|
115
|
+
* If claudish adds a column, every row stops matching and the catalogue comes back empty
|
|
116
|
+
* — which means "unknown", which suppresses the advisory and changes nothing on screen.
|
|
117
|
+
* The alternative (take the first seven of >= 7) would keep matching while reading the
|
|
118
|
+
* wrong field into `id`, and would then report live models as retired. Silence beats
|
|
119
|
+
* confident nonsense.
|
|
120
|
+
*/
|
|
121
|
+
test("an extra column makes the table unreadable rather than misread", () => {
|
|
122
|
+
const eightColumns =
|
|
123
|
+
" 4 deepseek-v4.1-flash deepseek $0.75/1M 1M TRV 2026-09-10 extra";
|
|
124
|
+
expect(parseModelsTable(eightColumns)).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("never throws on junk, however shaped", () => {
|
|
128
|
+
for (const junk of ["", "\n\n\n", "error: command not found", "{}", "—"]) {
|
|
129
|
+
expect(() => parseModelsTable(junk)).not.toThrow();
|
|
130
|
+
expect(parseModelsTable(junk)).toEqual([]);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("strips SGR sequences, so colour cannot ride into an id", () => {
|
|
135
|
+
// Measured: a piped `claudish --models` emits none today. This is about the day it
|
|
136
|
+
// starts colouring a pipe — an ESC left in the string would make a live model look
|
|
137
|
+
// retired, which is the one false positive this module must never produce.
|
|
138
|
+
const coloured =
|
|
139
|
+
" 57 [32mgrok-4.6[0m x-ai $4.00/1M 500K TRV 2026-08-12";
|
|
140
|
+
expect(idsOf(parseModelsTable(coloured))).toEqual(["grok-4.6"]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a repeated id is listed once", () => {
|
|
144
|
+
const twice = `
|
|
145
|
+
57 grok-4.6 x-ai $4.00/1M 500K TRV 2026-08-12
|
|
146
|
+
58 grok-4.6 x-ai $4.00/1M 500K TRV 2026-08-12
|
|
147
|
+
`;
|
|
148
|
+
expect(idsOf(parseModelsTable(twice))).toEqual(["grok-4.6"]);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("unknownBindings", () => {
|
|
153
|
+
const catalog = parseModelsTable(REAL_OUTPUT);
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* THE load-bearing test in this file.
|
|
157
|
+
*
|
|
158
|
+
* An unread catalogue lists nothing, so a naive "not in the set" check would mark every
|
|
159
|
+
* binding on the machine as retired the moment claudish was missing. The advisory has to
|
|
160
|
+
* go quiet exactly when it knows least.
|
|
161
|
+
*/
|
|
162
|
+
test("says nothing at all when the catalogue could not be read", () => {
|
|
163
|
+
expect(unknownBindings(["grok-4.6", "totally-made-up"], [])).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("names an id the catalogue does not list", () => {
|
|
167
|
+
expect(unknownBindings(["grok-4.6", "gpt-4-turbo"], catalog)).toEqual([
|
|
168
|
+
"gpt-4-turbo",
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("says nothing when every binding is live", () => {
|
|
173
|
+
expect(unknownBindings(["grok-4.6", "kimi-k3"], catalog)).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("exact match only — a neighbouring version is not a stand-in", () => {
|
|
177
|
+
// `grok-4.5` resolving a retired `grok-4.6` is the exact failure this exists to
|
|
178
|
+
// catch, wearing a helpful face. Same rule the image-model registry holds to.
|
|
179
|
+
expect(unknownBindings(["grok-4.5"], catalog)).toEqual(["grok-4.5"]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("reports a repeated unknown id once", () => {
|
|
183
|
+
expect(unknownBindings(["nope", "nope"], catalog)).toEqual(["nope"]);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("catalogIds", () => {
|
|
188
|
+
test("is every id, as a set", () => {
|
|
189
|
+
const ids = catalogIds(parseModelsTable(REAL_OUTPUT));
|
|
190
|
+
expect(ids.has("grok-4.6")).toBe(true);
|
|
191
|
+
expect(ids.has("gpt-4-turbo")).toBe(false);
|
|
192
|
+
expect(ids.size).toBe(9);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("stalenessNote", () => {
|
|
197
|
+
test("is null when there is nothing to say", () => {
|
|
198
|
+
expect(stalenessNote([])).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("hedges, because absence from a TOP-N list is weak evidence", () => {
|
|
202
|
+
// `--models` prints the top 100 of a 384-model pool, so a missing id may simply sit
|
|
203
|
+
// below the cut. The note must not claim the model is gone.
|
|
204
|
+
const note = stalenessNote(["gpt-4-turbo"]);
|
|
205
|
+
expect(note).toContain("gpt-4-turbo");
|
|
206
|
+
expect(note).toContain("may");
|
|
207
|
+
expect(note).not.toContain("deleted");
|
|
208
|
+
expect(note).not.toContain("retired");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("agrees with itself about number", () => {
|
|
212
|
+
expect(stalenessNote(["a"])).toContain(" is ");
|
|
213
|
+
expect(stalenessNote(["a", "b"])).toContain(" are ");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("loadMateCatalog", () => {
|
|
218
|
+
test("returns [] when claudish is not there to answer", async () => {
|
|
219
|
+
clearMateCatalogCache();
|
|
220
|
+
expect(await loadMateCatalog(async () => null)).toEqual([]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("returns [] when the fetcher throws, rather than propagating", async () => {
|
|
224
|
+
// Nothing above this module is allowed to see an exception from a catalogue read.
|
|
225
|
+
clearMateCatalogCache();
|
|
226
|
+
expect(
|
|
227
|
+
await loadMateCatalog(async () => {
|
|
228
|
+
throw new Error("claudish exploded");
|
|
229
|
+
}),
|
|
230
|
+
).toEqual([]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("parses what it is handed", async () => {
|
|
234
|
+
clearMateCatalogCache();
|
|
235
|
+
const models = await loadMateCatalog(async () => REAL_OUTPUT);
|
|
236
|
+
expect(idsOf(models)).toContain("grok-4.6");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("caches, so one screen's lookup answers for the session", async () => {
|
|
240
|
+
clearMateCatalogCache();
|
|
241
|
+
let calls = 0;
|
|
242
|
+
const counting = async () => {
|
|
243
|
+
calls += 1;
|
|
244
|
+
return REAL_OUTPUT;
|
|
245
|
+
};
|
|
246
|
+
await loadMateCatalog(counting);
|
|
247
|
+
await loadMateCatalog(counting);
|
|
248
|
+
expect(calls).toBe(1);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* A FAILURE is cached too, and that is deliberate.
|
|
253
|
+
*
|
|
254
|
+
* The catalogue cannot change while claudeup is open, so a machine with no claudish would
|
|
255
|
+
* otherwise pay the spawn — and its full timeout — once per screen that asks.
|
|
256
|
+
*/
|
|
257
|
+
test("caches the failure as well, so a missing claudish is paid for once", async () => {
|
|
258
|
+
clearMateCatalogCache();
|
|
259
|
+
let calls = 0;
|
|
260
|
+
const failing = async () => {
|
|
261
|
+
calls += 1;
|
|
262
|
+
return null;
|
|
263
|
+
};
|
|
264
|
+
await loadMateCatalog(failing);
|
|
265
|
+
await loadMateCatalog(failing);
|
|
266
|
+
expect(calls).toBe(1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("concurrent callers share one lookup", async () => {
|
|
270
|
+
clearMateCatalogCache();
|
|
271
|
+
let calls = 0;
|
|
272
|
+
const slow = async () => {
|
|
273
|
+
calls += 1;
|
|
274
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
275
|
+
return REAL_OUTPUT;
|
|
276
|
+
};
|
|
277
|
+
await Promise.all([loadMateCatalog(slow), loadMateCatalog(slow)]);
|
|
278
|
+
expect(calls).toBe(1);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("clearing the cache means the next call asks again", async () => {
|
|
282
|
+
// The negative control for all four cache tests above: without it, a `loadMateCatalog`
|
|
283
|
+
// that never called its fetcher at all would pass every one of them.
|
|
284
|
+
clearMateCatalogCache();
|
|
285
|
+
let calls = 0;
|
|
286
|
+
const counting = async () => {
|
|
287
|
+
calls += 1;
|
|
288
|
+
return REAL_OUTPUT;
|
|
289
|
+
};
|
|
290
|
+
await loadMateCatalog(counting);
|
|
291
|
+
clearMateCatalogCache();
|
|
292
|
+
await loadMateCatalog(counting);
|
|
293
|
+
expect(calls).toBe(2);
|
|
294
|
+
});
|
|
295
|
+
});
|