@wrongstack/cli 0.306.4 → 0.307.1

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.
Files changed (73) hide show
  1. package/dist/{auth-L3WGSGDX.js → auth-TVOOHL4Q.js} +45 -106
  2. package/dist/boot/short-circuit-flags.d.ts +2 -2
  3. package/dist/chimera-cascade-evidence.d.ts +1 -1
  4. package/dist/chimera-reviewer-policy.d.ts +1 -1
  5. package/dist/chunk-2VMJ3E5Z.js +71 -0
  6. package/dist/{chunk-RHLQT3EV.js → chunk-CB35UVXO.js} +1636 -1528
  7. package/dist/{chunk-YKBRFSGL.js → chunk-DOJLAULS.js} +28 -28
  8. package/dist/{chunk-KXN5HZDL.js → chunk-PKHGLWUJ.js} +24 -6
  9. package/dist/{chunk-HITHEXE3.js → chunk-TOG7OMKJ.js} +2 -2
  10. package/dist/{chunk-ARWGQUB3.js → chunk-XSG33WHN.js} +15 -5
  11. package/dist/{cli-main-4TZCCZR6.js → cli-main-DD3I7KOO.js} +29198 -28411
  12. package/dist/cli-main.d.ts +0 -20
  13. package/dist/{execution-547PM3NI.js → execution-4ASWJ7EY.js} +309 -273
  14. package/dist/fleet/host-learning-scheduler.d.ts +27 -0
  15. package/dist/fleet/host-shadow-manager.d.ts +64 -0
  16. package/dist/fleet/host.d.ts +5 -140
  17. package/dist/{hq-ZK6PGN3W.js → hq-PA6RH2UW.js} +2 -2
  18. package/dist/hq-server/mailbox-gateway-manager.d.ts +32 -0
  19. package/dist/hq-server/routes/auth/bootstrap-routes.d.ts +5 -0
  20. package/dist/hq-server/routes/auth/common.d.ts +11 -0
  21. package/dist/hq-server/routes/auth/password-routes.d.ts +9 -0
  22. package/dist/hq-server/routes/auth/session-audit-routes.d.ts +6 -0
  23. package/dist/hq-server/routes/auth/totp-routes.d.ts +9 -0
  24. package/dist/hq-server/routes/auth-handlers.d.ts +5 -132
  25. package/dist/hq-server/routes/command-handlers.d.ts +7 -0
  26. package/dist/hq-server/routes/data-handlers.d.ts +8 -0
  27. package/dist/hq-server/routes/mailbox-handlers.d.ts +22 -0
  28. package/dist/hq-server/routes.d.ts +3 -3
  29. package/dist/hq-server/server-lifecycle.d.ts +37 -0
  30. package/dist/hq-server/snapshot.d.ts +6 -5
  31. package/dist/hq-server/types.d.ts +4 -0
  32. package/dist/hq-server/upgrade-handler.d.ts +34 -0
  33. package/dist/hq-server/ws-client-events.d.ts +25 -0
  34. package/dist/hq-server/ws-leader.d.ts +47 -0
  35. package/dist/hq-server/ws-resume.d.ts +27 -0
  36. package/dist/hq-server/ws.d.ts +4 -71
  37. package/dist/{hq-server-IVZLVCFV.js → hq-server-PBRU275L.js} +2 -2
  38. package/dist/hq-server.d.ts +0 -38
  39. package/dist/index.js +1267 -122
  40. package/dist/{mcp-W7HHN7IA.js → mcp-CMLFHRG2.js} +14 -7
  41. package/dist/mcp-serve.d.ts +12 -1
  42. package/dist/{modeldiag-6VS5KDP5.js → modeldiag-ICMPHT5L.js} +868 -831
  43. package/dist/{providers-models-HQN3AT3K.js → providers-models-4FAKM2WO.js} +4 -4
  44. package/dist/repl-eternal-loop.d.ts +8 -0
  45. package/dist/repl-goal-banner.d.ts +21 -0
  46. package/dist/slash-commands/brain-council.d.ts +36 -0
  47. package/dist/slash-commands/brain-status.d.ts +12 -0
  48. package/dist/slash-commands/kanban-board-handlers.d.ts +13 -0
  49. package/dist/slash-commands/kanban-lifecycle-diagnostics.d.ts +16 -0
  50. package/dist/slash-commands/kanban-task-subcommands.d.ts +8 -0
  51. package/dist/slash-commands/memory-audience.d.ts +14 -0
  52. package/dist/slash-commands/memory-gather.d.ts +9 -0
  53. package/dist/slash-commands/memory-stats.d.ts +5 -0
  54. package/dist/slash-commands/settings-mutations.d.ts +6 -0
  55. package/dist/slash-commands/settings-view.d.ts +5 -0
  56. package/dist/subcommands/handlers/modeldiag-bench.d.ts +10 -0
  57. package/dist/subcommands/handlers/modeldiag-eval.d.ts +27 -0
  58. package/dist/subcommands/handlers/modeldiag-test.d.ts +8 -0
  59. package/dist/{version-help-ASKQYXDJ.js → version-help-TFD6VPF3.js} +2 -2
  60. package/dist/vibe-protocol-wiring.d.ts +2 -0
  61. package/dist/webui-server/credential-watcher.d.ts +15 -0
  62. package/dist/webui-server/domain-handlers.d.ts +21 -0
  63. package/dist/webui-server/route-contexts.d.ts +41 -0
  64. package/dist/{webui-server-FTG7JAZU.js → webui-server-IPHGJGXE.js} +515 -437
  65. package/dist/wiring/cli-execute-builder.d.ts +88 -0
  66. package/dist/wiring/cli-heap-watchdog.d.ts +33 -0
  67. package/dist/wiring/cli-prompt-and-tools-setup.d.ts +25 -0
  68. package/dist/wiring/cli-slash-commands-setup.d.ts +79 -0
  69. package/dist/wiring/heap-watchdog-setup.d.ts +53 -0
  70. package/dist/wiring/metrics.d.ts +1 -0
  71. package/dist/wiring/prompt-journal-recorder.d.ts +45 -0
  72. package/dist/wiring/replay-governance-setup.d.ts +31 -0
  73. package/package.json +27 -27
@@ -1,208 +1,26 @@
1
1
  import {
2
2
  discoverAndMergeProviders
3
3
  } from "./chunk-52C7PD2H.js";
4
+ import {
5
+ activeProfileConfigPath
6
+ } from "./chunk-YMXXOOFN.js";
4
7
  import {
5
8
  mutateConfigProviders,
6
9
  normalizeKeys,
7
10
  writeKeysBack
8
11
  } from "./chunk-SZ42FYPT.js";
9
- import {
10
- activeProfileConfigPath
11
- } from "./chunk-YMXXOOFN.js";
12
12
  import "./chunk-7OCVIDC7.js";
13
13
 
14
14
  // src/subcommands/handlers/modeldiag.ts
15
15
  import * as fs from "node:fs/promises";
16
- import { color as color2, toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
17
- import { makeProviderFromConfig, setOAuthTokenPersister } from "@wrongstack/providers";
16
+ import { color as color5 } from "@wrongstack/core/utils";
18
17
 
19
- // src/subcommands/handlers/model-smoke-test.ts
20
- import { ProviderError } from "@wrongstack/core/types";
21
- import { toErrorMessage } from "@wrongstack/core/utils";
22
- var DEFAULT_TIMEOUT_MS = 45e3;
23
- var DEFAULT_MAX_TOKENS = 32;
24
- function csv(value) {
25
- if (typeof value !== "string") return void 0;
26
- const values = value.split(",").map((part) => part.trim()).filter(Boolean);
27
- return values.length > 0 ? values : void 0;
28
- }
29
- function positiveInteger(value, fallback, label) {
30
- if (value === void 0) return fallback;
31
- const parsed = typeof value === "string" ? Number(value) : Number.NaN;
32
- if (!Number.isSafeInteger(parsed) || parsed <= 0) {
33
- throw new Error(`${label} must be a positive integer.`);
34
- }
35
- return parsed;
36
- }
37
- function parseModelSmokeOptions(args, flags = {}) {
38
- const local = /* @__PURE__ */ new Map();
39
- for (let i = 0; i < args.length; i++) {
40
- const arg = args[i];
41
- if (!arg?.startsWith("--")) continue;
42
- const eq = arg.indexOf("=");
43
- if (eq >= 0) {
44
- local.set(arg.slice(2, eq), arg.slice(eq + 1));
45
- continue;
46
- }
47
- const name = arg.slice(2);
48
- const next = args[i + 1];
49
- if (next && !next.startsWith("--")) {
50
- local.set(name, next);
51
- i++;
52
- } else {
53
- local.set(name, true);
54
- }
55
- }
56
- const get = (name) => local.get(name) ?? flags[name];
57
- const providerFilter = csv(get("providers") ?? get("provider"));
58
- const modelFilter = csv(get("models") ?? get("model"));
59
- return {
60
- allModels: get("all-models") === true || get("all-models") === "true",
61
- planOnly: get("plan") === true || get("plan") === "true",
62
- json: get("json") === true || get("json") === "true",
63
- timeoutMs: positiveInteger(get("timeout"), DEFAULT_TIMEOUT_MS, "--timeout"),
64
- maxTokens: positiveInteger(get("max-tokens"), DEFAULT_MAX_TOKENS, "--max-tokens"),
65
- ...providerFilter ? { providerFilter } : {},
66
- ...modelFilter ? { modelFilter } : {}
67
- };
68
- }
69
- function pushUnique(target, values) {
70
- const seen = new Set(target);
71
- for (const value of values) {
72
- const normalized = value?.trim();
73
- if (!normalized || seen.has(normalized)) continue;
74
- seen.add(normalized);
75
- target.push(normalized);
76
- }
77
- }
78
- async function catalogModelsFor(providerId, providerConfig, registry) {
79
- const direct = await registry.getProvider(providerId).catch(() => void 0);
80
- const inherited = direct ?? (providerConfig?.type && providerConfig.type !== providerId ? await registry.getProvider(providerConfig.type).catch(() => void 0) : void 0);
81
- return inherited?.models ?? [];
82
- }
83
- function supportsTextCompletion(model) {
84
- const outputs = model?.modalities?.output;
85
- return !outputs?.length || outputs.includes("text");
86
- }
87
- async function buildModelSmokeTargets(config, registry, options) {
88
- const providerIds = Object.keys(config.providers ?? {});
89
- if (config.provider && !providerIds.includes(config.provider)) providerIds.push(config.provider);
90
- const providerFilter = options.providerFilter ? new Set(options.providerFilter.map((id) => id.toLowerCase())) : void 0;
91
- const modelFilter = options.modelFilter ? new Set(options.modelFilter) : void 0;
92
- const targets = [];
93
- for (const providerId of providerIds) {
94
- if (providerFilter && !providerFilter.has(providerId.toLowerCase())) continue;
95
- const saved = config.providers?.[providerId];
96
- const catalogModels = await catalogModelsFor(providerId, saved, registry);
97
- const catalogById = new Map(catalogModels.map((model) => [model.id, model]));
98
- const textCatalogModels = catalogModels.filter(supportsTextCompletion);
99
- const candidates = [];
100
- if (options.allModels) {
101
- pushUnique(candidates, saved?.models ?? []);
102
- pushUnique(candidates, Object.keys(saved?.customModels ?? {}));
103
- pushUnique(
104
- candidates,
105
- textCatalogModels.map((model) => model.id)
106
- );
107
- if (providerId === config.provider) pushUnique(candidates, [config.model]);
108
- pushUnique(candidates, [saved?.model]);
109
- } else {
110
- const representative = (providerId === config.provider ? config.model : void 0) ?? saved?.model ?? saved?.models?.find(Boolean) ?? Object.keys(saved?.customModels ?? {})[0] ?? await registry.suggestModel(providerId).catch(() => void 0) ?? textCatalogModels[0]?.id;
111
- pushUnique(candidates, [representative]);
112
- }
113
- for (const modelId of candidates) {
114
- if (modelFilter && !modelFilter.has(modelId)) continue;
115
- if (!supportsTextCompletion(catalogById.get(modelId))) continue;
116
- targets.push({ providerId, modelId });
117
- }
118
- }
119
- return targets;
120
- }
121
- function classifyFailure(err) {
122
- if (err instanceof ProviderError || ProviderError.isProviderError(err)) {
123
- const providerError = err;
124
- return {
125
- error: providerError.describe(),
126
- errorKind: providerError.kind,
127
- httpStatus: providerError.status
128
- };
129
- }
130
- const isTimeout = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError") || /timed?\s*out/i.test(toErrorMessage(err));
131
- return {
132
- error: toErrorMessage(err),
133
- errorKind: isTimeout ? "timeout" : "unknown"
134
- };
135
- }
136
- async function runModelSmokeTests(params) {
137
- const results = [];
138
- let providerId;
139
- let provider;
140
- let providerError;
141
- for (let index = 0; index < params.targets.length; index++) {
142
- const target = params.targets[index];
143
- params.onTargetStart?.(target, index, params.targets.length);
144
- const started = Date.now();
145
- if (target.providerId !== providerId) {
146
- providerId = target.providerId;
147
- provider = void 0;
148
- providerError = void 0;
149
- try {
150
- provider = await params.createProvider(target.providerId);
151
- } catch (err) {
152
- providerError = err;
153
- }
154
- }
155
- if (!provider) {
156
- const failure = classifyFailure(providerError ?? new Error("Provider could not be created."));
157
- const result = {
158
- providerId: target.providerId,
159
- modelId: target.modelId,
160
- status: "failed",
161
- latencyMs: Date.now() - started,
162
- ...failure
163
- };
164
- results.push(result);
165
- params.onTargetComplete?.(result, index, params.targets.length);
166
- continue;
167
- }
168
- try {
169
- const response = await provider.complete(
170
- {
171
- model: target.modelId,
172
- messages: [
173
- {
174
- role: "user",
175
- content: [{ type: "text", text: "Reply with exactly: OK" }]
176
- }
177
- ],
178
- maxTokens: params.options.maxTokens
179
- },
180
- { signal: AbortSignal.timeout(params.options.timeoutMs) }
181
- );
182
- const result = {
183
- providerId: target.providerId,
184
- modelId: target.modelId,
185
- status: "passed",
186
- latencyMs: Date.now() - started,
187
- stopReason: response.stopReason,
188
- usage: response.usage
189
- };
190
- results.push(result);
191
- params.onTargetComplete?.(result, index, params.targets.length);
192
- } catch (err) {
193
- const result = {
194
- providerId: target.providerId,
195
- modelId: target.modelId,
196
- status: "failed",
197
- latencyMs: Date.now() - started,
198
- ...classifyFailure(err)
199
- };
200
- results.push(result);
201
- params.onTargetComplete?.(result, index, params.targets.length);
202
- }
203
- }
204
- return results;
205
- }
18
+ // src/subcommands/handlers/modeldiag-bench.ts
19
+ import { color as color3, toErrorMessage } from "@wrongstack/core/utils";
20
+
21
+ // src/subcommands/handlers/modeldiag-eval.ts
22
+ import { makeProviderFromConfig } from "@wrongstack/providers";
23
+ import { color as color2 } from "@wrongstack/core/utils";
206
24
 
207
25
  // src/subcommands/handlers/modeldiag-profiles.ts
208
26
  import { color } from "@wrongstack/core/utils";
@@ -447,7 +265,7 @@ function roleCat(role) {
447
265
  return ROLE_CATEGORY[role] ?? "general";
448
266
  }
449
267
 
450
- // src/subcommands/handlers/modeldiag.ts
268
+ // src/subcommands/handlers/modeldiag-eval.ts
451
269
  function createProviderForId(providerId, cfg) {
452
270
  const savedCfg = cfg.providers?.[providerId];
453
271
  const cfgWithType = Object.assign(
@@ -511,79 +329,836 @@ Output ONLY a ranked list, one per line:
511
329
  return responses.map((_, i) => i);
512
330
  }
513
331
  }
514
- async function readProviders(cachePath) {
515
- if (!cachePath) {
516
- return `${color2.red("Models cache not available")}.`;
517
- }
518
- try {
519
- const raw = await fs.readFile(cachePath, "utf8");
520
- const parsed = JSON.parse(raw);
521
- const payload = parsed.payload ?? parsed;
522
- return Object.entries(payload).map(([id, p]) => ({
523
- id: p.id ?? id,
524
- name: p.name ?? id,
525
- family: p.npm ?? id,
526
- models: Object.values(p.models ?? {}).map(
527
- (m) => ({
528
- id: m.id,
529
- name: m.name,
530
- capabilities: {
531
- contextWindow: m.limit?.context,
532
- maxOutputTokens: m.limit?.output
533
- },
534
- pricing: m.cost
535
- })
536
- )
537
- }));
538
- } catch {
539
- return `${color2.amber("Models cache not available")}. Run wstack sync-models.`;
540
- }
541
- }
542
- function checkHasKey(pid, config) {
543
- if (pid === config.provider && config.provider) return true;
544
- const pc = config.providers?.[pid];
545
- if (!pc) return false;
546
- if (typeof pc.apiKey === "string" && pc.apiKey.length > 0) return true;
547
- if (Array.isArray(pc.apiKeys) && pc.apiKeys.some((k) => k?.apiKey)) return true;
548
- return false;
549
- }
550
- var modeldiagCmd = async (args, deps) => {
551
- const sub = args[0]?.toLowerCase() || "full";
552
- const cacheResult = await readProviders(deps.paths.modelsCache);
553
- if (typeof cacheResult === "string") {
554
- deps.renderer.write(`${cacheResult}
555
- `);
556
- return cacheResult.includes(color2.red("")) ? 1 : 0;
557
- }
558
- const providers = cacheResult;
559
- const config = deps.config;
560
- const modelMatrix = config.modelMatrix ?? {};
561
- function hasKey(pid) {
562
- return checkHasKey(pid, config);
563
- }
564
- function writeLine(line = "") {
332
+ async function runModeldiagEval(args, deps, providers, config, hasKey) {
333
+ const writeLine = (line = "") => {
565
334
  deps.renderer.write(`${line}
566
335
  `);
336
+ };
337
+ const evalArgs = args.slice(1);
338
+ const providersEq = evalArgs.find((a) => a.startsWith("--providers="));
339
+ const providersValue = providersEq?.replace("--providers=", "") ?? (typeof deps.flags?.["providers"] === "string" ? deps.flags["providers"] : typeof deps.flags?.["provider"] === "string" ? deps.flags["provider"] : void 0);
340
+ const providerFilter = providersValue ? providersValue.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
341
+ const maxEq = evalArgs.find((a) => a.startsWith("--max="));
342
+ const maxValue = maxEq?.replace("--max=", "") ?? (typeof deps.flags?.["max"] === "string" ? deps.flags["max"] : void 0);
343
+ const maxModels = maxValue ? Math.max(1, parseInt(maxValue, 10) || 2) : 2;
344
+ const quick = evalArgs.includes("--quick") || deps.flags?.["quick"] === true || deps.flags?.["quick"] === "true";
345
+ const modelsPerCat = quick ? 1 : maxModels;
346
+ const roleFilter = evalArgs.find((a) => !a.startsWith("--"));
347
+ const targetCategories = roleFilter ? EVAL_CATEGORIES.includes(roleCat(roleFilter)) ? [roleCat(roleFilter)] : [] : EVAL_CATEGORIES;
348
+ if (targetCategories.length === 0 && roleFilter) {
349
+ writeLine(
350
+ `${color2.amber("Unknown role/category")}: "${roleFilter}". Try: ${EVAL_CATEGORIES.join(", ")}`
351
+ );
352
+ return 1;
567
353
  }
568
- if (sub === "keys") {
569
- writeLine(`${color2.bold("API Key Status")}`);
570
- writeLine();
571
- for (const prov of providers) {
572
- const k = hasKey(prov.id);
573
- writeLine(` ${checkMark(k)} ${color2.bold(prov.id.padEnd(18))} ${color2.dim(prov.name)}`);
354
+ const keyedProviderIds = providers.filter((p) => hasKey(p.id)).map((p) => p.id);
355
+ let targetProviderIds;
356
+ if (providerFilter && providerFilter.length > 0) {
357
+ const unknown = providerFilter.filter((pid) => !keyedProviderIds.includes(pid));
358
+ targetProviderIds = providerFilter.filter((pid) => keyedProviderIds.includes(pid));
359
+ if (targetProviderIds.length === 0) {
360
+ const noKeyMsg = unknown.length > 0 ? `None of the specified providers (${unknown.join(", ")}) have API keys. Add keys with wstack auth.` : "None of the specified providers have API keys configured.";
361
+ writeLine(`${color2.amber(noKeyMsg)}`);
362
+ return 0;
574
363
  }
575
- writeLine();
576
- writeLine(`${color2.dim(`Leader: ${config.provider}/${config.model}`)}`);
364
+ } else if (keyedProviderIds.length === 0) {
365
+ writeLine(`${color2.amber("No providers have API keys. Add keys with wstack auth.")}`);
577
366
  return 0;
578
- }
579
- if (sub === "caps") {
580
- writeLine(`${color2.bold("Model Capabilities")} ${color2.dim("\u2014 matched to known profiles")}`);
581
- writeLine();
582
- for (const prov of providers) {
583
- if (!hasKey(prov.id)) continue;
584
- writeLine(` ${color2.bold(prov.id)} ${color2.dim(`(${prov.name})`)}`);
585
- const tiers = {
586
- premium: [],
367
+ } else if (keyedProviderIds.length === 1) {
368
+ targetProviderIds = keyedProviderIds;
369
+ } else {
370
+ const providerList = keyedProviderIds.map((pid, i) => {
371
+ const info = providers.find((p) => p.id === pid);
372
+ return ` ${color2.cyan(String(i + 1))}) ${color2.bold(pid.padEnd(16))} ${color2.dim(info?.name ?? "")}`;
373
+ }).join("\n");
374
+ deps.renderer.write(
375
+ `
376
+ ${color2.bold("Select providers to evaluate")}
377
+
378
+ ${providerList}
379
+
380
+ ${color2.dim('Enter numbers or provider IDs (comma-separated, or "all"):')}
381
+ `
382
+ );
383
+ const input = await deps.reader.readLine(" > ");
384
+ const selected = input.trim().toLowerCase();
385
+ if (selected === "" || selected === "all") {
386
+ targetProviderIds = keyedProviderIds;
387
+ } else {
388
+ targetProviderIds = [];
389
+ for (const part of selected.split(",").map((s) => s.trim())) {
390
+ const idx = parseInt(part, 10);
391
+ if (idx >= 1 && idx <= keyedProviderIds.length) {
392
+ const pid = keyedProviderIds[idx - 1];
393
+ if (!targetProviderIds.includes(pid)) targetProviderIds.push(pid);
394
+ } else if (keyedProviderIds.includes(part)) {
395
+ if (!targetProviderIds.includes(part)) targetProviderIds.push(part);
396
+ }
397
+ }
398
+ }
399
+ if (targetProviderIds.length === 0) {
400
+ writeLine(color2.dim("No providers selected."));
401
+ return 0;
402
+ }
403
+ }
404
+ const leaderModel = config.model ?? "unknown";
405
+ const unknownProviders = providerFilter ? providerFilter.filter((pid) => !keyedProviderIds.includes(pid)) : [];
406
+ const warningLine = unknownProviders.length > 0 ? ` ${color2.amber("\u26A0 skipped (no key):")} ${unknownProviders.join(", ")}
407
+ ` : "";
408
+ writeLine(`${color2.bold("Model Competency Evaluation")}`);
409
+ writeLine(
410
+ color2.dim(
411
+ `Providers: ${targetProviderIds.join(", ")} | ${targetCategories.length} cats | ${modelsPerCat} model(s)/cat/provider`
412
+ )
413
+ );
414
+ writeLine(warningLine);
415
+ writeLine(color2.dim(`Leader (ranker): ${config.provider}/${leaderModel}`));
416
+ writeLine();
417
+ const collected = /* @__PURE__ */ new Map();
418
+ let total = 0;
419
+ let ok = 0;
420
+ for (const pid of targetProviderIds) {
421
+ const prov = createProviderForId(pid, config);
422
+ if (!prov) {
423
+ writeLine(color2.dim(` \u2298 ${pid}: provider unavailable, skipping`));
424
+ continue;
425
+ }
426
+ for (const cat of targetCategories) {
427
+ const task = EVAL_TASKS[cat];
428
+ if (!task) continue;
429
+ const candidates = rankModels(
430
+ providers,
431
+ (providerId) => providerId === pid && hasKey(providerId),
432
+ cat,
433
+ modelsPerCat
434
+ );
435
+ if (candidates.length === 0) continue;
436
+ if (!collected.has(cat)) collected.set(cat, /* @__PURE__ */ new Map());
437
+ for (const c of candidates) {
438
+ total++;
439
+ const modelKey = `${pid}/${c.model}`;
440
+ try {
441
+ const start = Date.now();
442
+ const resp = await prov.complete(
443
+ {
444
+ model: c.model,
445
+ system: [{ type: "text", text: "Be thorough and correct." }],
446
+ messages: [
447
+ { role: "user", content: [{ type: "text", text: task.prompt }] }
448
+ ],
449
+ maxTokens: 1024
450
+ },
451
+ { signal: AbortSignal.timeout(45e3) }
452
+ );
453
+ const respText = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
454
+ const respUsage = resp.usage;
455
+ collected.get(cat)?.set(modelKey, {
456
+ model: modelKey,
457
+ latency: Date.now() - start,
458
+ tokens: (respUsage?.input ?? 0) + (respUsage?.output ?? 0),
459
+ text: respText
460
+ });
461
+ ok++;
462
+ } catch {
463
+ collected.get(cat)?.set(modelKey, {
464
+ model: modelKey,
465
+ latency: -1,
466
+ tokens: 0,
467
+ text: ""
468
+ });
469
+ }
470
+ }
471
+ }
472
+ }
473
+ writeLine(`${color2.dim(`Phase 1: ${ok}/${total} calls succeeded`)}`);
474
+ writeLine();
475
+ if (collected.size === 0) {
476
+ writeLine(color2.amber("No responses collected. Check provider configuration."));
477
+ return 0;
478
+ }
479
+ const leaderProvider = createProviderForId(config.provider, config);
480
+ if (leaderProvider) {
481
+ writeLine(`${color2.bold("Phase 2")} \u2014 ${color2.dim("leader ranks responses")}`);
482
+ writeLine();
483
+ }
484
+ const rankings = /* @__PURE__ */ new Map();
485
+ for (const [cat, responses] of collected) {
486
+ const valid = Array.from(responses.values()).filter((r) => r.latency >= 0);
487
+ if (valid.length < 2) {
488
+ if (valid.length === 1) {
489
+ const m = valid[0]?.model;
490
+ if (m !== void 0) {
491
+ if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
492
+ rankings.get(m)?.set(cat, { rank: 1, total: 1 });
493
+ }
494
+ }
495
+ continue;
496
+ }
497
+ const task = EVAL_TASKS[cat];
498
+ if (leaderProvider) {
499
+ const ranked = await rankResponses(leaderProvider, leaderModel, task.prompt, valid);
500
+ for (let i = 0; i < valid.length; i++) {
501
+ const m = valid[ranked[i] ?? i]?.model;
502
+ if (m === void 0) continue;
503
+ if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
504
+ rankings.get(m)?.set(cat, { rank: i + 1, total: valid.length });
505
+ }
506
+ } else {
507
+ for (const r of valid) {
508
+ if (!rankings.has(r.model)) rankings.set(r.model, /* @__PURE__ */ new Map());
509
+ rankings.get(r.model)?.set(cat, { rank: 1, total: valid.length });
510
+ }
511
+ }
512
+ }
513
+ writeLine(`${color2.bold("Competency Report")}`);
514
+ writeLine();
515
+ const allModels = [.../* @__PURE__ */ new Set([...rankings.keys()])].sort();
516
+ const catList = [...collected.keys()];
517
+ const modelColWidth = Math.max(24, ...allModels.map((m) => m.length)) + 2;
518
+ const cw = 12;
519
+ writeLine(
520
+ ` ${color2.dim("model".padEnd(modelColWidth))}` + catList.map((c) => color2.dim((EVAL_TASKS[c]?.label ?? c).slice(0, cw).padEnd(cw + 2))).join("")
521
+ );
522
+ writeLine(` ${color2.dim("\u2500".repeat(modelColWidth + catList.length * (cw + 2)))}`);
523
+ for (const model of allModels) {
524
+ const mr = rankings.get(model);
525
+ const provFromModel = model.split("/")[0] ?? "";
526
+ const modelColor = provFromModel === config.provider ? color2.cyan : color2.green;
527
+ let row = ` ${modelColor(model.padEnd(modelColWidth))}`;
528
+ for (const cat of catList) {
529
+ const e = mr.get(cat);
530
+ if (e) {
531
+ const pct = Math.round((1 - (e.rank - 1) / Math.max(1, e.total - 1)) * 100);
532
+ const pc = pct >= 80 ? color2.green : pct >= 50 ? color2.amber : color2.red;
533
+ row += `${pc(`#${e.rank} ${pct}%`.padEnd(cw + 2))}`;
534
+ } else {
535
+ row += color2.dim("\u2014".padEnd(cw + 2));
536
+ }
537
+ }
538
+ writeLine(row);
539
+ }
540
+ writeLine();
541
+ writeLine(color2.dim("#1 100% = best in category. \u2014 = not tested."));
542
+ writeLine();
543
+ writeLine(color2.dim("Pin: wstack setmodel set <role> <provider>/<model>"));
544
+ writeLine(
545
+ color2.dim(
546
+ "Full: wstack modeldiag eval Providers: wstack modeldiag eval --providers=id1,id2"
547
+ )
548
+ );
549
+ writeLine(
550
+ color2.dim("Max: wstack modeldiag eval --max=3 Quick: wstack modeldiag eval --quick")
551
+ );
552
+ return 0;
553
+ }
554
+
555
+ // src/subcommands/handlers/modeldiag-bench.ts
556
+ async function runModeldiagBench(args, deps, providers, config, hasKey) {
557
+ const writeLine = (line = "") => {
558
+ deps.renderer.write(`${line}
559
+ `);
560
+ };
561
+ const benchArgs = args.slice(1);
562
+ if (benchArgs.length < 2) {
563
+ writeLine(
564
+ `${color3.amber("Usage:")} wstack modeldiag bench <role> "<test prompt>" [--providers=p1,p2]`
565
+ );
566
+ writeLine();
567
+ writeLine(
568
+ color3.dim(
569
+ 'Example: wstack modeldiag bench verify "Write a function that checks if a string is a palindrome"'
570
+ )
571
+ );
572
+ writeLine(
573
+ color3.dim(
574
+ "Tests the top 5 candidate models for the role with your prompt and reports results."
575
+ )
576
+ );
577
+ writeLine(color3.dim("Add --providers=anthropic,google to test across multiple providers."));
578
+ return 0;
579
+ }
580
+ const providersEqIdx = benchArgs.findIndex((a) => a.startsWith("--providers="));
581
+ let providerFilter;
582
+ if (providersEqIdx >= 0) {
583
+ const rawFilter = benchArgs[providersEqIdx]?.replace("--providers=", "").split(",");
584
+ if (rawFilter) {
585
+ providerFilter = rawFilter.map((s) => s.trim()).filter(Boolean);
586
+ benchArgs.splice(providersEqIdx, 1);
587
+ }
588
+ } else {
589
+ const rawFilter = deps.flags?.["providers"] ?? deps.flags?.["provider"];
590
+ if (typeof rawFilter === "string") {
591
+ providerFilter = rawFilter.split(",").map((s) => s.trim()).filter(Boolean);
592
+ }
593
+ }
594
+ const benchRole = benchArgs[0];
595
+ if (!benchRole) {
596
+ writeLine(
597
+ `${color3.amber("No benchmark role specified")}. Usage: wstack diag bench <role> [prompt]`
598
+ );
599
+ return 1;
600
+ }
601
+ const benchPrompt = benchArgs.slice(1).join(" ");
602
+ const cat = roleCat(benchRole);
603
+ const candidates = rankModels(providers, hasKey, cat, 5);
604
+ if (candidates.length === 0) {
605
+ writeLine(
606
+ `${color3.amber("No candidate models found")} for role "${benchRole}" (category: ${cat}).`
607
+ );
608
+ return 0;
609
+ }
610
+ let targetCandidates;
611
+ if (providerFilter && providerFilter.length > 0) {
612
+ targetCandidates = candidates.filter((c) => providerFilter.includes(c.provider));
613
+ if (targetCandidates.length === 0) {
614
+ writeLine(
615
+ `${color3.amber("No candidates match the specified providers")}: ${providerFilter.join(", ")}`
616
+ );
617
+ writeLine(
618
+ `Candidate providers: ${[...new Set(candidates.map((c) => c.provider))].join(", ")}`
619
+ );
620
+ return 0;
621
+ }
622
+ } else {
623
+ targetCandidates = candidates;
624
+ }
625
+ writeLine(
626
+ `${color3.bold("Model Benchmark")} \u2014 ${color3.amber(benchRole)} ${color3.dim(`(category: ${cat})`)}`
627
+ );
628
+ writeLine(
629
+ `${color3.dim("Prompt:")} "${benchPrompt.slice(0, 120)}${benchPrompt.length > 120 ? "\u2026" : ""}"`
630
+ );
631
+ writeLine();
632
+ writeLine(
633
+ ` ${color3.dim("# model".padEnd(52))} ${color3.dim("score".padEnd(12))} ${color3.dim("latency".padEnd(10))} ${color3.dim("tokens".padEnd(14))} ${color3.dim("first line")}`
634
+ );
635
+ writeLine(` ${color3.dim("\u2500".repeat(108))}`);
636
+ const providerInstances = /* @__PURE__ */ new Map();
637
+ for (const pid of [...new Set(targetCandidates.map((c) => c.provider))]) {
638
+ const prov = createProviderForId(pid, config);
639
+ if (prov) providerInstances.set(pid, prov);
640
+ }
641
+ let idx = 0;
642
+ for (const c of targetCandidates.slice(0, 20)) {
643
+ idx++;
644
+ const label = `${idx}`.padStart(2);
645
+ const modelKey = `${c.provider}/${c.model}`;
646
+ const prov = providerInstances.get(c.provider);
647
+ if (!prov) {
648
+ writeLine(
649
+ ` ${label} ${color3.red(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color3.red("NO PROVIDER")}`
650
+ );
651
+ continue;
652
+ }
653
+ try {
654
+ const start = Date.now();
655
+ const resp = await prov.complete(
656
+ {
657
+ model: c.model,
658
+ messages: [{ role: "user", content: [{ type: "text", text: benchPrompt }] }],
659
+ maxTokens: 256
660
+ },
661
+ { signal: AbortSignal.timeout(3e4) }
662
+ );
663
+ const latency = Date.now() - start;
664
+ const firstText = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
665
+ const firstLineClean = firstText.replace(/\n/g, " ").slice(0, 80) || color3.dim("(empty)");
666
+ const provColor = c.provider === config.provider ? color3.green : color3.cyan;
667
+ const usage = resp.usage;
668
+ writeLine(
669
+ ` ${label} ${provColor(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color3.amber(fmtMs(latency).padEnd(8))} ${color3.dim(`in${usage?.input ?? "?"}/out${usage?.output ?? "?"}`.padEnd(12))} ${firstLineClean}`
670
+ );
671
+ } catch (err) {
672
+ const errMsg = toErrorMessage(err);
673
+ writeLine(
674
+ ` ${label} ${color3.red(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color3.red("FAILED")} ${color3.dim(errMsg.slice(0, 40))}`
675
+ );
676
+ }
677
+ }
678
+ const testedProviders = [...new Set(targetCandidates.map((c) => c.provider))];
679
+ writeLine();
680
+ writeLine(
681
+ color3.dim(
682
+ `Tested ${idx} model(s) across ${testedProviders.length} provider(s): ${testedProviders.join(", ")}.`
683
+ )
684
+ );
685
+ writeLine(color3.dim("Pin the best: wstack setmodel set <role> <provider>/<model>"));
686
+ return 0;
687
+ }
688
+
689
+ // src/subcommands/handlers/modeldiag-test.ts
690
+ import { color as color4, toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
691
+ import { makeProviderFromConfig as makeProviderFromConfig2, setOAuthTokenPersister } from "@wrongstack/providers";
692
+
693
+ // src/subcommands/handlers/model-smoke-test.ts
694
+ import { ProviderError } from "@wrongstack/core/types";
695
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
696
+ var DEFAULT_TIMEOUT_MS = 45e3;
697
+ var DEFAULT_MAX_TOKENS = 32;
698
+ function csv(value) {
699
+ if (typeof value !== "string") return void 0;
700
+ const values = value.split(",").map((part) => part.trim()).filter(Boolean);
701
+ return values.length > 0 ? values : void 0;
702
+ }
703
+ function positiveInteger(value, fallback, label) {
704
+ if (value === void 0) return fallback;
705
+ const parsed = typeof value === "string" ? Number(value) : Number.NaN;
706
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
707
+ throw new Error(`${label} must be a positive integer.`);
708
+ }
709
+ return parsed;
710
+ }
711
+ function parseModelSmokeOptions(args, flags = {}) {
712
+ const local = /* @__PURE__ */ new Map();
713
+ for (let i = 0; i < args.length; i++) {
714
+ const arg = args[i];
715
+ if (!arg?.startsWith("--")) continue;
716
+ const eq = arg.indexOf("=");
717
+ if (eq >= 0) {
718
+ local.set(arg.slice(2, eq), arg.slice(eq + 1));
719
+ continue;
720
+ }
721
+ const name = arg.slice(2);
722
+ const next = args[i + 1];
723
+ if (next && !next.startsWith("--")) {
724
+ local.set(name, next);
725
+ i++;
726
+ } else {
727
+ local.set(name, true);
728
+ }
729
+ }
730
+ const get = (name) => local.get(name) ?? flags[name];
731
+ const providerFilter = csv(get("providers") ?? get("provider"));
732
+ const modelFilter = csv(get("models") ?? get("model"));
733
+ return {
734
+ allModels: get("all-models") === true || get("all-models") === "true",
735
+ planOnly: get("plan") === true || get("plan") === "true",
736
+ json: get("json") === true || get("json") === "true",
737
+ timeoutMs: positiveInteger(get("timeout"), DEFAULT_TIMEOUT_MS, "--timeout"),
738
+ maxTokens: positiveInteger(get("max-tokens"), DEFAULT_MAX_TOKENS, "--max-tokens"),
739
+ ...providerFilter ? { providerFilter } : {},
740
+ ...modelFilter ? { modelFilter } : {}
741
+ };
742
+ }
743
+ function pushUnique(target, values) {
744
+ const seen = new Set(target);
745
+ for (const value of values) {
746
+ const normalized = value?.trim();
747
+ if (!normalized || seen.has(normalized)) continue;
748
+ seen.add(normalized);
749
+ target.push(normalized);
750
+ }
751
+ }
752
+ async function catalogModelsFor(providerId, providerConfig, registry) {
753
+ const direct = await registry.getProvider(providerId).catch(() => void 0);
754
+ const inherited = direct ?? (providerConfig?.type && providerConfig.type !== providerId ? await registry.getProvider(providerConfig.type).catch(() => void 0) : void 0);
755
+ return inherited?.models ?? [];
756
+ }
757
+ function supportsTextCompletion(model) {
758
+ const outputs = model?.modalities?.output;
759
+ return !outputs?.length || outputs.includes("text");
760
+ }
761
+ async function buildModelSmokeTargets(config, registry, options) {
762
+ const providerIds = Object.keys(config.providers ?? {});
763
+ if (config.provider && !providerIds.includes(config.provider)) providerIds.push(config.provider);
764
+ const providerFilter = options.providerFilter ? new Set(options.providerFilter.map((id) => id.toLowerCase())) : void 0;
765
+ const modelFilter = options.modelFilter ? new Set(options.modelFilter) : void 0;
766
+ const targets = [];
767
+ for (const providerId of providerIds) {
768
+ if (providerFilter && !providerFilter.has(providerId.toLowerCase())) continue;
769
+ const saved = config.providers?.[providerId];
770
+ const catalogModels = await catalogModelsFor(providerId, saved, registry);
771
+ const catalogById = new Map(catalogModels.map((model) => [model.id, model]));
772
+ const textCatalogModels = catalogModels.filter(supportsTextCompletion);
773
+ const candidates = [];
774
+ if (options.allModels) {
775
+ pushUnique(candidates, saved?.models ?? []);
776
+ pushUnique(candidates, Object.keys(saved?.customModels ?? {}));
777
+ pushUnique(
778
+ candidates,
779
+ textCatalogModels.map((model) => model.id)
780
+ );
781
+ if (providerId === config.provider) pushUnique(candidates, [config.model]);
782
+ pushUnique(candidates, [saved?.model]);
783
+ } else {
784
+ const representative = (providerId === config.provider ? config.model : void 0) ?? saved?.model ?? saved?.models?.find(Boolean) ?? Object.keys(saved?.customModels ?? {})[0] ?? await registry.suggestModel(providerId).catch(() => void 0) ?? textCatalogModels[0]?.id;
785
+ pushUnique(candidates, [representative]);
786
+ }
787
+ for (const modelId of candidates) {
788
+ if (modelFilter && !modelFilter.has(modelId)) continue;
789
+ if (!supportsTextCompletion(catalogById.get(modelId))) continue;
790
+ targets.push({ providerId, modelId });
791
+ }
792
+ }
793
+ return targets;
794
+ }
795
+ function classifyFailure(err) {
796
+ if (err instanceof ProviderError || ProviderError.isProviderError(err)) {
797
+ const providerError = err;
798
+ return {
799
+ error: providerError.describe(),
800
+ errorKind: providerError.kind,
801
+ httpStatus: providerError.status
802
+ };
803
+ }
804
+ const isTimeout = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError") || /timed?\s*out/i.test(toErrorMessage2(err));
805
+ return {
806
+ error: toErrorMessage2(err),
807
+ errorKind: isTimeout ? "timeout" : "unknown"
808
+ };
809
+ }
810
+ async function runModelSmokeTests(params) {
811
+ const results = [];
812
+ let providerId;
813
+ let provider;
814
+ let providerError;
815
+ for (let index = 0; index < params.targets.length; index++) {
816
+ const target = params.targets[index];
817
+ params.onTargetStart?.(target, index, params.targets.length);
818
+ const started = Date.now();
819
+ if (target.providerId !== providerId) {
820
+ providerId = target.providerId;
821
+ provider = void 0;
822
+ providerError = void 0;
823
+ try {
824
+ provider = await params.createProvider(target.providerId);
825
+ } catch (err) {
826
+ providerError = err;
827
+ }
828
+ }
829
+ if (!provider) {
830
+ const failure = classifyFailure(providerError ?? new Error("Provider could not be created."));
831
+ const result = {
832
+ providerId: target.providerId,
833
+ modelId: target.modelId,
834
+ status: "failed",
835
+ latencyMs: Date.now() - started,
836
+ ...failure
837
+ };
838
+ results.push(result);
839
+ params.onTargetComplete?.(result, index, params.targets.length);
840
+ continue;
841
+ }
842
+ try {
843
+ const response = await provider.complete(
844
+ {
845
+ model: target.modelId,
846
+ messages: [
847
+ {
848
+ role: "user",
849
+ content: [{ type: "text", text: "Reply with exactly: OK" }]
850
+ }
851
+ ],
852
+ maxTokens: params.options.maxTokens
853
+ },
854
+ { signal: AbortSignal.timeout(params.options.timeoutMs) }
855
+ );
856
+ const result = {
857
+ providerId: target.providerId,
858
+ modelId: target.modelId,
859
+ status: "passed",
860
+ latencyMs: Date.now() - started,
861
+ stopReason: response.stopReason,
862
+ usage: response.usage
863
+ };
864
+ results.push(result);
865
+ params.onTargetComplete?.(result, index, params.targets.length);
866
+ } catch (err) {
867
+ const result = {
868
+ providerId: target.providerId,
869
+ modelId: target.modelId,
870
+ status: "failed",
871
+ latencyMs: Date.now() - started,
872
+ ...classifyFailure(err)
873
+ };
874
+ results.push(result);
875
+ params.onTargetComplete?.(result, index, params.targets.length);
876
+ }
877
+ }
878
+ return results;
879
+ }
880
+
881
+ // src/subcommands/handlers/modeldiag-test.ts
882
+ async function runModeldiagTest(args, deps, config) {
883
+ const writeLine = (line = "") => {
884
+ deps.renderer.write(`${line}
885
+ `);
886
+ };
887
+ if (args.slice(1).includes("--help") || deps.flags?.["help"] === true) {
888
+ writeLine(`${color4.bold("Usage:")} wstack modeldiag test [options]`);
889
+ writeLine();
890
+ writeLine(" (no flags) Probe one representative model per configured provider");
891
+ writeLine(" --all-models Probe every catalog-visible model sequentially");
892
+ writeLine(" --plan Show targets without making model calls");
893
+ writeLine(" --provider=<id,...> Restrict provider ids");
894
+ writeLine(" --model=<id,...> Restrict model ids");
895
+ writeLine(" --timeout=<ms> Per-model timeout (default 45000)");
896
+ writeLine(" --max-tokens=<n> Output cap for the tiny probe (default 32)");
897
+ writeLine(" --json Emit machine-readable output");
898
+ writeLine(" --yes Confirm a run larger than 50 requests");
899
+ return 0;
900
+ }
901
+ let smokeOptions;
902
+ try {
903
+ smokeOptions = parseModelSmokeOptions(args.slice(1), deps.flags);
904
+ } catch (err) {
905
+ writeLine(`${color4.red("Invalid smoke-test options:")} ${toErrorMessage3(err)}`);
906
+ return 2;
907
+ }
908
+ if ("mergeOverlay" in deps.modelsRegistry) {
909
+ await discoverAndMergeProviders({
910
+ config,
911
+ registry: deps.modelsRegistry,
912
+ cacheDir: deps.paths.cacheDir
913
+ });
914
+ }
915
+ const targets = await buildModelSmokeTargets(
916
+ config,
917
+ deps.modelsRegistry,
918
+ smokeOptions
919
+ );
920
+ const byProvider = /* @__PURE__ */ new Map();
921
+ for (const target of targets) {
922
+ byProvider.set(target.providerId, (byProvider.get(target.providerId) ?? 0) + 1);
923
+ }
924
+ if (smokeOptions.planOnly) {
925
+ const plan = {
926
+ mode: smokeOptions.allModels ? "all-models" : "representative",
927
+ targetCount: targets.length,
928
+ providers: Object.fromEntries(byProvider),
929
+ targets
930
+ };
931
+ if (smokeOptions.json) {
932
+ writeLine(JSON.stringify(plan, null, 2));
933
+ } else {
934
+ writeLine(`${color4.bold("Provider/Model Smoke Test Plan")}`);
935
+ writeLine();
936
+ for (const [providerId, count] of byProvider) {
937
+ writeLine(` ${color4.cyan(providerId.padEnd(24))} ${count} model(s)`);
938
+ }
939
+ writeLine();
940
+ writeLine(
941
+ color4.dim(
942
+ `${targets.length} request(s) would run sequentially. Remove --plan to execute.`
943
+ )
944
+ );
945
+ }
946
+ return targets.length > 0 ? 0 : 2;
947
+ }
948
+ if (targets.length === 0) {
949
+ const message = "No provider/model targets matched. Check the active profile or remove filters.";
950
+ if (smokeOptions.json) {
951
+ writeLine(
952
+ JSON.stringify({ results: [], summary: { passed: 0, failed: 0 }, error: message })
953
+ );
954
+ } else {
955
+ writeLine(color4.amber(message));
956
+ }
957
+ return 2;
958
+ }
959
+ const confirmed = deps.flags?.["yes"] === true || deps.flags?.["yes"] === "true" || args.slice(1).includes("--yes");
960
+ if (targets.length > 50 && !confirmed) {
961
+ const message = `${targets.length} live requests are planned. Review with --plan, then add --yes to run them.`;
962
+ if (smokeOptions.json) {
963
+ writeLine(JSON.stringify({ error: message, targetCount: targets.length }));
964
+ } else {
965
+ writeLine(color4.amber(message));
966
+ }
967
+ return 2;
968
+ }
969
+ if (!smokeOptions.json) {
970
+ writeLine(`${color4.bold("Live Provider/Model Smoke Test")}`);
971
+ writeLine(
972
+ color4.dim(
973
+ `${targets.length} request(s), sequential, ${smokeOptions.timeoutMs}ms timeout, max ${smokeOptions.maxTokens} output tokens.`
974
+ )
975
+ );
976
+ writeLine();
977
+ }
978
+ const resultLines = (result, index, total) => {
979
+ if (smokeOptions.json) return;
980
+ const label = `[${String(index + 1).padStart(String(total).length)}/${total}]`;
981
+ const target = `${result.providerId}/${result.modelId}`;
982
+ if (result.status === "passed") {
983
+ writeLine(
984
+ ` ${color4.green("\u2713")} ${color4.dim(label)} ${color4.cyan(target)} ${color4.dim(
985
+ `${fmtMs(result.latencyMs)} \xB7 in${result.usage.input}/out${result.usage.output} \xB7 ${result.stopReason}`
986
+ )}`
987
+ );
988
+ } else {
989
+ const status = result.httpStatus ? `HTTP ${result.httpStatus} \xB7 ` : "";
990
+ writeLine(
991
+ ` ${color4.red("\u2717")} ${color4.dim(label)} ${color4.red(target)} ${color4.dim(
992
+ `${fmtMs(result.latencyMs)} \xB7 ${status}${result.errorKind}`
993
+ )}`
994
+ );
995
+ writeLine(` ${color4.red(result.error.slice(0, 240))}`);
996
+ }
997
+ };
998
+ let oauthWriteChain = Promise.resolve();
999
+ let oauthWriteScheduled = false;
1000
+ const profilePath = activeProfileConfigPath(deps.paths, config);
1001
+ setOAuthTokenPersister((providerId, credentials) => {
1002
+ oauthWriteScheduled = true;
1003
+ oauthWriteChain = oauthWriteChain.then(
1004
+ () => mutateConfigProviders(profilePath, deps.vault, (all) => {
1005
+ const providerConfig = all[providerId];
1006
+ if (!providerConfig) return;
1007
+ const keys = normalizeKeys(providerConfig);
1008
+ const active = providerConfig.activeKey ? keys.find((key) => key.label === providerConfig.activeKey) : keys[0];
1009
+ if (!active) return;
1010
+ active.apiKey = credentials.accessToken;
1011
+ active.refreshToken = credentials.refreshToken;
1012
+ active.expiresAt = new Date(credentials.expiresAt).toISOString();
1013
+ if (credentials.accountId) active.accountId = credentials.accountId;
1014
+ writeKeysBack(providerConfig, keys);
1015
+ })
1016
+ );
1017
+ });
1018
+ let results;
1019
+ try {
1020
+ results = await runModelSmokeTests({
1021
+ targets,
1022
+ options: smokeOptions,
1023
+ createProvider: async (providerId) => {
1024
+ const saved = config.providers?.[providerId];
1025
+ const resolved = await deps.modelsRegistry.getProvider(providerId).catch(() => void 0) ?? (saved?.type && saved.type !== providerId ? await deps.modelsRegistry.getProvider(saved.type).catch(() => void 0) : void 0);
1026
+ const providerConfig = {
1027
+ ...providerId === config.provider ? {
1028
+ ...config.apiKey ? { apiKey: config.apiKey } : {},
1029
+ ...config.baseUrl ? { baseUrl: config.baseUrl } : {}
1030
+ } : {},
1031
+ ...saved,
1032
+ type: providerId,
1033
+ ...saved?.family ?? resolved?.family ? { family: saved?.family ?? resolved?.family } : {},
1034
+ ...saved?.baseUrl ?? resolved?.apiBase ? { baseUrl: saved?.baseUrl ?? resolved?.apiBase } : {},
1035
+ ...saved?.envVars ?? resolved?.envVars ? { envVars: saved?.envVars ?? resolved?.envVars } : {}
1036
+ };
1037
+ return makeProviderFromConfig2(providerId, providerConfig);
1038
+ },
1039
+ onTargetComplete: resultLines
1040
+ });
1041
+ } finally {
1042
+ setOAuthTokenPersister(void 0);
1043
+ }
1044
+ let oauthPersistenceError;
1045
+ if (oauthWriteScheduled) {
1046
+ try {
1047
+ await oauthWriteChain;
1048
+ } catch (err) {
1049
+ oauthPersistenceError = toErrorMessage3(err);
1050
+ }
1051
+ }
1052
+ const passed = results.filter((result) => result.status === "passed").length;
1053
+ const failed = results.length - passed;
1054
+ const summary = { passed, failed, total: results.length };
1055
+ if (smokeOptions.json) {
1056
+ writeLine(
1057
+ JSON.stringify(
1058
+ {
1059
+ results,
1060
+ summary,
1061
+ ...oauthPersistenceError ? {
1062
+ warning: `OAuth token refresh worked in-memory but could not be saved: ${oauthPersistenceError}`
1063
+ } : {}
1064
+ },
1065
+ null,
1066
+ 2
1067
+ )
1068
+ );
1069
+ } else {
1070
+ writeLine();
1071
+ writeLine(
1072
+ `${color4.bold("Summary:")} ${color4.green(`${passed} passed`)} \xB7 ${failed > 0 ? color4.red(`${failed} failed`) : color4.dim("0 failed")}`
1073
+ );
1074
+ if (!smokeOptions.allModels) {
1075
+ writeLine(color4.dim("Use --all-models to test every catalog-visible model sequentially."));
1076
+ }
1077
+ if (oauthPersistenceError) {
1078
+ writeLine(
1079
+ color4.amber(
1080
+ `OAuth token refresh worked in-memory but could not be saved: ${oauthPersistenceError}`
1081
+ )
1082
+ );
1083
+ }
1084
+ }
1085
+ return failed === 0 ? 0 : 1;
1086
+ }
1087
+
1088
+ // src/subcommands/handlers/modeldiag.ts
1089
+ async function readProviders(cachePath) {
1090
+ if (!cachePath) {
1091
+ return `${color5.red("Models cache not available")}.`;
1092
+ }
1093
+ try {
1094
+ const raw = await fs.readFile(cachePath, "utf8");
1095
+ const parsed = JSON.parse(raw);
1096
+ const payload = parsed.payload ?? parsed;
1097
+ return Object.entries(payload).map(([id, p]) => ({
1098
+ id: p.id ?? id,
1099
+ name: p.name ?? id,
1100
+ family: p.npm ?? id,
1101
+ models: Object.values(p.models ?? {}).map(
1102
+ (m) => ({
1103
+ id: m.id,
1104
+ name: m.name,
1105
+ capabilities: {
1106
+ contextWindow: m.limit?.context,
1107
+ maxOutputTokens: m.limit?.output
1108
+ },
1109
+ pricing: m.cost
1110
+ })
1111
+ )
1112
+ }));
1113
+ } catch {
1114
+ return `${color5.amber("Models cache not available")}. Run wstack sync-models.`;
1115
+ }
1116
+ }
1117
+ function checkHasKey(pid, config) {
1118
+ if (pid === config.provider && config.provider) return true;
1119
+ const pc = config.providers?.[pid];
1120
+ if (!pc) return false;
1121
+ if (typeof pc.apiKey === "string" && pc.apiKey.length > 0) return true;
1122
+ if (Array.isArray(pc.apiKeys) && pc.apiKeys.some((k) => k?.apiKey)) return true;
1123
+ return false;
1124
+ }
1125
+ var modeldiagCmd = async (args, deps) => {
1126
+ const sub = args[0]?.toLowerCase() || "full";
1127
+ const cacheResult = await readProviders(deps.paths.modelsCache);
1128
+ if (typeof cacheResult === "string") {
1129
+ deps.renderer.write(`${cacheResult}
1130
+ `);
1131
+ return cacheResult.includes(color5.red("")) ? 1 : 0;
1132
+ }
1133
+ const providers = cacheResult;
1134
+ const config = deps.config;
1135
+ const modelMatrix = config.modelMatrix ?? {};
1136
+ function hasKey(pid) {
1137
+ return checkHasKey(pid, config);
1138
+ }
1139
+ function writeLine(line = "") {
1140
+ deps.renderer.write(`${line}
1141
+ `);
1142
+ }
1143
+ if (sub === "keys") {
1144
+ writeLine(`${color5.bold("API Key Status")}`);
1145
+ writeLine();
1146
+ for (const prov of providers) {
1147
+ const k = hasKey(prov.id);
1148
+ writeLine(` ${checkMark(k)} ${color5.bold(prov.id.padEnd(18))} ${color5.dim(prov.name)}`);
1149
+ }
1150
+ writeLine();
1151
+ writeLine(`${color5.dim(`Leader: ${config.provider}/${config.model}`)}`);
1152
+ return 0;
1153
+ }
1154
+ if (sub === "caps") {
1155
+ writeLine(`${color5.bold("Model Capabilities")} ${color5.dim("\u2014 matched to known profiles")}`);
1156
+ writeLine();
1157
+ for (const prov of providers) {
1158
+ if (!hasKey(prov.id)) continue;
1159
+ writeLine(` ${color5.bold(prov.id)} ${color5.dim(`(${prov.name})`)}`);
1160
+ const tiers = {
1161
+ premium: [],
587
1162
  standard: [],
588
1163
  budget: [],
589
1164
  unknown: []
@@ -595,24 +1170,24 @@ var modeldiagCmd = async (args, deps) => {
595
1170
  for (const tier of ["premium", "standard", "budget", "unknown"]) {
596
1171
  const tierModels = tiers[tier];
597
1172
  if (tierModels.length === 0) continue;
598
- const label = tier === "unknown" ? color2.dim("unmatched") : `${costLabel(tier)} ${tier}`;
1173
+ const label = tier === "unknown" ? color5.dim("unmatched") : `${costLabel(tier)} ${tier}`;
599
1174
  writeLine(` ${label}`);
600
1175
  for (const m of tierModels) {
601
1176
  const cap = m.capabilities;
602
1177
  const ctx = cap?.contextWindow ?? 0;
603
1178
  const maxOut = cap?.maxOutputTokens ?? 0;
604
1179
  const profile = findProfile(prov.id, m.id);
605
- const family = profile ? `${speedLabel(profile.speedTier)} ${color2.green(profile.family)}` : color2.dim("no profile match");
606
- const pricing = m.pricing ? `${color2.dim("in")}${fmtPrice(m.pricing.input)} ${color2.dim("out")}${fmtPrice(m.pricing.output)}` : color2.dim("pricing ?");
1180
+ const family = profile ? `${speedLabel(profile.speedTier)} ${color5.green(profile.family)}` : color5.dim("no profile match");
1181
+ const pricing = m.pricing ? `${color5.dim("in")}${fmtPrice(m.pricing.input)} ${color5.dim("out")}${fmtPrice(m.pricing.output)}` : color5.dim("pricing ?");
607
1182
  writeLine(
608
- ` ${color2.cyan(m.id.padEnd(34))}${ctx > 0 ? `ctx ${fmtTokens(ctx).padEnd(6)}` : color2.dim("ctx ? ")}${maxOut > 0 ? `out ${fmtTokens(maxOut).padEnd(6)}` : " "}${family} ${pricing}`
1183
+ ` ${color5.cyan(m.id.padEnd(34))}${ctx > 0 ? `ctx ${fmtTokens(ctx).padEnd(6)}` : color5.dim("ctx ? ")}${maxOut > 0 ? `out ${fmtTokens(maxOut).padEnd(6)}` : " "}${family} ${pricing}`
609
1184
  );
610
1185
  }
611
1186
  }
612
1187
  writeLine();
613
1188
  }
614
1189
  writeLine(
615
- color2.dim(
1190
+ color5.dim(
616
1191
  "Prices in USD per 1M tokens (input/output). ctx = context window, out = max output."
617
1192
  )
618
1193
  );
@@ -621,10 +1196,10 @@ var modeldiagCmd = async (args, deps) => {
621
1196
  async function renderSuggest() {
622
1197
  writeLine();
623
1198
  writeLine(
624
- `${color2.bold("Agent \u2192 Model Suggestions")} ${color2.amber("(heuristic \u2014 untested)")}`
1199
+ `${color5.bold("Agent \u2192 Model Suggestions")} ${color5.amber("(heuristic \u2014 untested)")}`
625
1200
  );
626
1201
  writeLine(
627
- color2.dim(
1202
+ color5.dim(
628
1203
  'These are profile-based best guesses. Test them with wstack modeldiag bench <role> "<prompt>".'
629
1204
  )
630
1205
  );
@@ -632,7 +1207,7 @@ var modeldiagCmd = async (args, deps) => {
632
1207
  const keyedProviders = providers.filter((p) => hasKey(p.id));
633
1208
  if (keyedProviders.length === 0) {
634
1209
  writeLine(
635
- ` ${color2.amber("No providers have API keys configured. Add keys with wstack auth.")}`
1210
+ ` ${color5.amber("No providers have API keys configured. Add keys with wstack auth.")}`
636
1211
  );
637
1212
  } else {
638
1213
  const roles = [
@@ -654,606 +1229,68 @@ var modeldiagCmd = async (args, deps) => {
654
1229
  const entry = modelMatrix[role];
655
1230
  const p = entry.provider ?? config.provider;
656
1231
  writeLine(
657
- ` ${color2.dim(role.padEnd(20))} \u2192 ${color2.cyan(`${p}/${entry.model}`)} ${color2.dim("(user-configured)")}`
1232
+ ` ${color5.dim(role.padEnd(20))} \u2192 ${color5.cyan(`${p}/${entry.model}`)} ${color5.dim("(user-configured)")}`
658
1233
  );
659
1234
  continue;
660
1235
  }
661
1236
  const cat = roleCat(role);
662
1237
  const ranked = rankModels(providers, hasKey, cat, 3);
663
1238
  if (ranked.length === 0) {
664
- writeLine(` ${color2.dim(role.padEnd(20))} \u2192 ${color2.dim("no candidates")}`);
1239
+ writeLine(` ${color5.dim(role.padEnd(20))} \u2192 ${color5.dim("no candidates")}`);
665
1240
  continue;
666
1241
  }
667
1242
  const best = ranked[0];
668
- const family = best.profile ? ` ${color2.dim(`(${best.profile.family})`)}` : "";
1243
+ const family = best.profile ? ` ${color5.dim(`(${best.profile.family})`)}` : "";
669
1244
  const bar = scoreBar(best.score, 110);
670
1245
  writeLine(
671
- ` ${color2.amber(role.padEnd(20))} \u2192 ${color2.cyan(`${best.provider}/${best.model}`)}${family}`
1246
+ ` ${color5.amber(role.padEnd(20))} \u2192 ${color5.cyan(`${best.provider}/${best.model}`)}${family}`
672
1247
  );
673
- writeLine(` ${" ".repeat(22)} ${bar} ${color2.dim(cat)}`);
1248
+ writeLine(` ${" ".repeat(22)} ${bar} ${color5.dim(cat)}`);
674
1249
  if (ranked.length > 1 && (ranked[1]?.score ?? Number.NEGATIVE_INFINITY) >= best.score - 15) {
675
1250
  for (const alt of ranked.slice(1)) {
676
1251
  const af = alt.profile ? ` (${alt.profile.family})` : "";
677
1252
  writeLine(
678
- ` ${" ".repeat(22)} ${color2.dim(`${alt.provider}/${alt.model}${af} score ${alt.score}`)}`
1253
+ ` ${" ".repeat(22)} ${color5.dim(`${alt.provider}/${alt.model}${af} score ${alt.score}`)}`
679
1254
  );
680
1255
  }
681
1256
  }
682
1257
  }
683
1258
  writeLine();
684
1259
  writeLine(
685
- ` ${color2.bold("leader".padEnd(20))} \u2192 ${color2.cyan(`${config.provider}/${config.model}`)}`
1260
+ ` ${color5.bold("leader".padEnd(20))} \u2192 ${color5.cyan(`${config.provider}/${config.model}`)}`
686
1261
  );
687
1262
  }
688
1263
  }
689
1264
  if (sub === "suggest") {
690
1265
  await renderSuggest();
691
1266
  writeLine();
692
- writeLine(color2.dim("Pin a suggestion: wstack setmodel set <role> <provider>/<model>"));
693
- writeLine(color2.dim('Test candidates: wstack modeldiag bench <role> "<test prompt>"'));
1267
+ writeLine(color5.dim("Pin a suggestion: wstack setmodel set <role> <provider>/<model>"));
1268
+ writeLine(color5.dim('Test candidates: wstack modeldiag bench <role> "<test prompt>"'));
694
1269
  return 0;
695
1270
  }
696
1271
  if (sub === "test") {
697
- if (args.slice(1).includes("--help") || deps.flags?.["help"] === true) {
698
- writeLine(`${color2.bold("Usage:")} wstack modeldiag test [options]`);
699
- writeLine();
700
- writeLine(" (no flags) Probe one representative model per configured provider");
701
- writeLine(" --all-models Probe every catalog-visible model sequentially");
702
- writeLine(" --plan Show targets without making model calls");
703
- writeLine(" --provider=<id,...> Restrict provider ids");
704
- writeLine(" --model=<id,...> Restrict model ids");
705
- writeLine(" --timeout=<ms> Per-model timeout (default 45000)");
706
- writeLine(" --max-tokens=<n> Output cap for the tiny probe (default 32)");
707
- writeLine(" --json Emit machine-readable output");
708
- writeLine(" --yes Confirm a run larger than 50 requests");
709
- return 0;
710
- }
711
- let smokeOptions;
712
- try {
713
- smokeOptions = parseModelSmokeOptions(args.slice(1), deps.flags);
714
- } catch (err) {
715
- writeLine(`${color2.red("Invalid smoke-test options:")} ${toErrorMessage2(err)}`);
716
- return 2;
717
- }
718
- if ("mergeOverlay" in deps.modelsRegistry) {
719
- await discoverAndMergeProviders({
720
- config,
721
- registry: deps.modelsRegistry,
722
- cacheDir: deps.paths.cacheDir
723
- });
724
- }
725
- const targets = await buildModelSmokeTargets(
726
- config,
727
- deps.modelsRegistry,
728
- smokeOptions
729
- );
730
- const byProvider = /* @__PURE__ */ new Map();
731
- for (const target of targets) {
732
- byProvider.set(target.providerId, (byProvider.get(target.providerId) ?? 0) + 1);
733
- }
734
- if (smokeOptions.planOnly) {
735
- const plan = {
736
- mode: smokeOptions.allModels ? "all-models" : "representative",
737
- targetCount: targets.length,
738
- providers: Object.fromEntries(byProvider),
739
- targets
740
- };
741
- if (smokeOptions.json) {
742
- writeLine(JSON.stringify(plan, null, 2));
743
- } else {
744
- writeLine(`${color2.bold("Provider/Model Smoke Test Plan")}`);
745
- writeLine();
746
- for (const [providerId, count] of byProvider) {
747
- writeLine(` ${color2.cyan(providerId.padEnd(24))} ${count} model(s)`);
748
- }
749
- writeLine();
750
- writeLine(
751
- color2.dim(
752
- `${targets.length} request(s) would run sequentially. Remove --plan to execute.`
753
- )
754
- );
755
- }
756
- return targets.length > 0 ? 0 : 2;
757
- }
758
- if (targets.length === 0) {
759
- const message = "No provider/model targets matched. Check the active profile or remove filters.";
760
- if (smokeOptions.json) {
761
- writeLine(
762
- JSON.stringify({ results: [], summary: { passed: 0, failed: 0 }, error: message })
763
- );
764
- } else {
765
- writeLine(color2.amber(message));
766
- }
767
- return 2;
768
- }
769
- const confirmed = deps.flags?.["yes"] === true || deps.flags?.["yes"] === "true" || args.slice(1).includes("--yes");
770
- if (targets.length > 50 && !confirmed) {
771
- const message = `${targets.length} live requests are planned. Review with --plan, then add --yes to run them.`;
772
- if (smokeOptions.json) {
773
- writeLine(JSON.stringify({ error: message, targetCount: targets.length }));
774
- } else {
775
- writeLine(color2.amber(message));
776
- }
777
- return 2;
778
- }
779
- if (!smokeOptions.json) {
780
- writeLine(`${color2.bold("Live Provider/Model Smoke Test")}`);
781
- writeLine(
782
- color2.dim(
783
- `${targets.length} request(s), sequential, ${smokeOptions.timeoutMs}ms timeout, max ${smokeOptions.maxTokens} output tokens.`
784
- )
785
- );
786
- writeLine();
787
- }
788
- const resultLines = (result, index, total) => {
789
- if (smokeOptions.json) return;
790
- const label = `[${String(index + 1).padStart(String(total).length)}/${total}]`;
791
- const target = `${result.providerId}/${result.modelId}`;
792
- if (result.status === "passed") {
793
- writeLine(
794
- ` ${color2.green("\u2713")} ${color2.dim(label)} ${color2.cyan(target)} ${color2.dim(
795
- `${fmtMs(result.latencyMs)} \xB7 in${result.usage.input}/out${result.usage.output} \xB7 ${result.stopReason}`
796
- )}`
797
- );
798
- } else {
799
- const status = result.httpStatus ? `HTTP ${result.httpStatus} \xB7 ` : "";
800
- writeLine(
801
- ` ${color2.red("\u2717")} ${color2.dim(label)} ${color2.red(target)} ${color2.dim(
802
- `${fmtMs(result.latencyMs)} \xB7 ${status}${result.errorKind}`
803
- )}`
804
- );
805
- writeLine(` ${color2.red(result.error.slice(0, 240))}`);
806
- }
807
- };
808
- let oauthWriteChain = Promise.resolve();
809
- let oauthWriteScheduled = false;
810
- const profilePath = activeProfileConfigPath(deps.paths, config);
811
- setOAuthTokenPersister((providerId, credentials) => {
812
- oauthWriteScheduled = true;
813
- oauthWriteChain = oauthWriteChain.then(
814
- () => mutateConfigProviders(profilePath, deps.vault, (all) => {
815
- const providerConfig = all[providerId];
816
- if (!providerConfig) return;
817
- const keys = normalizeKeys(providerConfig);
818
- const active = providerConfig.activeKey ? keys.find((key) => key.label === providerConfig.activeKey) : keys[0];
819
- if (!active) return;
820
- active.apiKey = credentials.accessToken;
821
- active.refreshToken = credentials.refreshToken;
822
- active.expiresAt = new Date(credentials.expiresAt).toISOString();
823
- if (credentials.accountId) active.accountId = credentials.accountId;
824
- writeKeysBack(providerConfig, keys);
825
- })
826
- );
827
- });
828
- let results;
829
- try {
830
- results = await runModelSmokeTests({
831
- targets,
832
- options: smokeOptions,
833
- createProvider: async (providerId) => {
834
- const saved = config.providers?.[providerId];
835
- const resolved = await deps.modelsRegistry.getProvider(providerId).catch(() => void 0) ?? (saved?.type && saved.type !== providerId ? await deps.modelsRegistry.getProvider(saved.type).catch(() => void 0) : void 0);
836
- const providerConfig = {
837
- ...providerId === config.provider ? {
838
- ...config.apiKey ? { apiKey: config.apiKey } : {},
839
- ...config.baseUrl ? { baseUrl: config.baseUrl } : {}
840
- } : {},
841
- ...saved,
842
- type: providerId,
843
- ...saved?.family ?? resolved?.family ? { family: saved?.family ?? resolved?.family } : {},
844
- ...saved?.baseUrl ?? resolved?.apiBase ? { baseUrl: saved?.baseUrl ?? resolved?.apiBase } : {},
845
- ...saved?.envVars ?? resolved?.envVars ? { envVars: saved?.envVars ?? resolved?.envVars } : {}
846
- };
847
- return makeProviderFromConfig(providerId, providerConfig);
848
- },
849
- onTargetComplete: resultLines
850
- });
851
- } finally {
852
- setOAuthTokenPersister(void 0);
853
- }
854
- let oauthPersistenceError;
855
- if (oauthWriteScheduled) {
856
- try {
857
- await oauthWriteChain;
858
- } catch (err) {
859
- oauthPersistenceError = toErrorMessage2(err);
860
- }
861
- }
862
- const passed = results.filter((result) => result.status === "passed").length;
863
- const failed = results.length - passed;
864
- const summary = { passed, failed, total: results.length };
865
- if (smokeOptions.json) {
866
- writeLine(
867
- JSON.stringify(
868
- {
869
- results,
870
- summary,
871
- ...oauthPersistenceError ? {
872
- warning: `OAuth token refresh worked in-memory but could not be saved: ${oauthPersistenceError}`
873
- } : {}
874
- },
875
- null,
876
- 2
877
- )
878
- );
879
- } else {
880
- writeLine();
881
- writeLine(
882
- `${color2.bold("Summary:")} ${color2.green(`${passed} passed`)} \xB7 ${failed > 0 ? color2.red(`${failed} failed`) : color2.dim("0 failed")}`
883
- );
884
- if (!smokeOptions.allModels) {
885
- writeLine(color2.dim("Use --all-models to test every catalog-visible model sequentially."));
886
- }
887
- if (oauthPersistenceError) {
888
- writeLine(
889
- color2.amber(
890
- `OAuth token refresh worked in-memory but could not be saved: ${oauthPersistenceError}`
891
- )
892
- );
893
- }
894
- }
895
- return failed === 0 ? 0 : 1;
1272
+ return runModeldiagTest(args, deps, config);
896
1273
  }
897
1274
  if (sub === "bench") {
898
- const benchArgs = args.slice(1);
899
- if (benchArgs.length < 2) {
900
- writeLine(
901
- `${color2.amber("Usage:")} wstack modeldiag bench <role> "<test prompt>" [--providers=p1,p2]`
902
- );
903
- writeLine();
904
- writeLine(
905
- color2.dim(
906
- 'Example: wstack modeldiag bench verify "Write a function that checks if a string is a palindrome"'
907
- )
908
- );
909
- writeLine(
910
- color2.dim(
911
- "Tests the top 5 candidate models for the role with your prompt and reports results."
912
- )
913
- );
914
- writeLine(color2.dim("Add --providers=anthropic,google to test across multiple providers."));
915
- return 0;
916
- }
917
- const providersEqIdx = benchArgs.findIndex((a) => a.startsWith("--providers="));
918
- let providerFilter;
919
- if (providersEqIdx >= 0) {
920
- const rawFilter = benchArgs[providersEqIdx]?.replace("--providers=", "").split(",");
921
- if (rawFilter) {
922
- providerFilter = rawFilter.map((s) => s.trim()).filter(Boolean);
923
- benchArgs.splice(providersEqIdx, 1);
924
- }
925
- } else {
926
- const rawFilter = deps.flags?.["providers"] ?? deps.flags?.["provider"];
927
- if (typeof rawFilter === "string") {
928
- providerFilter = rawFilter.split(",").map((s) => s.trim()).filter(Boolean);
929
- }
930
- }
931
- const benchRole = benchArgs[0];
932
- if (!benchRole) {
933
- writeLine(
934
- `${color2.amber("No benchmark role specified")}. Usage: wstack diag bench <role> [prompt]`
935
- );
936
- return 1;
937
- }
938
- const benchPrompt = benchArgs.slice(1).join(" ");
939
- const cat = roleCat(benchRole);
940
- const candidates = rankModels(providers, hasKey, cat, 5);
941
- if (candidates.length === 0) {
942
- writeLine(
943
- `${color2.amber("No candidate models found")} for role "${benchRole}" (category: ${cat}).`
944
- );
945
- return 0;
946
- }
947
- let targetCandidates;
948
- if (providerFilter && providerFilter.length > 0) {
949
- targetCandidates = candidates.filter((c) => providerFilter.includes(c.provider));
950
- if (targetCandidates.length === 0) {
951
- writeLine(
952
- `${color2.amber("No candidates match the specified providers")}: ${providerFilter.join(", ")}`
953
- );
954
- writeLine(
955
- `Candidate providers: ${[...new Set(candidates.map((c) => c.provider))].join(", ")}`
956
- );
957
- return 0;
958
- }
959
- } else {
960
- targetCandidates = candidates;
961
- }
962
- writeLine(
963
- `${color2.bold("Model Benchmark")} \u2014 ${color2.amber(benchRole)} ${color2.dim(`(category: ${cat})`)}`
964
- );
965
- writeLine(
966
- `${color2.dim("Prompt:")} "${benchPrompt.slice(0, 120)}${benchPrompt.length > 120 ? "\u2026" : ""}"`
967
- );
968
- writeLine();
969
- writeLine(
970
- ` ${color2.dim("# model".padEnd(52))} ${color2.dim("score".padEnd(12))} ${color2.dim("latency".padEnd(10))} ${color2.dim("tokens".padEnd(14))} ${color2.dim("first line")}`
971
- );
972
- writeLine(` ${color2.dim("\u2500".repeat(108))}`);
973
- const providerInstances = /* @__PURE__ */ new Map();
974
- for (const pid of [...new Set(targetCandidates.map((c) => c.provider))]) {
975
- const prov = createProviderForId(pid, config);
976
- if (prov) providerInstances.set(pid, prov);
977
- }
978
- let idx = 0;
979
- for (const c of targetCandidates.slice(0, 20)) {
980
- idx++;
981
- const label = `${idx}`.padStart(2);
982
- const modelKey = `${c.provider}/${c.model}`;
983
- const prov = providerInstances.get(c.provider);
984
- if (!prov) {
985
- writeLine(
986
- ` ${label} ${color2.red(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color2.red("NO PROVIDER")}`
987
- );
988
- continue;
989
- }
990
- try {
991
- const start = Date.now();
992
- const resp = await prov.complete(
993
- {
994
- model: c.model,
995
- messages: [{ role: "user", content: [{ type: "text", text: benchPrompt }] }],
996
- maxTokens: 256
997
- },
998
- { signal: AbortSignal.timeout(3e4) }
999
- );
1000
- const latency = Date.now() - start;
1001
- const firstText = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
1002
- const firstLineClean = firstText.replace(/\n/g, " ").slice(0, 80) || color2.dim("(empty)");
1003
- const provColor = c.provider === config.provider ? color2.green : color2.cyan;
1004
- const usage = resp.usage;
1005
- writeLine(
1006
- ` ${label} ${provColor(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color2.amber(fmtMs(latency).padEnd(8))} ${color2.dim(`in${usage?.input ?? "?"}/out${usage?.output ?? "?"}`.padEnd(12))} ${firstLineClean}`
1007
- );
1008
- } catch (err) {
1009
- const errMsg = toErrorMessage2(err);
1010
- writeLine(
1011
- ` ${label} ${color2.red(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color2.red("FAILED")} ${color2.dim(errMsg.slice(0, 40))}`
1012
- );
1013
- }
1014
- }
1015
- const testedProviders = [...new Set(targetCandidates.map((c) => c.provider))];
1016
- writeLine();
1017
- writeLine(
1018
- color2.dim(
1019
- `Tested ${idx} model(s) across ${testedProviders.length} provider(s): ${testedProviders.join(", ")}.`
1020
- )
1021
- );
1022
- writeLine(color2.dim("Pin the best: wstack setmodel set <role> <provider>/<model>"));
1023
- return 0;
1275
+ return runModeldiagBench(args, deps, providers, config, hasKey);
1024
1276
  }
1025
1277
  if (sub === "eval" || sub === "evall") {
1026
- const evalArgs = args.slice(1);
1027
- const providersEq = evalArgs.find((a) => a.startsWith("--providers="));
1028
- const providersValue = providersEq?.replace("--providers=", "") ?? (typeof deps.flags?.["providers"] === "string" ? deps.flags["providers"] : typeof deps.flags?.["provider"] === "string" ? deps.flags["provider"] : void 0);
1029
- const providerFilter = providersValue ? providersValue.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
1030
- const maxEq = evalArgs.find((a) => a.startsWith("--max="));
1031
- const maxValue = maxEq?.replace("--max=", "") ?? (typeof deps.flags?.["max"] === "string" ? deps.flags["max"] : void 0);
1032
- const maxModels = maxValue ? Math.max(1, parseInt(maxValue, 10) || 2) : 2;
1033
- const quick = evalArgs.includes("--quick") || deps.flags?.["quick"] === true || deps.flags?.["quick"] === "true";
1034
- const modelsPerCat = quick ? 1 : maxModels;
1035
- const roleFilter = evalArgs.find((a) => !a.startsWith("--"));
1036
- const targetCategories = roleFilter ? EVAL_CATEGORIES.includes(roleCat(roleFilter)) ? [roleCat(roleFilter)] : [] : EVAL_CATEGORIES;
1037
- if (targetCategories.length === 0 && roleFilter) {
1038
- writeLine(
1039
- `${color2.amber("Unknown role/category")}: "${roleFilter}". Try: ${EVAL_CATEGORIES.join(", ")}`
1040
- );
1041
- return 1;
1042
- }
1043
- const keyedProviderIds = providers.filter((p) => hasKey(p.id)).map((p) => p.id);
1044
- let targetProviderIds;
1045
- if (providerFilter && providerFilter.length > 0) {
1046
- const unknown = providerFilter.filter((pid) => !keyedProviderIds.includes(pid));
1047
- targetProviderIds = providerFilter.filter((pid) => keyedProviderIds.includes(pid));
1048
- if (targetProviderIds.length === 0) {
1049
- const noKeyMsg = unknown.length > 0 ? `None of the specified providers (${unknown.join(", ")}) have API keys. Add keys with wstack auth.` : "None of the specified providers have API keys configured.";
1050
- writeLine(`${color2.amber(noKeyMsg)}`);
1051
- return 0;
1052
- }
1053
- } else if (keyedProviderIds.length === 0) {
1054
- writeLine(`${color2.amber("No providers have API keys. Add keys with wstack auth.")}`);
1055
- return 0;
1056
- } else if (keyedProviderIds.length === 1) {
1057
- targetProviderIds = keyedProviderIds;
1058
- } else {
1059
- const providerList = keyedProviderIds.map((pid, i) => {
1060
- const info = providers.find((p) => p.id === pid);
1061
- return ` ${color2.cyan(String(i + 1))}) ${color2.bold(pid.padEnd(16))} ${color2.dim(info?.name ?? "")}`;
1062
- }).join("\n");
1063
- deps.renderer.write(
1064
- `
1065
- ${color2.bold("Select providers to evaluate")}
1066
-
1067
- ${providerList}
1068
-
1069
- ${color2.dim('Enter numbers or provider IDs (comma-separated, or "all"):')}
1070
- `
1071
- );
1072
- const input = await deps.reader.readLine(" > ");
1073
- const selected = input.trim().toLowerCase();
1074
- if (selected === "" || selected === "all") {
1075
- targetProviderIds = keyedProviderIds;
1076
- } else {
1077
- targetProviderIds = [];
1078
- for (const part of selected.split(",").map((s) => s.trim())) {
1079
- const idx = parseInt(part, 10);
1080
- if (idx >= 1 && idx <= keyedProviderIds.length) {
1081
- const pid = keyedProviderIds[idx - 1];
1082
- if (!targetProviderIds.includes(pid)) targetProviderIds.push(pid);
1083
- } else if (keyedProviderIds.includes(part)) {
1084
- if (!targetProviderIds.includes(part)) targetProviderIds.push(part);
1085
- }
1086
- }
1087
- }
1088
- if (targetProviderIds.length === 0) {
1089
- writeLine(color2.dim("No providers selected."));
1090
- return 0;
1091
- }
1092
- }
1093
- const leaderModel = config.model ?? "unknown";
1094
- const unknownProviders = providerFilter ? providerFilter.filter((pid) => !keyedProviderIds.includes(pid)) : [];
1095
- const warningLine = unknownProviders.length > 0 ? ` ${color2.amber("\u26A0 skipped (no key):")} ${unknownProviders.join(", ")}
1096
- ` : "";
1097
- writeLine(`${color2.bold("Model Competency Evaluation")}`);
1098
- writeLine(
1099
- color2.dim(
1100
- `Providers: ${targetProviderIds.join(", ")} | ${targetCategories.length} cats | ${modelsPerCat} model(s)/cat/provider`
1101
- )
1102
- );
1103
- writeLine(warningLine);
1104
- writeLine(color2.dim(`Leader (ranker): ${config.provider}/${leaderModel}`));
1105
- writeLine();
1106
- const collected = /* @__PURE__ */ new Map();
1107
- let total = 0;
1108
- let ok = 0;
1109
- for (const pid of targetProviderIds) {
1110
- const prov = createProviderForId(pid, config);
1111
- if (!prov) {
1112
- writeLine(color2.dim(` \u2298 ${pid}: provider unavailable, skipping`));
1113
- continue;
1114
- }
1115
- for (const cat of targetCategories) {
1116
- const task = EVAL_TASKS[cat];
1117
- if (!task) continue;
1118
- const candidates = rankModels(
1119
- providers,
1120
- (providerId) => providerId === pid && hasKey(providerId),
1121
- cat,
1122
- modelsPerCat
1123
- );
1124
- if (candidates.length === 0) continue;
1125
- if (!collected.has(cat)) collected.set(cat, /* @__PURE__ */ new Map());
1126
- for (const c of candidates) {
1127
- total++;
1128
- const modelKey = `${pid}/${c.model}`;
1129
- try {
1130
- const start = Date.now();
1131
- const resp = await prov.complete(
1132
- {
1133
- model: c.model,
1134
- system: [{ type: "text", text: "Be thorough and correct." }],
1135
- messages: [
1136
- { role: "user", content: [{ type: "text", text: task.prompt }] }
1137
- ],
1138
- maxTokens: 1024
1139
- },
1140
- { signal: AbortSignal.timeout(45e3) }
1141
- );
1142
- const respText = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
1143
- const respUsage = resp.usage;
1144
- collected.get(cat)?.set(modelKey, {
1145
- model: modelKey,
1146
- latency: Date.now() - start,
1147
- tokens: (respUsage?.input ?? 0) + (respUsage?.output ?? 0),
1148
- text: respText
1149
- });
1150
- ok++;
1151
- } catch {
1152
- collected.get(cat)?.set(modelKey, {
1153
- model: modelKey,
1154
- latency: -1,
1155
- tokens: 0,
1156
- text: ""
1157
- });
1158
- }
1159
- }
1160
- }
1161
- }
1162
- writeLine(`${color2.dim(`Phase 1: ${ok}/${total} calls succeeded`)}`);
1163
- writeLine();
1164
- if (collected.size === 0) {
1165
- writeLine(color2.amber("No responses collected. Check provider configuration."));
1166
- return 0;
1167
- }
1168
- const leaderProvider = createProviderForId(config.provider, config);
1169
- if (leaderProvider) {
1170
- writeLine(`${color2.bold("Phase 2")} \u2014 ${color2.dim("leader ranks responses")}`);
1171
- writeLine();
1172
- }
1173
- const rankings = /* @__PURE__ */ new Map();
1174
- for (const [cat, responses] of collected) {
1175
- const valid = Array.from(responses.values()).filter((r) => r.latency >= 0);
1176
- if (valid.length < 2) {
1177
- if (valid.length === 1) {
1178
- const m = valid[0]?.model;
1179
- if (m !== void 0) {
1180
- if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
1181
- rankings.get(m)?.set(cat, { rank: 1, total: 1 });
1182
- }
1183
- }
1184
- continue;
1185
- }
1186
- const task = EVAL_TASKS[cat];
1187
- if (leaderProvider) {
1188
- const ranked = await rankResponses(leaderProvider, leaderModel, task.prompt, valid);
1189
- for (let i = 0; i < valid.length; i++) {
1190
- const m = valid[ranked[i] ?? i]?.model;
1191
- if (m === void 0) continue;
1192
- if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
1193
- rankings.get(m)?.set(cat, { rank: i + 1, total: valid.length });
1194
- }
1195
- } else {
1196
- for (const r of valid) {
1197
- if (!rankings.has(r.model)) rankings.set(r.model, /* @__PURE__ */ new Map());
1198
- rankings.get(r.model)?.set(cat, { rank: 1, total: valid.length });
1199
- }
1200
- }
1201
- }
1202
- writeLine(`${color2.bold("Competency Report")}`);
1203
- writeLine();
1204
- const allModels = [.../* @__PURE__ */ new Set([...rankings.keys()])].sort();
1205
- const catList = [...collected.keys()];
1206
- const modelColWidth = Math.max(24, ...allModels.map((m) => m.length)) + 2;
1207
- const cw = 12;
1208
- writeLine(
1209
- ` ${color2.dim("model".padEnd(modelColWidth))}` + catList.map((c) => color2.dim((EVAL_TASKS[c]?.label ?? c).slice(0, cw).padEnd(cw + 2))).join("")
1210
- );
1211
- writeLine(` ${color2.dim("\u2500".repeat(modelColWidth + catList.length * (cw + 2)))}`);
1212
- for (const model of allModels) {
1213
- const mr = rankings.get(model);
1214
- const provFromModel = model.split("/")[0] ?? "";
1215
- const modelColor = provFromModel === config.provider ? color2.cyan : color2.green;
1216
- let row = ` ${modelColor(model.padEnd(modelColWidth))}`;
1217
- for (const cat of catList) {
1218
- const e = mr.get(cat);
1219
- if (e) {
1220
- const pct = Math.round((1 - (e.rank - 1) / Math.max(1, e.total - 1)) * 100);
1221
- const pc = pct >= 80 ? color2.green : pct >= 50 ? color2.amber : color2.red;
1222
- row += `${pc(`#${e.rank} ${pct}%`.padEnd(cw + 2))}`;
1223
- } else {
1224
- row += color2.dim("\u2014".padEnd(cw + 2));
1225
- }
1226
- }
1227
- writeLine(row);
1228
- }
1229
- writeLine();
1230
- writeLine(color2.dim("#1 100% = best in category. \u2014 = not tested."));
1231
- writeLine();
1232
- writeLine(color2.dim("Pin: wstack setmodel set <role> <provider>/<model>"));
1233
- writeLine(
1234
- color2.dim(
1235
- "Full: wstack modeldiag eval Providers: wstack modeldiag eval --providers=id1,id2"
1236
- )
1237
- );
1238
- writeLine(
1239
- color2.dim("Max: wstack modeldiag eval --max=3 Quick: wstack modeldiag eval --quick")
1240
- );
1241
- return 0;
1278
+ return runModeldiagEval(args, deps, providers, config, hasKey);
1242
1279
  }
1243
- writeLine(`${color2.bold("API Key Status")}`);
1280
+ writeLine(`${color5.bold("API Key Status")}`);
1244
1281
  writeLine();
1245
1282
  for (const prov of providers) {
1246
1283
  const k = hasKey(prov.id);
1247
- writeLine(` ${checkMark(k)} ${color2.bold(prov.id.padEnd(18))} ${color2.dim(prov.name)}`);
1284
+ writeLine(` ${checkMark(k)} ${color5.bold(prov.id.padEnd(18))} ${color5.dim(prov.name)}`);
1248
1285
  }
1249
1286
  writeLine();
1250
- writeLine(`${color2.dim(`Leader: ${config.provider}/${config.model}`)}`);
1287
+ writeLine(`${color5.dim(`Leader: ${config.provider}/${config.model}`)}`);
1251
1288
  writeLine();
1252
- writeLine(`${color2.bold("Model Capabilities")} ${color2.dim("\u2014 matched to known profiles")}`);
1289
+ writeLine(`${color5.bold("Model Capabilities")} ${color5.dim("\u2014 matched to known profiles")}`);
1253
1290
  writeLine();
1254
1291
  for (const prov of providers) {
1255
1292
  if (!hasKey(prov.id)) continue;
1256
- writeLine(` ${color2.bold(prov.id)} ${color2.dim(`(${prov.name})`)}`);
1293
+ writeLine(` ${color5.bold(prov.id)} ${color5.dim(`(${prov.name})`)}`);
1257
1294
  const tiers = {
1258
1295
  premium: [],
1259
1296
  standard: [],
@@ -1267,17 +1304,17 @@ ${color2.dim('Enter numbers or provider IDs (comma-separated, or "all"):')}
1267
1304
  for (const tier of ["premium", "standard", "budget", "unknown"]) {
1268
1305
  const tierModels = tiers[tier];
1269
1306
  if (tierModels.length === 0) continue;
1270
- const label = tier === "unknown" ? color2.dim("unmatched") : `${costLabel(tier)} ${tier}`;
1307
+ const label = tier === "unknown" ? color5.dim("unmatched") : `${costLabel(tier)} ${tier}`;
1271
1308
  writeLine(` ${label}`);
1272
1309
  for (const m of tierModels) {
1273
1310
  const cap = m.capabilities;
1274
1311
  const ctx = cap?.contextWindow ?? 0;
1275
1312
  const maxOut = cap?.maxOutputTokens ?? 0;
1276
1313
  const profile = findProfile(prov.id, m.id);
1277
- const family = profile ? `${speedLabel(profile.speedTier)} ${color2.green(profile.family)}` : color2.dim("no profile match");
1278
- const pricing = m.pricing ? `${color2.dim("in")}${fmtPrice(m.pricing.input)} ${color2.dim("out")}${fmtPrice(m.pricing.output)}` : color2.dim("pricing ?");
1314
+ const family = profile ? `${speedLabel(profile.speedTier)} ${color5.green(profile.family)}` : color5.dim("no profile match");
1315
+ const pricing = m.pricing ? `${color5.dim("in")}${fmtPrice(m.pricing.input)} ${color5.dim("out")}${fmtPrice(m.pricing.output)}` : color5.dim("pricing ?");
1279
1316
  writeLine(
1280
- ` ${color2.cyan(m.id.padEnd(34))}${ctx > 0 ? `ctx ${fmtTokens(ctx).padEnd(6)}` : color2.dim("ctx ? ")}${maxOut > 0 ? `out ${fmtTokens(maxOut).padEnd(6)}` : " "}${family} ${pricing}`
1317
+ ` ${color5.cyan(m.id.padEnd(34))}${ctx > 0 ? `ctx ${fmtTokens(ctx).padEnd(6)}` : color5.dim("ctx ? ")}${maxOut > 0 ? `out ${fmtTokens(maxOut).padEnd(6)}` : " "}${family} ${pricing}`
1281
1318
  );
1282
1319
  }
1283
1320
  }
@@ -1285,11 +1322,11 @@ ${color2.dim('Enter numbers or provider IDs (comma-separated, or "all"):')}
1285
1322
  }
1286
1323
  await renderSuggest();
1287
1324
  writeLine();
1288
- writeLine(color2.dim("Pin a suggestion: wstack setmodel set <role> <provider>/<model>"));
1289
- writeLine(color2.dim('Test candidates: wstack modeldiag bench <role> "<test prompt>"'));
1325
+ writeLine(color5.dim("Pin a suggestion: wstack setmodel set <role> <provider>/<model>"));
1326
+ writeLine(color5.dim('Test candidates: wstack modeldiag bench <role> "<test prompt>"'));
1290
1327
  return 0;
1291
1328
  };
1292
1329
  export {
1293
1330
  modeldiagCmd
1294
1331
  };
1295
- //# sourceMappingURL=modeldiag-6VS5KDP5.js.map
1332
+ //# sourceMappingURL=modeldiag-ICMPHT5L.js.map