kiku-agent 1.3.1 → 1.3.2

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 (119) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -12
  3. package/server/agent/eval/AGENTS.md +228 -0
  4. package/server/agent/eval/analyze.py +684 -0
  5. package/server/agent/eval/gates.json +15 -0
  6. package/server/agent/eval/modules/analysis/compare.js +198 -0
  7. package/server/agent/eval/modules/analysis/gates.js +72 -0
  8. package/server/agent/eval/modules/analysis/judge.js +70 -0
  9. package/server/agent/eval/modules/core/cli.js +453 -0
  10. package/server/agent/eval/modules/core/config.js +60 -0
  11. package/server/agent/eval/modules/core/store.js +236 -0
  12. package/server/agent/eval/modules/evaluation/cases.js +689 -0
  13. package/server/agent/eval/modules/evaluation/runner.js +298 -0
  14. package/server/agent/eval/modules/evaluation/scoring.js +351 -0
  15. package/server/agent/eval/modules/evaluation/worker.js +122 -0
  16. package/server/agent/eval/modules/reporting/benchmark.js +265 -0
  17. package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
  18. package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
  19. package/server/agent/eval/modules/server/evalApi.js +1099 -0
  20. package/server/agent/eval/modules/ui/evalUi.js +4796 -0
  21. package/server/agent/eval/modules/utils/smokes.js +119 -0
  22. package/server/agent/tests/agentBubbleSystem.test.js +65 -0
  23. package/server/agent/tests/agentFollowUps.test.js +127 -0
  24. package/server/agent/tests/apiRouter.test.js +108 -0
  25. package/server/agent/tests/architectureFixes.test.js +81 -0
  26. package/server/agent/tests/architectureRefactoring.test.js +194 -0
  27. package/server/agent/tests/autoResearch.test.js +64 -0
  28. package/server/agent/tests/bilibiliEmbed.test.js +251 -0
  29. package/server/agent/tests/catalogSync.test.js +130 -0
  30. package/server/agent/tests/citations.test.js +180 -0
  31. package/server/agent/tests/clarificationAnalytics.test.js +219 -0
  32. package/server/agent/tests/clarifyingQuestions.test.js +594 -0
  33. package/server/agent/tests/cliAutomation.test.js +121 -0
  34. package/server/agent/tests/cloudProviders.test.js +234 -0
  35. package/server/agent/tests/commandInput.test.js +24 -0
  36. package/server/agent/tests/commands.test.js +417 -0
  37. package/server/agent/tests/contextIntelligence.test.js +162 -0
  38. package/server/agent/tests/conversationSafety.test.js +53 -0
  39. package/server/agent/tests/decisionAndSearchEnhancements.test.js +135 -0
  40. package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
  41. package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
  42. package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
  43. package/server/agent/tests/edgeCasesScenarios.test.js +49 -0
  44. package/server/agent/tests/entertainmentMetrics.test.js +126 -0
  45. package/server/agent/tests/envBootstrap.test.js +87 -0
  46. package/server/agent/tests/envCommand.test.js +215 -0
  47. package/server/agent/tests/errorHandling.test.js +80 -0
  48. package/server/agent/tests/errors.test.js +13 -0
  49. package/server/agent/tests/evalAnalyze.test.js +54 -0
  50. package/server/agent/tests/evalCommand.test.js +54 -0
  51. package/server/agent/tests/evalCorpus.test.js +126 -0
  52. package/server/agent/tests/evalHarness.test.js +62 -0
  53. package/server/agent/tests/evalJudgeContext.test.js +38 -0
  54. package/server/agent/tests/evalMetrics.test.js +312 -0
  55. package/server/agent/tests/evalStore.test.js +190 -0
  56. package/server/agent/tests/evalStudio.test.js +179 -0
  57. package/server/agent/tests/feedbackLearning.test.js +186 -0
  58. package/server/agent/tests/firstRun.test.js +44 -0
  59. package/server/agent/tests/gemma4eOptimization.test.js +95 -0
  60. package/server/agent/tests/hardwareTiers.test.js +206 -0
  61. package/server/agent/tests/hybridWebSearch.test.js +192 -0
  62. package/server/agent/tests/judgeService.test.js +251 -0
  63. package/server/agent/tests/lanDiscovery.test.js +319 -0
  64. package/server/agent/tests/mcpClient.test.js +201 -0
  65. package/server/agent/tests/modelManager.test.js +109 -0
  66. package/server/agent/tests/multilingualScenarios.test.js +69 -0
  67. package/server/agent/tests/newCommands.test.js +347 -0
  68. package/server/agent/tests/newCommandsTui.test.js +141 -0
  69. package/server/agent/tests/newTools.test.js +113 -0
  70. package/server/agent/tests/newsIntelligence.test.js +178 -0
  71. package/server/agent/tests/nextGenArchitecture.test.js +99 -0
  72. package/server/agent/tests/offlineInput.test.js +217 -0
  73. package/server/agent/tests/packagingGuard.test.js +57 -0
  74. package/server/agent/tests/presentation.test.js +61 -0
  75. package/server/agent/tests/providerPersistence.test.js +39 -0
  76. package/server/agent/tests/queryBenchmarks.test.js +83 -0
  77. package/server/agent/tests/queryDetails.test.js +126 -0
  78. package/server/agent/tests/realWorldValidation.test.js +391 -0
  79. package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
  80. package/server/agent/tests/runtimeWatchdog.test.js +32 -0
  81. package/server/agent/tests/sanitizer.test.js +37 -0
  82. package/server/agent/tests/scenariosData.js +378 -0
  83. package/server/agent/tests/schemaRouting.test.js +54 -0
  84. package/server/agent/tests/securityAuditRemediation.test.js +214 -0
  85. package/server/agent/tests/securityHardening.test.js +146 -0
  86. package/server/agent/tests/securitySanitizer.test.js +38 -0
  87. package/server/agent/tests/sessionAndCopy.test.js +232 -0
  88. package/server/agent/tests/sidebarControls.test.js +101 -0
  89. package/server/agent/tests/statefulMemory.test.js +270 -0
  90. package/server/agent/tests/subagent.test.js +216 -0
  91. package/server/agent/tests/subagentProgress.test.js +109 -0
  92. package/server/agent/tests/subagentTextFallback.test.js +115 -0
  93. package/server/agent/tests/suggestions.test.js +199 -0
  94. package/server/agent/tests/suggestionsTui.test.js +251 -0
  95. package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
  96. package/server/agent/tests/terminalImage.test.js +80 -0
  97. package/server/agent/tests/terminalMarkdown.test.js +166 -0
  98. package/server/agent/tests/terminalText.test.js +133 -0
  99. package/server/agent/tests/textProtocolLoop.test.js +126 -0
  100. package/server/agent/tests/textToolParsing.test.js +139 -0
  101. package/server/agent/tests/theme.test.js +26 -0
  102. package/server/agent/tests/toolRegistryParity.test.js +53 -0
  103. package/server/agent/tests/toolRoutingRegressions.test.js +119 -0
  104. package/server/agent/tests/tools.test.js +259 -0
  105. package/server/agent/tests/unifiedEndpoints.test.js +152 -0
  106. package/server/agent/tests/v110Features.test.js +262 -0
  107. package/server/agent/tests/versionBump.test.js +69 -0
  108. package/server/agent/tests/vramEstimator.test.js +520 -0
  109. package/server/agent/tests/webChatInspection.test.js +300 -0
  110. package/server/agent/tests/webChatMemory.test.js +112 -0
  111. package/server/agent/tests/webChatSecurity.test.js +169 -0
  112. package/server/agent/tests/webExtract.test.js +261 -0
  113. package/server/agent/tests/webSources.test.js +247 -0
  114. package/server/eval/judgeRubrics.js +110 -0
  115. package/server/eval/judgeService.js +290 -0
  116. package/server/eval/runBenchmark.js +182 -0
  117. package/server/mcp/examples/librechat.example.yaml +16 -0
  118. package/server/mcp/tests/connection.test.js +156 -0
  119. package/server/supabase_schema.sql +63 -0
@@ -0,0 +1,453 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { parseArgs } from 'node:util';
5
+ import { cases, securityCases, allCases, validateCases } from '../evaluation/cases.js';
6
+ import { DEFAULTS, REPO_ROOT, normalizeBaseUrl, normalizeTimeoutMs } from './config.js';
7
+ import { loadReport, verifyCorpusFreshness, acquireRunLock } from './store.js';
8
+ import { runSuite } from '../evaluation/runner.js';
9
+ import { runWorkerMain } from '../evaluation/worker.js';
10
+ import { judgeReport } from '../analysis/judge.js';
11
+ import { applyGates } from '../analysis/gates.js';
12
+ import { createSaver } from './store.js';
13
+ import { diffReports } from '../analysis/compare.js';
14
+ import { writeDashboardReport } from '../reporting/dashboard.js';
15
+ import { writeBenchmarkPng } from '../reporting/benchmarkPng.js';
16
+ import { runWebSmoke, runResearchSmoke, runTokenCalibration } from '../utils/smokes.js';
17
+ import { spawnSync } from 'node:child_process';
18
+
19
+ const SUBCOMMANDS = ['validate', 'run', 'judge', 'compare', 'dashboard', 'dashboard-png', 'dashboard-enterprise', 'smoke', 'calibrate', 'gate', 'analyze', 'ui', '__worker'];
20
+
21
+ const HELP = `Kiku eval harness — single entry point (dev-only, never published to npm)
22
+
23
+ Usage:
24
+ node server/agent/eval/modules/cli.js <command> [options]
25
+ npm run eval -- <command> [options]
26
+
27
+ Commands:
28
+ validate Validate eval corpus (no LM Studio needed)
29
+ run [options] Run catalog/security suite with real agent + LM Studio
30
+ judge --out <f> --judge-model <id> Grade an existing report with an LLM judge
31
+ compare --baseline <f> --candidate <f> [--json] [--fail-on-regression]
32
+ dashboard [--in <f>] [--out <f>] [--compare <f>]
33
+ dashboard-png [--in <f>] [--out <f>] [--width <n>]
34
+ dashboard-enterprise Launch interactive Kiku Eval Studio web UI (primary)
35
+ smoke web|research External-tool / research-orchestrator availability checks
36
+ calibrate [--model <id>] Compare context estimator vs real prompt tokens
37
+ gate --in <f> [--gates-file <f>] Apply gates.json thresholds to a report
38
+ analyze [--mode all|codebase|report] [--report <f>] [--out <f>] [--markdown-out <f>] [--skip-corpus]
39
+ ui [--port <n>] [--open] Launch interactive Kiku Eval Studio web UI
40
+
41
+ run options:
42
+ --suite catalog|security|all (default: catalog) --ids p01,w01 --limit 3
43
+ --model <id> --base-url <url> --timeout-ms <n> --out <file> --resume
44
+ --judge-model <id> --gate --dashboard --save-as <snapshot-name>
45
+
46
+ Examples:
47
+ npm run eval:validate
48
+ npm run eval:quick
49
+ npm run eval:run -- --suite all --limit 5
50
+ npm run eval:judge -- --out artifacts/eval/live.json --judge-model google/gemma-4-e4b
51
+ npm run eval:compare -- --baseline a.json --candidate b.json
52
+ npm run eval:dashboard
53
+ `;
54
+
55
+ function opt(spec) {
56
+ return parseArgs({ args: spec, options: {
57
+ model: { type: 'string' },
58
+ 'base-url': { type: 'string' },
59
+ 'timeout-ms': { type: 'string' },
60
+ out: { type: 'string' },
61
+ in: { type: 'string' },
62
+ suite: { type: 'string' },
63
+ ids: { type: 'string' },
64
+ limit: { type: 'string' },
65
+ 'judge-model': { type: 'string' },
66
+ resume: { type: 'boolean' },
67
+ 'retry-failed': { type: 'boolean' },
68
+ 'retry-errors': { type: 'boolean' },
69
+ from: { type: 'string' },
70
+ gate: { type: 'boolean' },
71
+ 'gates-file': { type: 'string' },
72
+ dashboard: { type: 'boolean' },
73
+ 'dashboard-out': { type: 'string' },
74
+ 'save-as': { type: 'string' },
75
+ baseline: { type: 'string' },
76
+ candidate: { type: 'string' },
77
+ compare: { type: 'string' },
78
+ json: { type: 'boolean' },
79
+ 'fail-on-regression': { type: 'boolean' },
80
+ id: { type: 'string' },
81
+ mode: { type: 'string' },
82
+ width: { type: 'string' },
83
+ report: { type: 'string' },
84
+ gates: { type: 'string' },
85
+ 'markdown-out': { type: 'string' },
86
+ 'skip-corpus': { type: 'boolean' },
87
+ root: { type: 'string' },
88
+ port: { type: 'string' },
89
+ open: { type: 'boolean' },
90
+ help: { type: 'boolean', short: 'h' },
91
+ }, allowPositionals: true });
92
+ }
93
+
94
+ async function cmdValidate() {
95
+ validateCases();
96
+ console.log(JSON.stringify({
97
+ valid: true,
98
+ count: allCases.length,
99
+ catalogCount: cases.length,
100
+ securityCount: securityCases.length,
101
+ categories: [...new Set(allCases.map(c => c.category))]
102
+ }));
103
+ }
104
+
105
+ async function cmdRun(raw) {
106
+ const { values } = opt(raw);
107
+ await runSuite({
108
+ suite: values.suite ?? DEFAULTS.suite,
109
+ ids: values.ids,
110
+ limit: values.limit,
111
+ model: values.model ?? DEFAULTS.model,
112
+ baseUrl: normalizeBaseUrl(values['base-url'] ?? DEFAULTS.baseUrl),
113
+ timeoutMs: normalizeTimeoutMs(values['timeout-ms'] ?? DEFAULTS.timeoutMs),
114
+ out: values.out ?? DEFAULTS.out,
115
+ from: values.from,
116
+ resume: values.resume ?? false,
117
+ retryFailed: values['retry-failed'] ?? false,
118
+ retryErrors: values['retry-errors'] ?? false,
119
+ judgeModel: values['judge-model'],
120
+ gate: values.gate ?? false,
121
+ gatesFile: values['gates-file'] ?? DEFAULTS.gatesFile,
122
+ saveAs: values['save-as'],
123
+ dashboard: values.dashboard ?? false,
124
+ dashboardOut: values['dashboard-out'] ?? DEFAULTS.dashboardOut,
125
+ });
126
+ }
127
+
128
+ async function cmdJudge(raw) {
129
+ const { values } = opt(raw);
130
+ const outPath = path.resolve(REPO_ROOT, values.out ?? DEFAULTS.out);
131
+ if (!values['judge-model']) throw new Error('judge requires --judge-model <id>');
132
+ const report = await loadReport(outPath);
133
+ await verifyCorpusFreshness(report);
134
+ const save = createSaver(outPath, report);
135
+ const lock = await acquireRunLock(outPath);
136
+ try {
137
+ await judgeReport(report,
138
+ { judgeModel: values['judge-model'], baseUrl: normalizeBaseUrl(values['base-url'] ?? DEFAULTS.baseUrl), timeoutMs: normalizeTimeoutMs(values['timeout-ms'] ?? DEFAULTS.timeoutMs) },
139
+ save);
140
+ await save();
141
+ } finally {
142
+ await lock.release();
143
+ }
144
+ console.log(JSON.stringify(report.summary, null, 2));
145
+ }
146
+
147
+ async function cmdCompare(raw) {
148
+ const { values } = opt(raw);
149
+ if (!values.baseline || !values.candidate) throw new Error('compare requires --baseline <file> --candidate <file>');
150
+ const base = JSON.parse(await fs.readFile(path.resolve(REPO_ROOT, values.baseline), 'utf8'));
151
+ const cand = JSON.parse(await fs.readFile(path.resolve(REPO_ROOT, values.candidate), 'utf8'));
152
+ const diff = diffReports(base, cand);
153
+ if (values.json) {
154
+ console.log(JSON.stringify(diff, null, 2));
155
+ } else {
156
+ console.log('=== Kiku Evaluation Report Comparison ===');
157
+ console.log('Baseline:', values.baseline);
158
+ console.log('Candidate:', values.candidate);
159
+ console.log('\n--- Metric Deltas ---');
160
+ for (const [m, data] of Object.entries(diff.metricDeltas)) {
161
+ const deltaSign = data.delta !== null && data.delta > 0 ? `+${data.delta}` : `${data.delta ?? '-'}`;
162
+ console.log(`${m.padEnd(25)}: ${String(data.baseline ?? '-').padEnd(10)} -> ${String(data.candidate ?? '-').padEnd(10)} (delta: ${deltaSign})`);
163
+ }
164
+ console.log('\n--- Case Diff Summary ---');
165
+ console.log(`Regressions : ${diff.caseDiffs.summary.regressionsCount}`);
166
+ console.log(`Improvements: ${diff.caseDiffs.summary.improvementsCount}`);
167
+ console.log(`Unchanged : ${diff.caseDiffs.summary.unchangedCount}`);
168
+ if (diff.caseDiffs.regressions.length > 0) {
169
+ console.log('\n[REGRESSION] Regressions detected:');
170
+ for (const r of diff.caseDiffs.regressions) {
171
+ console.log(` - [${r.id}] (${r.category}): base=${r.baseline.status}/matched=${r.baseline.allMatched}/tool=${r.baseline.toolCorrect} -> cand=${r.candidate.status}/matched=${r.candidate.allMatched}/tool=${r.candidate.toolCorrect}`);
172
+ }
173
+ }
174
+ }
175
+ if (values['fail-on-regression'] && diff.caseDiffs.regressions.length > 0) process.exit(2);
176
+ }
177
+
178
+ async function cmdDashboard(raw) {
179
+ const { values } = opt(raw);
180
+ const inPath = path.resolve(REPO_ROOT, values.in ?? DEFAULTS.out);
181
+ const outPath = path.resolve(REPO_ROOT, values.out ?? DEFAULTS.dashboardOut);
182
+ const report = JSON.parse(await fs.readFile(inPath, 'utf8'));
183
+ let compareReport = null;
184
+ if (values.compare) compareReport = JSON.parse(await fs.readFile(path.resolve(REPO_ROOT, values.compare), 'utf8'));
185
+ await writeDashboardReport(report, outPath, compareReport);
186
+
187
+ let lastMtime = 0;
188
+ try {
189
+ const st = await fs.stat(inPath);
190
+ lastMtime = st.mtimeMs;
191
+ } catch {}
192
+
193
+ const watcher = setInterval(async () => {
194
+ try {
195
+ const st = await fs.stat(inPath);
196
+ if (st.mtimeMs !== lastMtime) {
197
+ lastMtime = st.mtimeMs;
198
+ const freshRaw = await fs.readFile(inPath, 'utf8');
199
+ const freshReport = JSON.parse(freshRaw);
200
+ await writeDashboardReport(freshReport, outPath, compareReport);
201
+ }
202
+ } catch {}
203
+ }, 1000);
204
+ if (typeof watcher.unref === 'function') {
205
+ watcher.unref();
206
+ }
207
+
208
+ const shouldOpen = values.open !== false;
209
+ if (shouldOpen) {
210
+ let targetUrl = `file:///${outPath.replace(/\\/g, '/')}`;
211
+ try {
212
+ const ping3001 = await fetch('http://localhost:3001/api/eval/status', { signal: AbortSignal.timeout(600) });
213
+ if (ping3001.ok) {
214
+ targetUrl = 'http://localhost:3001/eval/dashboard';
215
+ } else {
216
+ const ping3000 = await fetch('http://localhost:3000/api/eval/status', { signal: AbortSignal.timeout(600) });
217
+ if (ping3000.ok) targetUrl = 'http://localhost:3000/eval/dashboard';
218
+ }
219
+ } catch {}
220
+ const safeUrl = targetUrl.replace(/["`$&|;<>^%]/g, '');
221
+ const openCmd = process.platform === 'win32'
222
+ ? `start "" "${safeUrl}"`
223
+ : (process.platform === 'darwin' ? `open "${safeUrl}"` : `xdg-open "${safeUrl}"`);
224
+ const cp = await import('node:child_process');
225
+ cp.exec(openCmd, () => {});
226
+ }
227
+ }
228
+
229
+ async function cmdDashboardPng(raw) {
230
+ const { values } = opt(raw);
231
+ const inPath = path.resolve(REPO_ROOT, values.in ?? DEFAULTS.out);
232
+ const outPath = path.resolve(REPO_ROOT, values.out ?? DEFAULTS.dashboardPngOut);
233
+ const report = JSON.parse(await fs.readFile(inPath, 'utf8'));
234
+ const width = Number(values.width ?? 1440) || 1440;
235
+ const mode = values.mode ?? 'full';
236
+ await writeBenchmarkPng(report, outPath, { width, mode });
237
+ }
238
+
239
+ async function cmdDashboardEnterprise(raw) {
240
+ return cmdUi(raw);
241
+ }
242
+
243
+ async function cmdSmoke(raw) {
244
+ const [target, ...rest] = raw;
245
+ const { values } = opt(rest);
246
+ if (target === 'web') {
247
+ await runWebSmoke({ out: values.out ?? DEFAULTS.webSmokeOut });
248
+ } else if (target === 'research') {
249
+ await runResearchSmoke({
250
+ out: values.out ?? DEFAULTS.researchSmokeOut,
251
+ model: values.model ?? DEFAULTS.model,
252
+ baseUrl: normalizeBaseUrl(values['base-url'] ?? DEFAULTS.baseUrl),
253
+ timeoutMs: normalizeTimeoutMs(values['timeout-ms'] ?? DEFAULTS.timeoutMs),
254
+ });
255
+ } else {
256
+ throw new Error('smoke requires a target: web | research');
257
+ }
258
+ }
259
+
260
+ async function cmdCalibrate(raw) {
261
+ const { values } = opt(raw);
262
+ await runTokenCalibration({
263
+ out: values.out ?? DEFAULTS.tokenCalibrationOut,
264
+ model: values.model ?? DEFAULTS.model,
265
+ baseUrl: normalizeBaseUrl(values['base-url'] ?? DEFAULTS.baseUrl),
266
+ });
267
+ }
268
+
269
+ async function cmdGate(raw) {
270
+ const { values } = opt(raw);
271
+ const inPath = path.resolve(REPO_ROOT, values.in ?? DEFAULTS.out);
272
+ const report = JSON.parse(await fs.readFile(inPath, 'utf8'));
273
+ const save = createSaver(inPath, report);
274
+ const lock = await acquireRunLock(inPath);
275
+ try {
276
+ const result = await applyGates(report, values['gates-file'] ?? DEFAULTS.gatesFile, save);
277
+ console.log(JSON.stringify(result, null, 2));
278
+ if (!result.passed) process.exitCode = 2;
279
+ } finally {
280
+ await lock.release();
281
+ }
282
+ }
283
+
284
+ async function cmdAnalyze(raw) {
285
+ const { values } = opt(raw);
286
+ const script = new URL('../../analyze.py', import.meta.url);
287
+ const scriptPath = fileURLToPath(script);
288
+ const args = [scriptPath, '--mode', values.mode ?? 'all'];
289
+ if (values.report) args.push('--report', values.report);
290
+ if (values.out) args.push('--out', values.out);
291
+ if (values['markdown-out']) args.push('--markdown-out', values['markdown-out']);
292
+ if (values.gates) args.push('--gates', values.gates);
293
+ if (values.root) args.push('--root', values.root);
294
+ if (values['skip-corpus']) args.push('--skip-corpus');
295
+ const candidates = process.platform === 'win32'
296
+ ? [['python', []], ['py', ['-3']], ['python3', []]]
297
+ : [['python3', []], ['python', []]];
298
+ let lastError = null;
299
+ for (const [bin, prefix] of candidates) {
300
+ const result = spawnSync(bin, [...prefix, ...args], { cwd: REPO_ROOT, stdio: 'inherit' });
301
+ if (result.error?.code === 'ENOENT') { lastError = result.error; continue; }
302
+ if (typeof result.status === 'number' && result.status !== 0) process.exit(result.status);
303
+ return;
304
+ }
305
+ throw new Error(`Python interpreter not found for analyze (${lastError?.message ?? 'no candidates'})`);
306
+ }
307
+
308
+ async function cmdUi(raw) {
309
+ const { values } = opt(raw);
310
+ const port = Number(values.port || 3001);
311
+ const shouldOpen = values.open ?? false;
312
+ const { createServer } = await import('node:http');
313
+ const { handleEvalApiRequest } = await import('../server/evalApi.js');
314
+
315
+ const server = createServer(async (req, res) => {
316
+ try {
317
+ const handled = await handleEvalApiRequest(req, res);
318
+ if (!handled) {
319
+ res.writeHead(302, { Location: '/eval' });
320
+ res.end();
321
+ }
322
+ } catch (err) {
323
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
324
+ res.end('Server error: ' + err.message);
325
+ }
326
+ });
327
+
328
+ server.on('error', (err) => {
329
+ if (err.code === 'EADDRINUSE') {
330
+ if (shouldOpen) {
331
+ const url = `http://localhost:${port}/eval`;
332
+ const openCmd = process.platform === 'win32' ? `start "" "${url}"` : (process.platform === 'darwin' ? `open "${url}"` : `xdg-open "${url}"`);
333
+ import('node:child_process').then(cp => {
334
+ cp.exec(openCmd, () => {});
335
+ });
336
+ }
337
+ return;
338
+ }
339
+ throw err;
340
+ });
341
+
342
+ server.listen(port, () => {
343
+ const url = `http://localhost:${port}/eval`;
344
+ console.log(`[Eval Studio] Running at ${url}`);
345
+ if (shouldOpen) {
346
+ const openCmd = process.platform === 'win32' ? `start "" "${url}"` : (process.platform === 'darwin' ? `open "${url}"` : `xdg-open "${url}"`);
347
+ import('node:child_process').then(cp => {
348
+ cp.exec(openCmd, () => {});
349
+ });
350
+ }
351
+ });
352
+ }
353
+
354
+ async function cmdLegacy(argv) {
355
+ const { values } = parseArgs({ args: argv, options: {
356
+ worker: { type: 'string' },
357
+ out: { type: 'string', default: DEFAULTS.out },
358
+ suite: { type: 'string', default: DEFAULTS.suite },
359
+ dashboard: { type: 'boolean', default: false },
360
+ 'dashboard-out': { type: 'string', default: DEFAULTS.dashboardOut },
361
+ ids: { type: 'string' },
362
+ limit: { type: 'string' },
363
+ model: { type: 'string', default: DEFAULTS.model },
364
+ 'base-url': { type: 'string', default: DEFAULTS.baseUrl },
365
+ 'timeout-ms': { type: 'string', default: String(DEFAULTS.timeoutMs) },
366
+ 'judge-model': { type: 'string' },
367
+ 'judge-only': { type: 'boolean', default: false },
368
+ resume: { type: 'boolean', default: false },
369
+ validate: { type: 'boolean', default: false },
370
+ gate: { type: 'boolean', default: false },
371
+ 'gates-file': { type: 'string', default: DEFAULTS.gatesFile },
372
+ 'save-as': { type: 'string' },
373
+ }, allowPositionals: true });
374
+
375
+ validateCases();
376
+ const timeoutMs = normalizeTimeoutMs(values['timeout-ms']);
377
+ const baseUrl = normalizeBaseUrl(values['base-url']);
378
+
379
+ if (values.validate) {
380
+ await cmdValidate();
381
+ return;
382
+ }
383
+ if (values.worker) {
384
+ await runWorkerMain({ caseId: values.worker, model: values.model, baseUrl, timeoutMs });
385
+ return;
386
+ }
387
+ await runSuite({
388
+ suite: values.suite,
389
+ ids: values.ids,
390
+ limit: values.limit,
391
+ model: values.model,
392
+ baseUrl,
393
+ timeoutMs,
394
+ out: values.out,
395
+ resume: values.resume,
396
+ judgeModel: values['judge-model'],
397
+ judgeOnly: values['judge-only'],
398
+ gate: values.gate,
399
+ gatesFile: values['gates-file'],
400
+ saveAs: values['save-as'],
401
+ dashboard: values.dashboard,
402
+ dashboardOut: values['dashboard-out'],
403
+ });
404
+ }
405
+
406
+ export async function runCli(argv = process.argv.slice(2)) {
407
+ const [first, ...rest] = argv;
408
+ if (!first || first === '--help' || first === '-h') {
409
+ console.log(HELP);
410
+ return;
411
+ }
412
+ if (rest.includes('--help') || rest.includes('-h')) {
413
+ console.log(HELP);
414
+ return;
415
+ }
416
+ if (!SUBCOMMANDS.includes(first)) {
417
+ await cmdLegacy(argv);
418
+ return;
419
+ }
420
+ if (first === '__worker') {
421
+ const { values } = opt(rest);
422
+ if (!values.id) throw new Error('__worker requires --id <caseId>');
423
+ await runWorkerMain({
424
+ caseId: values.id,
425
+ model: values.model ?? DEFAULTS.model,
426
+ baseUrl: normalizeBaseUrl(values['base-url'] ?? DEFAULTS.baseUrl),
427
+ timeoutMs: normalizeTimeoutMs(values['timeout-ms'] ?? DEFAULTS.timeoutMs),
428
+ });
429
+ return;
430
+ }
431
+ switch (first) {
432
+ case 'validate': await cmdValidate(); break;
433
+ case 'run': await cmdRun(rest); break;
434
+ case 'judge': await cmdJudge(rest); break;
435
+ case 'compare': await cmdCompare(rest); break;
436
+ case 'dashboard': await cmdDashboard(rest); break;
437
+ case 'dashboard-png': await cmdDashboardPng(rest); break;
438
+ case 'dashboard-enterprise': await cmdDashboardEnterprise(rest); break;
439
+ case 'smoke': await cmdSmoke(rest); break;
440
+ case 'calibrate': await cmdCalibrate(rest); break;
441
+ case 'gate': await cmdGate(rest); break;
442
+ case 'analyze': await cmdAnalyze(rest); break;
443
+ case 'ui': await cmdUi(rest); break;
444
+ default: throw new Error(`Unknown command: ${first}`);
445
+ }
446
+ }
447
+
448
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
449
+ runCli(process.argv.slice(2)).catch(err => {
450
+ console.error(err);
451
+ process.exit(1);
452
+ });
453
+ }
@@ -0,0 +1,60 @@
1
+ import { fileURLToPath } from 'node:url';
2
+
3
+ export const REPO_ROOT = fileURLToPath(new URL('../../../../../', import.meta.url));
4
+
5
+ export const DEFAULTS = {
6
+ model: 'google/gemma-4-e4b',
7
+ baseUrl: 'http://127.0.0.1:1234/v1',
8
+ timeoutMs: 240000,
9
+ workerGraceMs: 15000,
10
+ out: 'artifacts/eval/live.json',
11
+ suite: 'catalog',
12
+ dashboardOut: 'artifacts/eval/dashboard.html',
13
+ dashboardPngOut: 'artifacts/eval/benchmark.png',
14
+ gatesFile: 'server/agent/eval/gates.json',
15
+ snapshotsDir: 'artifacts/eval/snapshots',
16
+ webSmokeOut: 'artifacts/eval/web-smoke.json',
17
+ researchSmokeOut: 'artifacts/eval/research-smoke.json',
18
+ tokenCalibrationOut: 'artifacts/eval/token-calibration.json',
19
+ };
20
+
21
+ export const ALLOWED_TOOLS = [
22
+ 'get_profile_and_fandom',
23
+ 'get_quotes',
24
+ 'search_works',
25
+ 'get_work_detail',
26
+ 'get_character_roles',
27
+ 'search_discography',
28
+ 'get_music_videos',
29
+ 'get_media_links',
30
+ 'get_brand_endorsements',
31
+ 'get_career_timeline',
32
+ 'get_awards',
33
+ 'get_gallery_photos',
34
+ 'global_search',
35
+ ];
36
+
37
+ export const TRACKED_AGENT_FILES = [
38
+ 'server/agent/agentCore.js',
39
+ 'server/agent/llmClient.js',
40
+ 'server/agent/config.js',
41
+ 'server/agent/tools.js',
42
+ 'server/agent/textToolProtocol.js',
43
+ 'server/agent/contextCompactor.js',
44
+ ];
45
+
46
+ export function normalizeBaseUrl(raw = DEFAULTS.baseUrl) {
47
+ const base = new URL(raw);
48
+ if (!['127.0.0.1', 'localhost', '[::1]'].includes(base.hostname) || base.username || base.password || base.search) {
49
+ throw new Error('This harness only supports local, credential-free LM Studio URLs');
50
+ }
51
+ return raw;
52
+ }
53
+
54
+ export function normalizeTimeoutMs(raw = DEFAULTS.timeoutMs) {
55
+ const timeoutMs = Number(raw);
56
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1000) {
57
+ throw new Error('timeout-ms must be an integer >= 1000');
58
+ }
59
+ return timeoutMs;
60
+ }