@wrongstack/cli 1.0.19 → 1.0.24

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 (124) hide show
  1. package/dist/{acp-INCNOH42.js → acp-AJWSKWE5.js} +18 -7
  2. package/dist/arg-parser.d.ts +1 -0
  3. package/dist/{auth-YXK3EDUL.js → auth-GRBZ7Y3D.js} +61 -15
  4. package/dist/auth-menu/direct.d.ts +1 -0
  5. package/dist/auth-menu/local.d.ts +2 -0
  6. package/dist/auth-menu/oauth-menu.d.ts +3 -1
  7. package/dist/auth-menu/panel-service.d.ts +20 -0
  8. package/dist/{bench-YBU6BVQP.js → bench-N5K7Z5O3.js} +2 -2
  9. package/dist/boot/config-backup-recovery.d.ts +2 -1
  10. package/dist/boot/dispatch-webui.d.ts +11 -0
  11. package/dist/boot/short-circuit-desktop.d.ts +1 -0
  12. package/dist/boot/tui-project-picker-callback.d.ts +1 -1
  13. package/dist/boot/tui-settings-adapter.d.ts +15 -2
  14. package/dist/{boot-auto-thin-MANHOYHE.js → boot-auto-thin-6Q56FY3F.js} +3 -2
  15. package/dist/{chunk-6WRKAACA.js → chunk-2BKFWIER.js} +4 -2
  16. package/dist/{chunk-JY52HWL6.js → chunk-2EBVTYJR.js} +2 -2
  17. package/dist/chunk-3OCUS3VN.js +46 -0
  18. package/dist/chunk-424ORSE7.js +56 -0
  19. package/dist/{chunk-KEPABQYT.js → chunk-5SEYGOCH.js} +8 -82
  20. package/dist/chunk-BBHWW6LR.js +549 -0
  21. package/dist/{chunk-CJTBLTS4.js → chunk-BLD5DSVA.js} +4 -3
  22. package/dist/{chunk-KK2LB3ZS.js → chunk-DRM7PSW7.js} +9 -5
  23. package/dist/{chunk-XJ7K67PP.js → chunk-JF2MULWF.js} +17 -10
  24. package/dist/{chunk-SIS37L3W.js → chunk-L2RVEQWO.js} +211 -193
  25. package/dist/{chunk-RDHFXOJM.js → chunk-LFMVHT3R.js} +106 -57
  26. package/dist/chunk-LUMETJFO.js +171 -0
  27. package/dist/{chunk-C3Z4N6A6.js → chunk-MHEBWVXL.js} +6 -2
  28. package/dist/{chunk-2ENZU66X.js → chunk-PGKJ2OQP.js} +5 -5
  29. package/dist/{chunk-672UXYL3.js → chunk-QBBF7GIK.js} +2 -2
  30. package/dist/{chunk-DE7BZDNK.js → chunk-QFPQXKCG.js} +11 -3
  31. package/dist/chunk-R4QGL6GB.js +10 -0
  32. package/dist/chunk-S4FFHDJG.js +93 -0
  33. package/dist/chunk-SFBFYIOW.js +271 -0
  34. package/dist/{chunk-JX6KL7OF.js → chunk-SKPDE62W.js} +4 -3
  35. package/dist/{chunk-RMQNCHGO.js → chunk-U46QR4WR.js} +5 -9
  36. package/dist/{chunk-B4JLSXZB.js → chunk-WSTVGC2C.js} +73 -45
  37. package/dist/chunk-YBUCXXYZ.js +36 -0
  38. package/dist/{chunk-2VMJ3E5Z.js → chunk-YTEOAWOJ.js} +6 -2
  39. package/dist/{chunk-GPPRNAKW.js → chunk-ZMX5HRLI.js} +2 -2
  40. package/dist/{chunk-YLQEGYV4.js → chunk-ZZRSG7VK.js} +14 -4
  41. package/dist/{cli-context-33F74CSZ.js → cli-context-34Q5IJRA.js} +121 -89
  42. package/dist/cli-entry-point.d.ts +2 -0
  43. package/dist/{cli-main-FNGMKPBA.js → cli-main-AELJDBGU.js} +1439 -1276
  44. package/dist/config-doctor.d.ts +10 -2
  45. package/dist/{config-transfer-2YF3CRTN.js → config-transfer-RUWLDCNK.js} +34 -13
  46. package/dist/{diag-doctor-ANPRUYFM.js → diag-doctor-WIFJH3XH.js} +2 -2
  47. package/dist/execute-deps.d.ts +2 -1
  48. package/dist/{execution-A6WJLSOC.js → execution-PYC5FCS4.js} +303 -256
  49. package/dist/{hq-XQWQTRDA.js → hq-JTUTFI4L.js} +274 -258
  50. package/dist/hq-command-controller.d.ts +1 -1
  51. package/dist/{hq-server-IIBZQZU4.js → hq-server-PC7GZMPT.js} +2 -2
  52. package/dist/{hq-service-L5INQFM4.js → hq-service-NW56MUP2.js} +11 -4
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.js +17 -12
  55. package/dist/kanban-supervisor-67ECABTD.js +7 -0
  56. package/dist/live-settings-input.d.ts +7 -0
  57. package/dist/{mailbox-serve-3GFOK2I7.js → mailbox-serve-WDOCH6KR.js} +3 -3
  58. package/dist/{mcp-DC5WSBOO.js → mcp-LBS45HWZ.js} +4 -4
  59. package/dist/modeldiag-36OGAL5C.js +828 -0
  60. package/dist/{per-subcommand-help-DHTYU524.js → per-subcommand-help-XE77MTGJ.js} +3 -3
  61. package/dist/permission-prompt-mirror.d.ts +1 -1
  62. package/dist/permission-prompt.d.ts +2 -2
  63. package/dist/plugin-config.d.ts +12 -0
  64. package/dist/plugin-management-types.d.ts +31 -0
  65. package/dist/plugin-management.d.ts +2 -30
  66. package/dist/plugin-package-install.d.ts +3 -0
  67. package/dist/{plugin-usage-M6KET4OZ.js → plugin-usage-S5LVGMBW.js} +6 -5
  68. package/dist/{plugins-WJLW6ULV.js → plugins-RHBIVLI7.js} +4 -3
  69. package/dist/provider-config-utils.d.ts +1 -6
  70. package/dist/{providers-models-LFNZ5WI7.js → providers-models-2CFG46RD.js} +41 -31
  71. package/dist/release-asset-download.d.ts +3 -0
  72. package/dist/services/dispatch-classifier.d.ts +39 -5
  73. package/dist/{sessions-config-UL45XKX7.js → sessions-config-IQUH2GXT.js} +13 -5
  74. package/dist/settings-menu.d.ts +2 -0
  75. package/dist/short-circuit-flags-SDT53OML.js +10 -0
  76. package/dist/{simpleui-dist-CXSIDAXZ.js → simpleui-dist-7BQISSEP.js} +7 -3
  77. package/dist/skill-suggest-L2EVWB6R.js +290 -0
  78. package/dist/slash-commands/effort.d.ts +1 -1
  79. package/dist/slash-commands/fallback.d.ts +1 -1
  80. package/dist/slash-commands/gitid.d.ts +1 -1
  81. package/dist/slash-commands/jev.d.ts +4 -0
  82. package/dist/slash-commands/openai-quota.d.ts +15 -0
  83. package/dist/slash-commands/sdd-task-mutations.d.ts +4 -0
  84. package/dist/slash-commands/settings-keys.d.ts +2 -0
  85. package/dist/slash-commands/settings-reset.d.ts +6 -0
  86. package/dist/standalone-update-MW47J3AK.js +164 -0
  87. package/dist/standalone-update.d.ts +19 -0
  88. package/dist/subcommands/handlers/hq-alerts.d.ts +3 -0
  89. package/dist/subcommands/handlers/hq-audit.d.ts +46 -0
  90. package/dist/subcommands/handlers/hq-data-dir.d.ts +13 -0
  91. package/dist/subcommands/handlers/skill-suggest.d.ts +21 -0
  92. package/dist/subcommands/handlers/typesafe-brain-replay.d.ts +26 -0
  93. package/dist/subcommands/handlers/typesafe-judgment-check.d.ts +3 -0
  94. package/dist/subcommands/handlers/typesafe-topic-replay.d.ts +19 -0
  95. package/dist/subcommands/handlers/typesafe-triage-replay.d.ts +18 -0
  96. package/dist/subcommands/handlers/typesafe.d.ts +23 -0
  97. package/dist/{subcommands-XSLXMIDP.js → subcommands-ZTSIN7LG.js} +2 -2
  98. package/dist/typesafe-HVEVPA6R.js +316 -0
  99. package/dist/typesafe-brain-replay-C5276FVR.js +222 -0
  100. package/dist/typesafe-judgment-check-GGYYV5UF.js +29 -0
  101. package/dist/typesafe-topic-replay-4W73BOLV.js +218 -0
  102. package/dist/typesafe-triage-replay-FGAW7QM7.js +215 -0
  103. package/dist/{update-5DPB4YD4.js → update-7XIOEXTD.js} +4 -3
  104. package/dist/{update-check-BS3DHTSI.js → update-check-2POMBHFB.js} +3 -2
  105. package/dist/update-check.d.ts +1 -1
  106. package/dist/version-help-WZ623NWM.js +10 -0
  107. package/dist/version.d.ts +3 -2
  108. package/dist/webui-client-observability.d.ts +7 -0
  109. package/dist/{webui-server-MRPGTTKL.js → webui-server-L2SICPTB.js} +158 -94
  110. package/dist/webui-server-options.d.ts +11 -0
  111. package/dist/webui-server.d.ts +15 -1
  112. package/dist/wiring/domain-glossary.d.ts +5 -4
  113. package/dist/wiring/plugins.d.ts +1 -1
  114. package/dist/wiring/provider-persisters.d.ts +24 -0
  115. package/dist/wiring/provider-runtime.d.ts +9 -1
  116. package/dist/wiring/runtime-dispatch-state.d.ts +3 -1
  117. package/package.json +28 -28
  118. package/dist/chunk-2CZEORLV.js +0 -54
  119. package/dist/chunk-DR2OXU46.js +0 -174
  120. package/dist/chunk-JACOMTIH.js +0 -275
  121. package/dist/chunk-XJXDOF63.js +0 -28
  122. package/dist/modeldiag-CJSFHZ3J.js +0 -1356
  123. package/dist/short-circuit-flags-7GQIGMVD.js +0 -10
  124. package/dist/version-help-WXI42PZ3.js +0 -10
@@ -1,1356 +0,0 @@
1
- import {
2
- discoverAndMergeProviders
3
- } from "./chunk-D5GB5MHA.js";
4
- import {
5
- activeProfileConfigPath
6
- } from "./chunk-YMXXOOFN.js";
7
- import {
8
- mutateConfigProviders,
9
- normalizeKeys,
10
- writeKeysBack
11
- } from "./chunk-DR2OXU46.js";
12
-
13
- // src/subcommands/handlers/modeldiag.ts
14
- import * as fs from "node:fs/promises";
15
- import { color as color5 } from "@wrongstack/core/utils";
16
-
17
- // src/subcommands/handlers/modeldiag-bench.ts
18
- import { color as color3, toErrorMessage } from "@wrongstack/core/utils";
19
-
20
- // src/subcommands/handlers/modeldiag-eval.ts
21
- import { color as color2 } from "@wrongstack/core/utils";
22
- import { makeProviderFromConfig } from "@wrongstack/providers";
23
-
24
- // src/subcommands/handlers/modeldiag-profiles.ts
25
- import { color } from "@wrongstack/core/utils";
26
- var MODEL_PROFILES = [
27
- {
28
- provider: "anthropic",
29
- pattern: /claude-opus/i,
30
- family: "Claude Opus",
31
- strengths: ["reasoning", "planning"],
32
- bestFor: ["planning", "security", "debugging"],
33
- costTier: "premium",
34
- speedTier: "slow"
35
- },
36
- {
37
- provider: "anthropic",
38
- pattern: /claude-haiku/i,
39
- family: "Claude Haiku",
40
- strengths: ["speed"],
41
- bestFor: ["lightweight", "docs"],
42
- avoidFor: ["planning"],
43
- costTier: "budget",
44
- speedTier: "fast"
45
- },
46
- {
47
- provider: "openai",
48
- pattern: /gpt-5|o3|o4/i,
49
- family: "GPT-5/o3/o4",
50
- strengths: ["reasoning", "coding"],
51
- bestFor: ["planning", "coding", "debugging"],
52
- costTier: "premium",
53
- speedTier: "normal"
54
- },
55
- {
56
- provider: "openai",
57
- // Must precede /gpt-4/ — every gpt-4o-mini id also contains gpt-4.
58
- pattern: /gpt-4o-mini/i,
59
- family: "GPT-4o Mini",
60
- strengths: ["speed"],
61
- bestFor: ["lightweight", "docs"],
62
- avoidFor: ["planning"],
63
- costTier: "budget",
64
- speedTier: "fast"
65
- },
66
- {
67
- provider: "openai",
68
- pattern: /gpt-4/i,
69
- family: "GPT-4",
70
- strengths: ["coding"],
71
- bestFor: ["coding", "docs"],
72
- costTier: "standard",
73
- speedTier: "fast"
74
- },
75
- {
76
- provider: "google",
77
- // Must precede /gemini-(?:2\.5|3)/ — flash variants (gemini-2.5-flash,
78
- // gemini-3-flash, …) are budget/fast models and need this profile.
79
- pattern: /gemini.*flash/i,
80
- family: "Gemini Flash",
81
- strengths: ["speed"],
82
- bestFor: ["lightweight", "docs"],
83
- avoidFor: ["planning"],
84
- costTier: "budget",
85
- speedTier: "fast"
86
- },
87
- {
88
- provider: "google",
89
- pattern: /gemini-(?:2\.5|3)/i,
90
- family: "Gemini 2.5/3",
91
- strengths: ["context", "coding"],
92
- bestFor: ["coding", "data"],
93
- costTier: "standard",
94
- speedTier: "normal"
95
- },
96
- {
97
- provider: "deepseek",
98
- pattern: /deepseek/i,
99
- family: "DeepSeek",
100
- strengths: ["coding", "cost-effective"],
101
- bestFor: ["coding", "general"],
102
- costTier: "standard",
103
- speedTier: "normal"
104
- }
105
- ];
106
- function fmtTokens(n) {
107
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
108
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
109
- return String(n);
110
- }
111
- function fmtMs(ms) {
112
- if (ms >= 1e3) return `${(ms / 1e3).toFixed(1)}s`;
113
- return `${ms}ms`;
114
- }
115
- function fmtPrice(usdPer1M) {
116
- if (usdPer1M === void 0) return color.dim("?");
117
- if (usdPer1M >= 10) return `$${usdPer1M.toFixed(1)}`;
118
- return `$${usdPer1M.toFixed(2)}`;
119
- }
120
- function checkMark(ok) {
121
- return ok ? color.green("\u2713") : color.red("\u2717");
122
- }
123
- function costLabel(tier) {
124
- switch (tier) {
125
- case "premium":
126
- return color.red("$$$");
127
- case "standard":
128
- return color.amber("$$");
129
- case "budget":
130
- return color.green("$");
131
- default:
132
- return color.dim("?");
133
- }
134
- }
135
- function speedLabel(tier) {
136
- switch (tier) {
137
- case "fast":
138
- return color.green("\u26A1");
139
- case "normal":
140
- return color.amber("\u2192");
141
- case "slow":
142
- return color.red("\u{1F422}");
143
- default:
144
- return color.dim("?");
145
- }
146
- }
147
- function scoreBar(score, max) {
148
- const pct = Math.min(1, Math.max(0, score / max));
149
- const filled = Math.round(pct * 10);
150
- const bar = color.green("\u2588".repeat(filled)) + color.dim("\u2591".repeat(10 - filled));
151
- return `${bar} ${score}/${max}`;
152
- }
153
- var ROLE_CATEGORY = {
154
- "security-scanner": "security",
155
- "security-reviewer": "security",
156
- "bug-hunter": "debugging",
157
- debugger: "debugging",
158
- tracer: "debugging",
159
- planner: "planning",
160
- architect: "planning",
161
- "refactor-planner": "planning",
162
- verifier: "testing",
163
- test: "testing",
164
- e2e: "testing",
165
- document: "docs",
166
- simplifier: "docs",
167
- "code-reviewer": "review",
168
- reviewer: "review",
169
- critic: "review",
170
- executor: "coding",
171
- refactor: "refactoring",
172
- migration: "coding",
173
- frontend: "frontend",
174
- backend: "backend",
175
- api: "backend",
176
- auth: "backend",
177
- designer: "frontend",
178
- analyst: "data",
179
- data: "data",
180
- database: "data",
181
- explore: "planning",
182
- search: "planning",
183
- researcher: "planning"
184
- };
185
- function findProfile(pid, mid) {
186
- for (const p of MODEL_PROFILES) {
187
- if (p.provider === pid && p.pattern.test(mid)) return p;
188
- }
189
- return void 0;
190
- }
191
- function scoreModel(pid, mid, category, ctxWindow) {
192
- const profile = findProfile(pid, mid);
193
- let score = 50;
194
- if (profile) {
195
- if (profile.bestFor.includes(category)) score += 35;
196
- if (profile.avoidFor?.includes(category)) score -= 50;
197
- if (category === "planning" && profile.costTier === "premium") score += 15;
198
- if (profile.speedTier === "slow" && category === "planning") score += 10;
199
- if (profile.costTier === "budget" && category !== "planning" && category !== "security")
200
- score += 10;
201
- }
202
- if (ctxWindow > 2e5) score += 10;
203
- else if (ctxWindow > 1e5) score += 5;
204
- else if (ctxWindow > 32e3) score += 2;
205
- return { score, profile };
206
- }
207
- function rankModels(providers, hasKey, category, limit) {
208
- const candidates = [];
209
- for (const prov of providers) {
210
- if (!hasKey(prov.id)) continue;
211
- for (const m of prov.models ?? []) {
212
- const ctxWindow = m.capabilities?.contextWindow ?? 0;
213
- const { score, profile } = scoreModel(prov.id, m.id, category, ctxWindow);
214
- if (score > 0) {
215
- candidates.push({
216
- provider: prov.id,
217
- model: m.id,
218
- profile,
219
- score,
220
- ctxWindow,
221
- maxOutput: m.capabilities?.maxOutputTokens ?? 0,
222
- inputPrice: m.pricing?.input,
223
- outputPrice: m.pricing?.output
224
- });
225
- }
226
- }
227
- }
228
- candidates.sort((a, b) => b.score - a.score);
229
- return candidates.slice(0, limit);
230
- }
231
- var EVAL_TASKS = {
232
- coding: {
233
- label: "Code Generation",
234
- prompt: "Write a TypeScript function parseCSV(input: string): { headers: string[]; rows: string[][] } that handles quoted fields, escaped quotes, and empty lines. Return an error string on malformed input. Keep under 40 lines."
235
- },
236
- planning: {
237
- label: "Architecture Planning",
238
- prompt: "Design the folder structure and key interfaces for a monorepo CLI tool with slash commands, model routing, subagent spawning, and config persistence. List packages, their responsibilities, and the 5 most important TypeScript interfaces."
239
- },
240
- security: {
241
- label: "Vulnerability Detection",
242
- prompt: 'Review this code for security issues:\n```ts\napp.get("/api/user", (req, res) => {\n const id = req.query.id;\n const user = db.query("SELECT * FROM users WHERE id = " + id);\n res.json(user);\n});\n\napp.post("/api/run", (req, res) => {\n const { cmd } = req.body;\n exec("echo " + cmd, (err, stdout) => res.send(stdout));\n});\n```\nList every vulnerability, its severity (critical/high/medium), and the exact fix.'
243
- },
244
- debugging: {
245
- label: "Bug Diagnosis",
246
- prompt: 'This async function has 2 bugs. Find and fix both:\n```ts\nasync function processBatch(items: string[]) {\n const results = [];\n for (const item of items) {\n const result = await fetch("https://api.example.com/" + item);\n results.push(result);\n }\n return results.map(r => r.json());\n}\n```\nExplain what each bug is, why it fails, and write the corrected version.'
247
- },
248
- testing: {
249
- label: "Test Authoring",
250
- prompt: 'Write vitest test cases for this deepMerge function:\n```ts\nfunction deepMerge(base: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {\n const merged = { ...base };\n for (const [key, val] of Object.entries(overrides)) {\n if (val === null) { delete merged[key]; continue; }\n if (typeof val === "object" && !Array.isArray(val) && typeof merged[key] === "object" && !Array.isArray(merged[key])) {\n merged[key] = deepMerge(merged[key] as Record<string, unknown>, val as Record<string, unknown>);\n } else { merged[key] = val; }\n }\n return merged;\n}\n```\nCover: happy path, edge cases, and error conditions.'
251
- },
252
- docs: {
253
- label: "Documentation",
254
- prompt: "Write TSDoc comments for this RateLimiter interface. Include @param, @returns, @throws, and @example for each method:\n```ts\ninterface RateLimiter {\n tryAcquire(key: string, maxPerWindow: number, windowMs: number): Promise<boolean>;\n getRemaining(key: string): Promise<number>;\n reset(key: string): Promise<void>;\n}\n```"
255
- },
256
- review: {
257
- label: "Code Review",
258
- prompt: 'Review this PR change:\n```diff\n async function loadConfig(path: string) {\n- const raw = await fs.readFile(path, "utf8");\n- return JSON.parse(raw);\n+ const raw = await fs.readFile(path);\n+ const config = JSON.parse(raw);\n+ process.env.API_KEY = config.apiKey;\n+ return config;\n }\n```\nList issues by severity (blocking / should-fix / nit) and explain your reasoning.'
259
- },
260
- refactoring: {
261
- label: "Refactoring",
262
- prompt: 'Refactor this nested condition into a cleaner pattern:\n```ts\nfunction getDiscount(user: { type: string; years: number; coupon?: string }): number {\n if (user.type === "premium") {\n if (user.years > 5) {\n if (user.coupon === "BLACKFRIDAY") return 0.5;\n return 0.3;\n }\n return 0.2;\n }\n if (user.type === "standard") {\n if (user.years > 3) return 0.15;\n return 0.1;\n }\n return 0;\n}\n```\nShow your refactored code and explain why your approach is cleaner.'
263
- }
264
- };
265
- var EVAL_CATEGORIES = Object.keys(EVAL_TASKS);
266
- function roleCat(role) {
267
- return ROLE_CATEGORY[role] ?? "general";
268
- }
269
-
270
- // src/subcommands/handlers/modeldiag-eval.ts
271
- function createProviderForId(providerId, cfg) {
272
- const savedCfg = cfg.providers?.[providerId];
273
- const cfgWithType = Object.assign(
274
- { type: providerId },
275
- savedCfg ?? { apiKey: cfg.apiKey, baseUrl: cfg.baseUrl }
276
- );
277
- try {
278
- return makeProviderFromConfig(providerId, cfgWithType);
279
- } catch {
280
- return void 0;
281
- }
282
- }
283
- async function rankResponses(provider, leaderModel, taskPrompt, responses) {
284
- const labelToIdx = /* @__PURE__ */ new Map();
285
- const responseBlock = responses.map((r, i) => {
286
- const label = String.fromCharCode(65 + i);
287
- labelToIdx.set(label, i);
288
- return `=== Response ${label} ===
289
- ${r.text.slice(0, 800)}`;
290
- }).join("\n\n");
291
- const rankingPrompt = `Rank these responses from BEST (1) to WORST.
292
-
293
- TASK:
294
- ${taskPrompt.slice(0, 600)}
295
-
296
- RESPONSES:
297
- ${responseBlock}
298
-
299
- Output ONLY a ranked list, one per line:
300
- 1. Response X \u2014 brief reason
301
- 2. Response Y \u2014 brief reason`;
302
- try {
303
- const resp = await provider.complete(
304
- {
305
- model: leaderModel,
306
- system: [
307
- {
308
- type: "text",
309
- text: "You are an expert evaluator. Rank responses concisely. Output ONLY the ranked list."
310
- }
311
- ],
312
- messages: [{ role: "user", content: [{ type: "text", text: rankingPrompt }] }],
313
- maxTokens: 400
314
- },
315
- { signal: AbortSignal.timeout(3e4) }
316
- );
317
- const text = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
318
- const rankings = [];
319
- for (const line of text.split("\n")) {
320
- const m = line.match(/^\s*(\d+)[.)]\s*Response\s+([A-Z])/i);
321
- if (m) {
322
- const label = m[2]?.toUpperCase();
323
- const idx = label !== void 0 ? labelToIdx.get(label) : void 0;
324
- if (idx !== void 0 && !rankings.includes(idx)) {
325
- rankings.push(idx);
326
- }
327
- }
328
- }
329
- return rankings.length > 0 ? rankings : responses.map((_, i) => i);
330
- } catch {
331
- return responses.map((_, i) => i);
332
- }
333
- }
334
- async function runModeldiagEval(args, deps, providers, config, hasKey) {
335
- const writeLine = (line = "") => {
336
- deps.renderer.write(`${line}
337
- `);
338
- };
339
- const evalArgs = args.slice(1);
340
- const providersEq = evalArgs.find((a) => a.startsWith("--providers="));
341
- const providersValue = providersEq?.replace("--providers=", "") ?? (typeof deps.flags?.["providers"] === "string" ? deps.flags["providers"] : typeof deps.flags?.["provider"] === "string" ? deps.flags["provider"] : void 0);
342
- const providerFilter = providersValue ? providersValue.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
343
- const maxEq = evalArgs.find((a) => a.startsWith("--max="));
344
- const maxValue = maxEq?.replace("--max=", "") ?? (typeof deps.flags?.["max"] === "string" ? deps.flags["max"] : void 0);
345
- const maxModels = maxValue ? Math.max(1, parseInt(maxValue, 10) || 2) : 2;
346
- const quick = evalArgs.includes("--quick") || deps.flags?.["quick"] === true || deps.flags?.["quick"] === "true";
347
- const modelsPerCat = quick ? 1 : maxModels;
348
- const roleFilter = evalArgs.find((a) => !a.startsWith("--"));
349
- const targetCategories = roleFilter ? EVAL_CATEGORIES.includes(roleCat(roleFilter)) ? [roleCat(roleFilter)] : [] : EVAL_CATEGORIES;
350
- if (targetCategories.length === 0 && roleFilter) {
351
- writeLine(
352
- `${color2.amber("Unknown role/category")}: "${roleFilter}". Try: ${EVAL_CATEGORIES.join(", ")}`
353
- );
354
- return 1;
355
- }
356
- const keyedProviderIds = providers.filter((p) => hasKey(p.id)).map((p) => p.id);
357
- let targetProviderIds;
358
- if (providerFilter && providerFilter.length > 0) {
359
- const unknown = providerFilter.filter((pid) => !keyedProviderIds.includes(pid));
360
- targetProviderIds = providerFilter.filter((pid) => keyedProviderIds.includes(pid));
361
- if (targetProviderIds.length === 0) {
362
- 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.";
363
- writeLine(`${color2.amber(noKeyMsg)}`);
364
- return 0;
365
- }
366
- } else if (keyedProviderIds.length === 0) {
367
- writeLine(`${color2.amber("No providers have API keys. Add keys with wstack auth.")}`);
368
- return 0;
369
- } else if (keyedProviderIds.length === 1) {
370
- targetProviderIds = keyedProviderIds;
371
- } else {
372
- const providerList = keyedProviderIds.map((pid, i) => {
373
- const info = providers.find((p) => p.id === pid);
374
- return ` ${color2.cyan(String(i + 1))}) ${color2.bold(pid.padEnd(16))} ${color2.dim(info?.name ?? "")}`;
375
- }).join("\n");
376
- deps.renderer.write(
377
- `
378
- ${color2.bold("Select providers to evaluate")}
379
-
380
- ${providerList}
381
-
382
- ${color2.dim('Enter numbers or provider IDs (comma-separated, or "all"):')}
383
- `
384
- );
385
- const input = await deps.reader.readLine(" > ");
386
- const selected = input.trim().toLowerCase();
387
- if (selected === "" || selected === "all") {
388
- targetProviderIds = keyedProviderIds;
389
- } else {
390
- targetProviderIds = [];
391
- for (const part of selected.split(",").map((s) => s.trim())) {
392
- const idx = parseInt(part, 10);
393
- if (idx >= 1 && idx <= keyedProviderIds.length) {
394
- const pid = keyedProviderIds[idx - 1];
395
- if (!targetProviderIds.includes(pid)) targetProviderIds.push(pid);
396
- } else if (keyedProviderIds.includes(part)) {
397
- if (!targetProviderIds.includes(part)) targetProviderIds.push(part);
398
- }
399
- }
400
- }
401
- if (targetProviderIds.length === 0) {
402
- writeLine(color2.dim("No providers selected."));
403
- return 0;
404
- }
405
- }
406
- const leaderModel = config.model ?? "unknown";
407
- const unknownProviders = providerFilter ? providerFilter.filter((pid) => !keyedProviderIds.includes(pid)) : [];
408
- const warningLine = unknownProviders.length > 0 ? ` ${color2.amber("\u26A0 skipped (no key):")} ${unknownProviders.join(", ")}
409
- ` : "";
410
- writeLine(`${color2.bold("Model Competency Evaluation")}`);
411
- writeLine(
412
- color2.dim(
413
- `Providers: ${targetProviderIds.join(", ")} | ${targetCategories.length} cats | ${modelsPerCat} model(s)/cat/provider`
414
- )
415
- );
416
- writeLine(warningLine);
417
- writeLine(color2.dim(`Leader (ranker): ${config.provider}/${leaderModel}`));
418
- writeLine();
419
- const collected = /* @__PURE__ */ new Map();
420
- let total = 0;
421
- let ok = 0;
422
- for (const pid of targetProviderIds) {
423
- const prov = createProviderForId(pid, config);
424
- if (!prov) {
425
- writeLine(color2.dim(` \u2298 ${pid}: provider unavailable, skipping`));
426
- continue;
427
- }
428
- for (const cat of targetCategories) {
429
- const task = EVAL_TASKS[cat];
430
- if (!task) continue;
431
- const candidates = rankModels(
432
- providers,
433
- (providerId) => providerId === pid && hasKey(providerId),
434
- cat,
435
- modelsPerCat
436
- );
437
- if (candidates.length === 0) continue;
438
- if (!collected.has(cat)) collected.set(cat, /* @__PURE__ */ new Map());
439
- for (const c of candidates) {
440
- total++;
441
- const modelKey = `${pid}/${c.model}`;
442
- try {
443
- const start = Date.now();
444
- const resp = await prov.complete(
445
- {
446
- model: c.model,
447
- system: [{ type: "text", text: "Be thorough and correct." }],
448
- messages: [{ role: "user", content: [{ type: "text", text: task.prompt }] }],
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 {
692
- buildProviderFactoriesFromRegistry,
693
- makeProviderFromConfig as makeProviderFromConfig2,
694
- setOAuthTokenPersister
695
- } from "@wrongstack/providers";
696
-
697
- // src/subcommands/handlers/model-smoke-test.ts
698
- import { ProviderError } from "@wrongstack/core/types";
699
- import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
700
- var DEFAULT_TIMEOUT_MS = 45e3;
701
- var DEFAULT_MAX_TOKENS = 32;
702
- function csv(value) {
703
- if (typeof value !== "string") return void 0;
704
- const values = value.split(",").map((part) => part.trim()).filter(Boolean);
705
- return values.length > 0 ? values : void 0;
706
- }
707
- function positiveInteger(value, fallback, label) {
708
- if (value === void 0) return fallback;
709
- const parsed = typeof value === "string" ? Number(value) : Number.NaN;
710
- if (!Number.isSafeInteger(parsed) || parsed <= 0) {
711
- throw new Error(`${label} must be a positive integer.`);
712
- }
713
- return parsed;
714
- }
715
- function parseModelSmokeOptions(args, flags = {}) {
716
- const local = /* @__PURE__ */ new Map();
717
- for (let i = 0; i < args.length; i++) {
718
- const arg = args[i];
719
- if (!arg?.startsWith("--")) continue;
720
- const eq = arg.indexOf("=");
721
- if (eq >= 0) {
722
- local.set(arg.slice(2, eq), arg.slice(eq + 1));
723
- continue;
724
- }
725
- const name = arg.slice(2);
726
- const next = args[i + 1];
727
- if (next && !next.startsWith("--")) {
728
- local.set(name, next);
729
- i++;
730
- } else {
731
- local.set(name, true);
732
- }
733
- }
734
- const get = (name) => local.get(name) ?? flags[name];
735
- const providerFilter = csv(get("providers") ?? get("provider"));
736
- const modelFilter = csv(get("models") ?? get("model"));
737
- return {
738
- allModels: get("all-models") === true || get("all-models") === "true",
739
- planOnly: get("plan") === true || get("plan") === "true",
740
- json: get("json") === true || get("json") === "true",
741
- timeoutMs: positiveInteger(get("timeout"), DEFAULT_TIMEOUT_MS, "--timeout"),
742
- maxTokens: positiveInteger(get("max-tokens"), DEFAULT_MAX_TOKENS, "--max-tokens"),
743
- ...providerFilter ? { providerFilter } : {},
744
- ...modelFilter ? { modelFilter } : {}
745
- };
746
- }
747
- function pushUnique(target, values) {
748
- const seen = new Set(target);
749
- for (const value of values) {
750
- const normalized = value?.trim();
751
- if (!normalized || seen.has(normalized)) continue;
752
- seen.add(normalized);
753
- target.push(normalized);
754
- }
755
- }
756
- async function catalogModelsFor(providerId, providerConfig, registry) {
757
- const direct = await registry.getProvider(providerId).catch(() => void 0);
758
- const inherited = direct ?? (providerConfig?.type && providerConfig.type !== providerId ? await registry.getProvider(providerConfig.type).catch(() => void 0) : void 0);
759
- return inherited?.models ?? [];
760
- }
761
- function supportsTextCompletion(model) {
762
- const outputs = model?.modalities?.output;
763
- return !outputs?.length || outputs.includes("text");
764
- }
765
- async function buildModelSmokeTargets(config, registry, options) {
766
- const providerIds = Object.keys(config.providers ?? {});
767
- if (config.provider && !providerIds.includes(config.provider)) providerIds.push(config.provider);
768
- const providerFilter = options.providerFilter ? new Set(options.providerFilter.map((id) => id.toLowerCase())) : void 0;
769
- const modelFilter = options.modelFilter ? new Set(options.modelFilter) : void 0;
770
- const targets = [];
771
- for (const providerId of providerIds) {
772
- if (providerFilter && !providerFilter.has(providerId.toLowerCase())) continue;
773
- const saved = config.providers?.[providerId];
774
- const catalogModels = await catalogModelsFor(providerId, saved, registry);
775
- const catalogById = new Map(catalogModels.map((model) => [model.id, model]));
776
- const textCatalogModels = catalogModels.filter(supportsTextCompletion);
777
- const candidates = [];
778
- if (options.allModels) {
779
- pushUnique(candidates, saved?.models ?? []);
780
- pushUnique(candidates, Object.keys(saved?.customModels ?? {}));
781
- pushUnique(
782
- candidates,
783
- textCatalogModels.map((model) => model.id)
784
- );
785
- if (providerId === config.provider) pushUnique(candidates, [config.model]);
786
- pushUnique(candidates, [saved?.model]);
787
- } else {
788
- 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;
789
- pushUnique(candidates, [representative]);
790
- }
791
- for (const modelId of candidates) {
792
- if (modelFilter && !modelFilter.has(modelId)) continue;
793
- if (!supportsTextCompletion(catalogById.get(modelId))) continue;
794
- targets.push({ providerId, modelId });
795
- }
796
- }
797
- return targets;
798
- }
799
- function classifyFailure(err) {
800
- if (err instanceof ProviderError || ProviderError.isProviderError(err)) {
801
- const providerError = err;
802
- return {
803
- error: providerError.describe(),
804
- errorKind: providerError.kind,
805
- httpStatus: providerError.status
806
- };
807
- }
808
- const isTimeout = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError") || /timed?\s*out/i.test(toErrorMessage2(err));
809
- return {
810
- error: toErrorMessage2(err),
811
- errorKind: isTimeout ? "timeout" : "unknown"
812
- };
813
- }
814
- async function runModelSmokeTests(params) {
815
- const results = [];
816
- let providerId;
817
- let provider;
818
- let providerError;
819
- for (let index = 0; index < params.targets.length; index++) {
820
- const target = params.targets[index];
821
- params.onTargetStart?.(target, index, params.targets.length);
822
- const started = Date.now();
823
- if (target.providerId !== providerId) {
824
- providerId = target.providerId;
825
- provider = void 0;
826
- providerError = void 0;
827
- try {
828
- provider = await params.createProvider(target.providerId);
829
- } catch (err) {
830
- providerError = err;
831
- }
832
- }
833
- if (!provider) {
834
- const failure = classifyFailure(providerError ?? new Error("Provider could not be created."));
835
- const result = {
836
- providerId: target.providerId,
837
- modelId: target.modelId,
838
- status: "failed",
839
- latencyMs: Date.now() - started,
840
- ...failure
841
- };
842
- results.push(result);
843
- params.onTargetComplete?.(result, index, params.targets.length);
844
- continue;
845
- }
846
- try {
847
- const response = await provider.complete(
848
- {
849
- model: target.modelId,
850
- messages: [
851
- {
852
- role: "user",
853
- content: [{ type: "text", text: "Reply with exactly: OK" }]
854
- }
855
- ],
856
- maxTokens: params.options.maxTokens
857
- },
858
- { signal: AbortSignal.timeout(params.options.timeoutMs) }
859
- );
860
- const result = {
861
- providerId: target.providerId,
862
- modelId: target.modelId,
863
- status: "passed",
864
- latencyMs: Date.now() - started,
865
- stopReason: response.stopReason,
866
- usage: response.usage
867
- };
868
- results.push(result);
869
- params.onTargetComplete?.(result, index, params.targets.length);
870
- } catch (err) {
871
- const result = {
872
- providerId: target.providerId,
873
- modelId: target.modelId,
874
- status: "failed",
875
- latencyMs: Date.now() - started,
876
- ...classifyFailure(err)
877
- };
878
- results.push(result);
879
- params.onTargetComplete?.(result, index, params.targets.length);
880
- }
881
- }
882
- return results;
883
- }
884
-
885
- // src/subcommands/handlers/modeldiag-test.ts
886
- async function createModelDiagSmokeProvider(params) {
887
- const { providerId, config, modelsRegistry, providerFactories } = params;
888
- const saved = config.providers?.[providerId];
889
- const factoryType = saved?.type ?? providerId;
890
- const resolved = await modelsRegistry.getProvider(providerId).catch(() => void 0) ?? (factoryType !== providerId ? await modelsRegistry.getProvider(factoryType).catch(() => void 0) : void 0);
891
- const providerConfig = {
892
- ...providerId === config.provider ? {
893
- ...config.apiKey ? { apiKey: config.apiKey } : {},
894
- ...config.baseUrl ? { baseUrl: config.baseUrl } : {}
895
- } : {},
896
- ...saved,
897
- // Keep the user-visible alias on the factory input. The selected factory
898
- // already captures the canonical models.dev provider and its per-model
899
- // wire metadata.
900
- type: providerId,
901
- ...saved?.family ?? resolved?.family ? { family: saved?.family ?? resolved?.family } : {},
902
- ...saved?.baseUrl ?? resolved?.apiBase ? { baseUrl: saved?.baseUrl ?? resolved?.apiBase } : {},
903
- ...saved?.envVars ?? resolved?.envVars ? { envVars: saved?.envVars ?? resolved?.envVars } : {}
904
- };
905
- const factory = providerFactories.get(factoryType);
906
- return factory ? factory.create(providerConfig) : makeProviderFromConfig2(providerId, providerConfig);
907
- }
908
- async function runModeldiagTest(args, deps, config) {
909
- const writeLine = (line = "") => {
910
- deps.renderer.write(`${line}
911
- `);
912
- };
913
- if (args.slice(1).includes("--help") || deps.flags?.["help"] === true) {
914
- writeLine(`${color4.bold("Usage:")} wstack modeldiag test [options]`);
915
- writeLine();
916
- writeLine(" (no flags) Probe one representative model per configured provider");
917
- writeLine(" --all-models Probe every catalog-visible model sequentially");
918
- writeLine(" --plan Show targets without making model calls");
919
- writeLine(" --provider=<id,...> Restrict provider ids");
920
- writeLine(" --model=<id,...> Restrict model ids");
921
- writeLine(" --timeout=<ms> Per-model timeout (default 45000)");
922
- writeLine(" --max-tokens=<n> Output cap for the tiny probe (default 32)");
923
- writeLine(" --json Emit machine-readable output");
924
- writeLine(" --yes Confirm a run larger than 50 requests");
925
- return 0;
926
- }
927
- let smokeOptions;
928
- try {
929
- smokeOptions = parseModelSmokeOptions(args.slice(1), deps.flags);
930
- } catch (err) {
931
- writeLine(`${color4.red("Invalid smoke-test options:")} ${toErrorMessage3(err)}`);
932
- return 2;
933
- }
934
- if ("mergeOverlay" in deps.modelsRegistry) {
935
- await discoverAndMergeProviders({
936
- config,
937
- registry: deps.modelsRegistry,
938
- cacheDir: deps.paths.cacheDir
939
- });
940
- }
941
- const targets = await buildModelSmokeTargets(
942
- config,
943
- deps.modelsRegistry,
944
- smokeOptions
945
- );
946
- const byProvider = /* @__PURE__ */ new Map();
947
- for (const target of targets) {
948
- byProvider.set(target.providerId, (byProvider.get(target.providerId) ?? 0) + 1);
949
- }
950
- if (smokeOptions.planOnly) {
951
- const plan = {
952
- mode: smokeOptions.allModels ? "all-models" : "representative",
953
- targetCount: targets.length,
954
- providers: Object.fromEntries(byProvider),
955
- targets
956
- };
957
- if (smokeOptions.json) {
958
- writeLine(JSON.stringify(plan, null, 2));
959
- } else {
960
- writeLine(`${color4.bold("Provider/Model Smoke Test Plan")}`);
961
- writeLine();
962
- for (const [providerId, count] of byProvider) {
963
- writeLine(` ${color4.cyan(providerId.padEnd(24))} ${count} model(s)`);
964
- }
965
- writeLine();
966
- writeLine(
967
- color4.dim(`${targets.length} request(s) would run sequentially. Remove --plan to execute.`)
968
- );
969
- }
970
- return targets.length > 0 ? 0 : 2;
971
- }
972
- if (targets.length === 0) {
973
- const message = "No provider/model targets matched. Check the active profile or remove filters.";
974
- if (smokeOptions.json) {
975
- writeLine(JSON.stringify({ results: [], summary: { passed: 0, failed: 0 }, error: message }));
976
- } else {
977
- writeLine(color4.amber(message));
978
- }
979
- return 2;
980
- }
981
- const confirmed = deps.flags?.["yes"] === true || deps.flags?.["yes"] === "true" || args.slice(1).includes("--yes");
982
- if (targets.length > 50 && !confirmed) {
983
- const message = `${targets.length} live requests are planned. Review with --plan, then add --yes to run them.`;
984
- if (smokeOptions.json) {
985
- writeLine(JSON.stringify({ error: message, targetCount: targets.length }));
986
- } else {
987
- writeLine(color4.amber(message));
988
- }
989
- return 2;
990
- }
991
- if (!smokeOptions.json) {
992
- writeLine(`${color4.bold("Live Provider/Model Smoke Test")}`);
993
- writeLine(
994
- color4.dim(
995
- `${targets.length} request(s), sequential, ${smokeOptions.timeoutMs}ms timeout, max ${smokeOptions.maxTokens} output tokens.`
996
- )
997
- );
998
- writeLine();
999
- }
1000
- const resultLines = (result, index, total) => {
1001
- if (smokeOptions.json) return;
1002
- const label = `[${String(index + 1).padStart(String(total).length)}/${total}]`;
1003
- const target = `${result.providerId}/${result.modelId}`;
1004
- if (result.status === "passed") {
1005
- writeLine(
1006
- ` ${color4.green("\u2713")} ${color4.dim(label)} ${color4.cyan(target)} ${color4.dim(
1007
- `${fmtMs(result.latencyMs)} \xB7 in${result.usage.input}/out${result.usage.output} \xB7 ${result.stopReason}`
1008
- )}`
1009
- );
1010
- } else {
1011
- const status = result.httpStatus ? `HTTP ${result.httpStatus} \xB7 ` : "";
1012
- writeLine(
1013
- ` ${color4.red("\u2717")} ${color4.dim(label)} ${color4.red(target)} ${color4.dim(
1014
- `${fmtMs(result.latencyMs)} \xB7 ${status}${result.errorKind}`
1015
- )}`
1016
- );
1017
- writeLine(` ${color4.red(result.error.slice(0, 240))}`);
1018
- }
1019
- };
1020
- let oauthWriteChain = Promise.resolve();
1021
- let oauthWriteScheduled = false;
1022
- const profilePath = activeProfileConfigPath(deps.paths, config);
1023
- setOAuthTokenPersister((providerId, credentials) => {
1024
- oauthWriteScheduled = true;
1025
- oauthWriteChain = oauthWriteChain.then(
1026
- () => mutateConfigProviders(profilePath, deps.vault, (all) => {
1027
- const providerConfig = all[providerId];
1028
- if (!providerConfig) return;
1029
- const keys = normalizeKeys(providerConfig);
1030
- const active = providerConfig.activeKey ? keys.find((key) => key.label === providerConfig.activeKey) : keys[0];
1031
- if (!active) return;
1032
- active.apiKey = credentials.accessToken;
1033
- active.refreshToken = credentials.refreshToken;
1034
- active.expiresAt = new Date(credentials.expiresAt).toISOString();
1035
- if (credentials.accountId) active.accountId = credentials.accountId;
1036
- writeKeysBack(providerConfig, keys);
1037
- })
1038
- );
1039
- });
1040
- let results;
1041
- try {
1042
- const providerFactories = new Map(
1043
- (await buildProviderFactoriesFromRegistry({ registry: deps.modelsRegistry })).map(
1044
- (factory) => [factory.type, factory]
1045
- )
1046
- );
1047
- results = await runModelSmokeTests({
1048
- targets,
1049
- options: smokeOptions,
1050
- createProvider: (providerId) => createModelDiagSmokeProvider({
1051
- providerId,
1052
- config,
1053
- modelsRegistry: deps.modelsRegistry,
1054
- providerFactories
1055
- }),
1056
- onTargetComplete: resultLines
1057
- });
1058
- } finally {
1059
- setOAuthTokenPersister(void 0);
1060
- }
1061
- let oauthPersistenceError;
1062
- if (oauthWriteScheduled) {
1063
- try {
1064
- await oauthWriteChain;
1065
- } catch (err) {
1066
- oauthPersistenceError = toErrorMessage3(err);
1067
- }
1068
- }
1069
- const passed = results.filter((result) => result.status === "passed").length;
1070
- const failed = results.length - passed;
1071
- const summary = { passed, failed, total: results.length };
1072
- if (smokeOptions.json) {
1073
- writeLine(
1074
- JSON.stringify(
1075
- {
1076
- results,
1077
- summary,
1078
- ...oauthPersistenceError ? {
1079
- warning: `OAuth token refresh worked in-memory but could not be saved: ${oauthPersistenceError}`
1080
- } : {}
1081
- },
1082
- null,
1083
- 2
1084
- )
1085
- );
1086
- } else {
1087
- writeLine();
1088
- writeLine(
1089
- `${color4.bold("Summary:")} ${color4.green(`${passed} passed`)} \xB7 ${failed > 0 ? color4.red(`${failed} failed`) : color4.dim("0 failed")}`
1090
- );
1091
- if (!smokeOptions.allModels) {
1092
- writeLine(color4.dim("Use --all-models to test every catalog-visible model sequentially."));
1093
- }
1094
- if (oauthPersistenceError) {
1095
- writeLine(
1096
- color4.amber(
1097
- `OAuth token refresh worked in-memory but could not be saved: ${oauthPersistenceError}`
1098
- )
1099
- );
1100
- }
1101
- }
1102
- return failed === 0 ? 0 : 1;
1103
- }
1104
-
1105
- // src/subcommands/handlers/modeldiag.ts
1106
- async function readProviders(cachePath) {
1107
- if (!cachePath) {
1108
- return { ok: false, message: `${color5.red("Models cache not available")}.`, hard: true };
1109
- }
1110
- try {
1111
- const raw = await fs.readFile(cachePath, "utf8");
1112
- const parsed = JSON.parse(raw);
1113
- const payload = parsed.payload ?? parsed;
1114
- return {
1115
- ok: true,
1116
- providers: Object.entries(payload).map(([id, p]) => ({
1117
- id: p.id ?? id,
1118
- name: p.name ?? id,
1119
- family: p.npm ?? id,
1120
- models: Object.values(p.models ?? {}).map(
1121
- (m) => ({
1122
- id: m.id,
1123
- name: m.name,
1124
- capabilities: {
1125
- contextWindow: m.limit?.context,
1126
- maxOutputTokens: m.limit?.output
1127
- },
1128
- pricing: m.cost
1129
- })
1130
- )
1131
- }))
1132
- };
1133
- } catch {
1134
- return {
1135
- ok: false,
1136
- message: `${color5.amber("Models cache not available")}. Run wstack sync-models.`,
1137
- hard: false
1138
- };
1139
- }
1140
- }
1141
- function checkHasKey(pid, config) {
1142
- if (pid === config.provider && config.provider) return true;
1143
- const pc = config.providers?.[pid];
1144
- if (!pc) return false;
1145
- if (typeof pc.apiKey === "string" && pc.apiKey.length > 0) return true;
1146
- if (Array.isArray(pc.apiKeys) && pc.apiKeys.some((k) => k?.apiKey)) return true;
1147
- return false;
1148
- }
1149
- var modeldiagCmd = async (args, deps) => {
1150
- const sub = args[0]?.toLowerCase() || "full";
1151
- const cacheResult = await readProviders(deps.paths.modelsCache);
1152
- if (!cacheResult.ok) {
1153
- deps.renderer.write(`${cacheResult.message}
1154
- `);
1155
- return cacheResult.hard ? 1 : 0;
1156
- }
1157
- const providers = cacheResult.providers;
1158
- const config = deps.config;
1159
- const modelMatrix = config.modelMatrix ?? {};
1160
- function hasKey(pid) {
1161
- return checkHasKey(pid, config);
1162
- }
1163
- function writeLine(line = "") {
1164
- deps.renderer.write(`${line}
1165
- `);
1166
- }
1167
- if (sub === "keys") {
1168
- writeLine(`${color5.bold("API Key Status")}`);
1169
- writeLine();
1170
- for (const prov of providers) {
1171
- const k = hasKey(prov.id);
1172
- writeLine(` ${checkMark(k)} ${color5.bold(prov.id.padEnd(18))} ${color5.dim(prov.name)}`);
1173
- }
1174
- writeLine();
1175
- writeLine(`${color5.dim(`Leader: ${config.provider}/${config.model}`)}`);
1176
- return 0;
1177
- }
1178
- if (sub === "caps") {
1179
- writeLine(`${color5.bold("Model Capabilities")} ${color5.dim("\u2014 matched to known profiles")}`);
1180
- writeLine();
1181
- for (const prov of providers) {
1182
- if (!hasKey(prov.id)) continue;
1183
- writeLine(` ${color5.bold(prov.id)} ${color5.dim(`(${prov.name})`)}`);
1184
- const tiers = {
1185
- premium: [],
1186
- standard: [],
1187
- budget: [],
1188
- unknown: []
1189
- };
1190
- for (const m of prov.models ?? []) {
1191
- const profile = findProfile(prov.id, m.id);
1192
- tiers[profile?.costTier ?? "unknown"]?.push(m);
1193
- }
1194
- for (const tier of ["premium", "standard", "budget", "unknown"]) {
1195
- const tierModels = tiers[tier];
1196
- if (tierModels.length === 0) continue;
1197
- const label = tier === "unknown" ? color5.dim("unmatched") : `${costLabel(tier)} ${tier}`;
1198
- writeLine(` ${label}`);
1199
- for (const m of tierModels) {
1200
- const cap = m.capabilities;
1201
- const ctx = cap?.contextWindow ?? 0;
1202
- const maxOut = cap?.maxOutputTokens ?? 0;
1203
- const profile = findProfile(prov.id, m.id);
1204
- const family = profile ? `${speedLabel(profile.speedTier)} ${color5.green(profile.family)}` : color5.dim("no profile match");
1205
- const pricing = m.pricing ? `${color5.dim("in")}${fmtPrice(m.pricing.input)} ${color5.dim("out")}${fmtPrice(m.pricing.output)}` : color5.dim("pricing ?");
1206
- writeLine(
1207
- ` ${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}`
1208
- );
1209
- }
1210
- }
1211
- writeLine();
1212
- }
1213
- writeLine(
1214
- color5.dim(
1215
- "Prices in USD per 1M tokens (input/output). ctx = context window, out = max output."
1216
- )
1217
- );
1218
- return 0;
1219
- }
1220
- async function renderSuggest() {
1221
- writeLine();
1222
- writeLine(
1223
- `${color5.bold("Agent \u2192 Model Suggestions")} ${color5.amber("(heuristic \u2014 untested)")}`
1224
- );
1225
- writeLine(
1226
- color5.dim(
1227
- 'These are profile-based best guesses. Test them with wstack modeldiag bench <role> "<prompt>".'
1228
- )
1229
- );
1230
- writeLine();
1231
- const keyedProviders = providers.filter((p) => hasKey(p.id));
1232
- if (keyedProviders.length === 0) {
1233
- writeLine(
1234
- ` ${color5.amber("No providers have API keys configured. Add keys with wstack auth.")}`
1235
- );
1236
- } else {
1237
- const roles = [
1238
- "security-scanner",
1239
- "bug-hunter",
1240
- "planner",
1241
- "architect",
1242
- "refactor-planner",
1243
- "verifier",
1244
- "test",
1245
- "document",
1246
- "reviewer",
1247
- "code-reviewer",
1248
- "executor",
1249
- "debugger"
1250
- ];
1251
- for (const role of roles) {
1252
- if (modelMatrix[role]) {
1253
- const entry = modelMatrix[role];
1254
- const p = entry.provider ?? config.provider;
1255
- writeLine(
1256
- ` ${color5.dim(role.padEnd(20))} \u2192 ${color5.cyan(`${p}/${entry.model}`)} ${color5.dim("(user-configured)")}`
1257
- );
1258
- continue;
1259
- }
1260
- const cat = roleCat(role);
1261
- const ranked = rankModels(providers, hasKey, cat, 3);
1262
- if (ranked.length === 0) {
1263
- writeLine(` ${color5.dim(role.padEnd(20))} \u2192 ${color5.dim("no candidates")}`);
1264
- continue;
1265
- }
1266
- const best = ranked[0];
1267
- const family = best.profile ? ` ${color5.dim(`(${best.profile.family})`)}` : "";
1268
- const bar = scoreBar(best.score, 110);
1269
- writeLine(
1270
- ` ${color5.amber(role.padEnd(20))} \u2192 ${color5.cyan(`${best.provider}/${best.model}`)}${family}`
1271
- );
1272
- writeLine(` ${" ".repeat(22)} ${bar} ${color5.dim(cat)}`);
1273
- if (ranked.length > 1 && (ranked[1]?.score ?? Number.NEGATIVE_INFINITY) >= best.score - 15) {
1274
- for (const alt of ranked.slice(1)) {
1275
- const af = alt.profile ? ` (${alt.profile.family})` : "";
1276
- writeLine(
1277
- ` ${" ".repeat(22)} ${color5.dim(`${alt.provider}/${alt.model}${af} score ${alt.score}`)}`
1278
- );
1279
- }
1280
- }
1281
- }
1282
- writeLine();
1283
- writeLine(
1284
- ` ${color5.bold("leader".padEnd(20))} \u2192 ${color5.cyan(`${config.provider}/${config.model}`)}`
1285
- );
1286
- }
1287
- }
1288
- if (sub === "suggest") {
1289
- await renderSuggest();
1290
- writeLine();
1291
- writeLine(color5.dim("Pin a suggestion: wstack setmodel set <role> <provider>/<model>"));
1292
- writeLine(color5.dim('Test candidates: wstack modeldiag bench <role> "<test prompt>"'));
1293
- return 0;
1294
- }
1295
- if (sub === "test") {
1296
- return runModeldiagTest(args, deps, config);
1297
- }
1298
- if (sub === "bench") {
1299
- return runModeldiagBench(args, deps, providers, config, hasKey);
1300
- }
1301
- if (sub === "eval" || sub === "evall") {
1302
- return runModeldiagEval(args, deps, providers, config, hasKey);
1303
- }
1304
- writeLine(`${color5.bold("API Key Status")}`);
1305
- writeLine();
1306
- for (const prov of providers) {
1307
- const k = hasKey(prov.id);
1308
- writeLine(` ${checkMark(k)} ${color5.bold(prov.id.padEnd(18))} ${color5.dim(prov.name)}`);
1309
- }
1310
- writeLine();
1311
- writeLine(`${color5.dim(`Leader: ${config.provider}/${config.model}`)}`);
1312
- writeLine();
1313
- writeLine(`${color5.bold("Model Capabilities")} ${color5.dim("\u2014 matched to known profiles")}`);
1314
- writeLine();
1315
- for (const prov of providers) {
1316
- if (!hasKey(prov.id)) continue;
1317
- writeLine(` ${color5.bold(prov.id)} ${color5.dim(`(${prov.name})`)}`);
1318
- const tiers = {
1319
- premium: [],
1320
- standard: [],
1321
- budget: [],
1322
- unknown: []
1323
- };
1324
- for (const m of prov.models ?? []) {
1325
- const profile = findProfile(prov.id, m.id);
1326
- tiers[profile?.costTier ?? "unknown"]?.push(m);
1327
- }
1328
- for (const tier of ["premium", "standard", "budget", "unknown"]) {
1329
- const tierModels = tiers[tier];
1330
- if (tierModels.length === 0) continue;
1331
- const label = tier === "unknown" ? color5.dim("unmatched") : `${costLabel(tier)} ${tier}`;
1332
- writeLine(` ${label}`);
1333
- for (const m of tierModels) {
1334
- const cap = m.capabilities;
1335
- const ctx = cap?.contextWindow ?? 0;
1336
- const maxOut = cap?.maxOutputTokens ?? 0;
1337
- const profile = findProfile(prov.id, m.id);
1338
- const family = profile ? `${speedLabel(profile.speedTier)} ${color5.green(profile.family)}` : color5.dim("no profile match");
1339
- const pricing = m.pricing ? `${color5.dim("in")}${fmtPrice(m.pricing.input)} ${color5.dim("out")}${fmtPrice(m.pricing.output)}` : color5.dim("pricing ?");
1340
- writeLine(
1341
- ` ${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}`
1342
- );
1343
- }
1344
- }
1345
- writeLine();
1346
- }
1347
- await renderSuggest();
1348
- writeLine();
1349
- writeLine(color5.dim("Pin a suggestion: wstack setmodel set <role> <provider>/<model>"));
1350
- writeLine(color5.dim('Test candidates: wstack modeldiag bench <role> "<test prompt>"'));
1351
- return 0;
1352
- };
1353
- export {
1354
- modeldiagCmd
1355
- };
1356
- //# sourceMappingURL=modeldiag-CJSFHZ3J.js.map