kiku-agent 1.3.1 → 1.3.3

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 (232) hide show
  1. package/README.md +69 -12
  2. package/package.json +5 -12
  3. package/server/agent/agentCore.js +319 -46
  4. package/server/agent/appPaths.js +31 -0
  5. package/server/agent/audioPlayer.js +339 -12
  6. package/server/agent/autoContext.js +193 -0
  7. package/server/agent/catalogEntityResolver.js +170 -28
  8. package/server/agent/clarifyingQuestions.js +137 -35
  9. package/server/agent/cli.js +177 -48
  10. package/server/agent/cloudAdapters.js +116 -0
  11. package/server/agent/commandRegistry.js +409 -80
  12. package/server/agent/commands.js +1056 -125
  13. package/server/agent/config.js +1088 -690
  14. package/server/agent/contextCompactor.js +213 -21
  15. package/server/agent/cpuMetrics.js +199 -0
  16. package/server/agent/engineService.js +591 -0
  17. package/server/agent/eval/AGENTS.md +228 -0
  18. package/server/agent/eval/analyze.py +684 -0
  19. package/server/agent/eval/gates.json +15 -0
  20. package/server/agent/eval/modules/analysis/compare.js +198 -0
  21. package/server/agent/eval/modules/analysis/gates.js +72 -0
  22. package/server/agent/eval/modules/analysis/judge.js +70 -0
  23. package/server/agent/eval/modules/core/cli.js +453 -0
  24. package/server/agent/eval/modules/core/config.js +60 -0
  25. package/server/agent/eval/modules/core/store.js +236 -0
  26. package/server/agent/eval/modules/evaluation/cases.js +689 -0
  27. package/server/agent/eval/modules/evaluation/runner.js +288 -0
  28. package/server/agent/eval/modules/evaluation/scoring.js +363 -0
  29. package/server/agent/eval/modules/evaluation/worker.js +122 -0
  30. package/server/agent/eval/modules/reporting/benchmark.js +265 -0
  31. package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
  32. package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
  33. package/server/agent/eval/modules/server/evalApi.js +1099 -0
  34. package/server/agent/eval/modules/ui/evalUi.js +4799 -0
  35. package/server/agent/eval/modules/utils/smokes.js +119 -0
  36. package/server/agent/eventBus.js +1 -0
  37. package/server/agent/ink/main.jsx +196 -191
  38. package/server/agent/jobManager.js +11 -1
  39. package/server/agent/lanDiscovery.js +721 -23
  40. package/server/agent/llmClient.js +317 -71
  41. package/server/agent/lmStudioWakeup.js +517 -65
  42. package/server/agent/locationService.js +797 -0
  43. package/server/agent/mcpQuizData.js +509 -0
  44. package/server/agent/memory/contextAllocator.js +19 -7
  45. package/server/agent/memory.js +3 -1
  46. package/server/agent/modelCompatibility.js +352 -0
  47. package/server/agent/modelDefaults.js +1 -0
  48. package/server/agent/modelManager.js +46 -2
  49. package/server/agent/newsWatch.js +9 -3
  50. package/server/agent/processManager.js +229 -0
  51. package/server/agent/prompts.js +174 -8
  52. package/server/agent/routingInterceptor.js +9 -1
  53. package/server/agent/sanitizer.js +41 -6
  54. package/server/agent/searchQueryPlanner.js +202 -0
  55. package/server/agent/session.js +59 -4
  56. package/server/agent/tests/agentBubbleSystem.test.js +65 -0
  57. package/server/agent/tests/agentFollowUps.test.js +253 -0
  58. package/server/agent/tests/agentMusicControl.test.js +245 -0
  59. package/server/agent/tests/apiRouter.test.js +108 -0
  60. package/server/agent/tests/architectureFixes.test.js +81 -0
  61. package/server/agent/tests/architectureRefactoring.test.js +194 -0
  62. package/server/agent/tests/autoContext.test.js +176 -0
  63. package/server/agent/tests/autoResearch.test.js +64 -0
  64. package/server/agent/tests/bilibiliEmbed.test.js +296 -0
  65. package/server/agent/tests/catalogSync.test.js +130 -0
  66. package/server/agent/tests/chatHardening.test.js +121 -0
  67. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  68. package/server/agent/tests/citations.test.js +180 -0
  69. package/server/agent/tests/clarificationAnalytics.test.js +219 -0
  70. package/server/agent/tests/clarifyingQuestions.test.js +627 -0
  71. package/server/agent/tests/cliAutomation.test.js +158 -0
  72. package/server/agent/tests/cloudProviders.test.js +322 -0
  73. package/server/agent/tests/commandInput.test.js +24 -0
  74. package/server/agent/tests/commands.test.js +506 -0
  75. package/server/agent/tests/contextIntelligence.test.js +209 -0
  76. package/server/agent/tests/conversationSafety.test.js +53 -0
  77. package/server/agent/tests/cpuMetrics.test.js +101 -0
  78. package/server/agent/tests/decisionAndSearchEnhancements.test.js +167 -0
  79. package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
  80. package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
  81. package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
  82. package/server/agent/tests/edgeCasesScenarios.test.js +131 -0
  83. package/server/agent/tests/engineService.test.js +436 -0
  84. package/server/agent/tests/engineUnload.test.js +94 -0
  85. package/server/agent/tests/entertainmentMetrics.test.js +126 -0
  86. package/server/agent/tests/envBootstrap.test.js +87 -0
  87. package/server/agent/tests/envCommand.test.js +284 -0
  88. package/server/agent/tests/errorHandling.test.js +80 -0
  89. package/server/agent/tests/errors.test.js +13 -0
  90. package/server/agent/tests/evalAnalyze.test.js +54 -0
  91. package/server/agent/tests/evalCommand.test.js +54 -0
  92. package/server/agent/tests/evalCorpus.test.js +126 -0
  93. package/server/agent/tests/evalHarness.test.js +62 -0
  94. package/server/agent/tests/evalJudgeContext.test.js +38 -0
  95. package/server/agent/tests/evalMetrics.test.js +320 -0
  96. package/server/agent/tests/evalStore.test.js +190 -0
  97. package/server/agent/tests/evalStudio.test.js +179 -0
  98. package/server/agent/tests/feedbackLearning.test.js +186 -0
  99. package/server/agent/tests/fetchApi.test.js +99 -0
  100. package/server/agent/tests/firstRun.test.js +44 -0
  101. package/server/agent/tests/gemma4eOptimization.test.js +95 -0
  102. package/server/agent/tests/hardwareTiers.test.js +206 -0
  103. package/server/agent/tests/hybridWebSearch.test.js +194 -0
  104. package/server/agent/tests/judgeService.test.js +251 -0
  105. package/server/agent/tests/lanDiscovery.test.js +324 -0
  106. package/server/agent/tests/lanInterfaces.test.js +510 -0
  107. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  108. package/server/agent/tests/locationTool.test.js +278 -0
  109. package/server/agent/tests/mcpClient.test.js +201 -0
  110. package/server/agent/tests/memoryRoute.test.js +173 -0
  111. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  112. package/server/agent/tests/modelCompatibility.test.js +148 -0
  113. package/server/agent/tests/modelManager.test.js +141 -0
  114. package/server/agent/tests/multilingualScenarios.test.js +69 -0
  115. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  116. package/server/agent/tests/newCommands.test.js +347 -0
  117. package/server/agent/tests/newCommandsTui.test.js +141 -0
  118. package/server/agent/tests/newTools.test.js +113 -0
  119. package/server/agent/tests/newsIntelligence.test.js +178 -0
  120. package/server/agent/tests/nextGenArchitecture.test.js +101 -0
  121. package/server/agent/tests/offlineInput.test.js +217 -0
  122. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  123. package/server/agent/tests/openDomainSearch.test.js +121 -0
  124. package/server/agent/tests/packagingGuard.test.js +57 -0
  125. package/server/agent/tests/presentation.test.js +61 -0
  126. package/server/agent/tests/processManager.test.js +144 -0
  127. package/server/agent/tests/providerPersistence.test.js +44 -0
  128. package/server/agent/tests/queryBenchmarks.test.js +83 -0
  129. package/server/agent/tests/queryDetails.test.js +126 -0
  130. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  131. package/server/agent/tests/realWorldValidation.test.js +391 -0
  132. package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
  133. package/server/agent/tests/runtimeWatchdog.test.js +32 -0
  134. package/server/agent/tests/sanitizer.test.js +37 -0
  135. package/server/agent/tests/scenariosData.js +378 -0
  136. package/server/agent/tests/schemaRouting.test.js +54 -0
  137. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  138. package/server/agent/tests/securityAuditRemediation.test.js +221 -0
  139. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  140. package/server/agent/tests/securityHardening.test.js +146 -0
  141. package/server/agent/tests/securitySanitizer.test.js +38 -0
  142. package/server/agent/tests/sessionAndCopy.test.js +267 -0
  143. package/server/agent/tests/sidebarControls.test.js +135 -0
  144. package/server/agent/tests/statefulMemory.test.js +277 -0
  145. package/server/agent/tests/subagent.test.js +216 -0
  146. package/server/agent/tests/subagentProgress.test.js +109 -0
  147. package/server/agent/tests/subagentTextFallback.test.js +115 -0
  148. package/server/agent/tests/suggestions.test.js +199 -0
  149. package/server/agent/tests/suggestionsTui.test.js +251 -0
  150. package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
  151. package/server/agent/tests/terminalImage.test.js +80 -0
  152. package/server/agent/tests/terminalMarkdown.test.js +166 -0
  153. package/server/agent/tests/terminalText.test.js +133 -0
  154. package/server/agent/tests/textProtocolLoop.test.js +197 -0
  155. package/server/agent/tests/textToolParsing.test.js +187 -0
  156. package/server/agent/tests/theme.test.js +26 -0
  157. package/server/agent/tests/toolRegistryParity.test.js +53 -0
  158. package/server/agent/tests/toolRoutingRegressions.test.js +160 -0
  159. package/server/agent/tests/tools.test.js +259 -0
  160. package/server/agent/tests/unifiedEndpoints.test.js +159 -0
  161. package/server/agent/tests/v110Features.test.js +262 -0
  162. package/server/agent/tests/versionBump.test.js +69 -0
  163. package/server/agent/tests/vramEstimator.test.js +595 -0
  164. package/server/agent/tests/webChatDocs.test.js +199 -0
  165. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  166. package/server/agent/tests/webChatInspection.test.js +551 -0
  167. package/server/agent/tests/webChatMcp.test.js +94 -0
  168. package/server/agent/tests/webChatMemory.test.js +112 -0
  169. package/server/agent/tests/webChatSecurity.test.js +169 -0
  170. package/server/agent/tests/webExtract.test.js +284 -0
  171. package/server/agent/tests/webSources.test.js +281 -0
  172. package/server/agent/textToolProtocol.js +173 -46
  173. package/server/agent/toolRouter.js +318 -44
  174. package/server/agent/tools.js +877 -96
  175. package/server/agent/ui.js +39 -34
  176. package/server/agent/vramService.js +35 -54
  177. package/server/agent/webSearch.js +441 -162
  178. package/server/agent/websearch/orchestrator.js +15 -5
  179. package/server/agent/websearch/resilience/cache.js +9 -2
  180. package/server/agent/websearch/security/ssrf.js +41 -10
  181. package/server/agent/youtubeAuth.js +5 -20
  182. package/server/apiRouter.js +5558 -4422
  183. package/server/eval/judgeRubrics.js +110 -0
  184. package/server/eval/judgeService.js +290 -0
  185. package/server/eval/runBenchmark.js +182 -0
  186. package/server/mcp/data/discography.js +2 -2
  187. package/server/mcp/examples/antigravity.example.json +17 -0
  188. package/server/mcp/examples/librechat.example.yaml +16 -0
  189. package/server/mcp/handler.js +8 -4
  190. package/server/mcp/security.js +220 -21
  191. package/server/mcp/server.js +2 -1
  192. package/server/mcp/tests/connection.test.js +162 -0
  193. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  194. package/server/mcp/tools.js +7 -1
  195. package/server/supabase_migration_prefix.sql +102 -0
  196. package/server/supabase_schema.sql +63 -0
  197. package/server/web-chat/core/connection.js +179 -218
  198. package/server/web-chat/core/conversationContext.js +9 -2
  199. package/server/web-chat/core/directCatalogEngine.js +52 -28
  200. package/server/web-chat/core/dramaKnowledge.js +33 -22
  201. package/server/web-chat/core/followUpEngine.js +735 -81
  202. package/server/web-chat/core/futureEventGuard.js +5 -1
  203. package/server/web-chat/core/musicControlAction.js +70 -0
  204. package/server/web-chat/core/security.js +32 -21
  205. package/server/web-chat/core/toolInspector.js +6 -6
  206. package/server/web-chat/core/urlGuard.js +45 -0
  207. package/server/web-chat/router.js +204 -157
  208. package/server/web-chat/routes/chatRoute.js +885 -488
  209. package/server/web-chat/routes/engineRoute.js +270 -0
  210. package/server/web-chat/routes/exportRoute.js +1 -1
  211. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  212. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  213. package/server/web-chat/routes/mcpRoute.js +386 -0
  214. package/server/web-chat/routes/memoryRoute.js +253 -0
  215. package/server/web-chat/routes/providerRoute.js +49 -16
  216. package/server/web-chat/routes/staticRoute.js +306 -17
  217. package/server/web-chat/routes/statusRoute.js +194 -73
  218. package/server/web-chat/routes/suggestRoute.js +11 -3
  219. package/server/web-chat/routes/verifyRoute.js +26 -4
  220. package/server/web-chat/server.js +7 -6
  221. package/server/web-chat/ui/docsBuilder.js +3558 -0
  222. package/server/web-chat/ui/htmlBuilder.js +12 -0
  223. package/server/web-chat/ui/scripts/client.js +3401 -1302
  224. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  225. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  226. package/server/web-chat/ui/styles/lan-card.css +165 -0
  227. package/server/web-chat/ui/styles/main.css +4289 -447
  228. package/server/web-chat/ui/sw.js +64 -0
  229. package/server/web-chat/ui/templates/body.html +407 -281
  230. package/src/data/awardsData.js +146 -146
  231. package/src/data/musicVideoData.js +6 -4
  232. package/src/lib/vram/estimator.js +195 -19
@@ -0,0 +1,87 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { bootstrapEnv } from '../envBootstrap.js';
7
+ import { isCookieFileValid } from '../youtubeAuth.js';
8
+
9
+ function tmpDir() {
10
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'kiku-envboot-'));
11
+ }
12
+
13
+ function withCleanEnv(keys, fn) {
14
+ const saved = keys.map(k => [k, process.env[k]]);
15
+ for (const [k] of saved) delete process.env[k];
16
+ return Promise.resolve()
17
+ .then(fn)
18
+ .finally(() => {
19
+ for (const [k, v] of saved) {
20
+ if (v === undefined) delete process.env[k];
21
+ else process.env[k] = v;
22
+ }
23
+ });
24
+ }
25
+
26
+ test('bootstrapEnv: package file wins, user file fills unset keys, process.env always wins', async () => {
27
+ const dir = tmpDir();
28
+ const pkg = path.join(dir, 'package.env');
29
+ const user = path.join(dir, 'user.env');
30
+ fs.writeFileSync(pkg, 'BOOT_A=pkg\nBOOT_SHARED=pkg\n');
31
+ fs.writeFileSync(user, 'BOOT_SHARED=user\nBOOT_B=user\n# BOOT_COMMENTED=hidden\n');
32
+
33
+ await withCleanEnv(['BOOT_A', 'BOOT_SHARED', 'BOOT_B', 'BOOT_COMMENTED'], async () => {
34
+ process.env.BOOT_SHARED = 'already-set';
35
+ const result = bootstrapEnv({ packageEnvPath: pkg, userEnvPath: user, force: true });
36
+
37
+ assert.equal(result.applied, true);
38
+ assert.equal(process.env.BOOT_A, 'pkg');
39
+ assert.equal(process.env.BOOT_B, 'user');
40
+ assert.equal(process.env.BOOT_SHARED, 'already-set', 'existing process.env values must never be overwritten');
41
+ assert.equal(process.env.BOOT_COMMENTED, undefined, 'commented keys must stay inactive');
42
+ assert.equal(result.files.length, 2);
43
+ });
44
+ });
45
+
46
+ test('bootstrapEnv: is idempotent without force and forceable for tests', async () => {
47
+ const dir = tmpDir();
48
+ const first = bootstrapEnv({ force: true, packageEnvPath: path.join(dir, 'missing.env'), userEnvPath: path.join(dir, 'missing2.env') });
49
+ assert.equal(first.applied, true);
50
+
51
+ const second = bootstrapEnv({ packageEnvPath: path.join(dir, 'x.env'), userEnvPath: path.join(dir, 'y.env') });
52
+ assert.equal(second.files.length, 0, 'non-forced call must be a no-op after bootstrap');
53
+
54
+ const forced = bootstrapEnv({ force: true, packageEnvPath: path.join(dir, 'x.env'), userEnvPath: path.join(dir, 'y.env') });
55
+ assert.equal(forced.files.length, 0);
56
+ fs.rmSync(dir, { recursive: true, force: true });
57
+ });
58
+
59
+ test('bootstrapEnv: tolerates CRLF files and strips surrounding quotes', async () => {
60
+ const dir = tmpDir();
61
+ const pkg = path.join(dir, 'package.env');
62
+ fs.writeFileSync(pkg, 'BOOT_CRLF = "crlf value" \r\nBOOT_QUOTED=\'quoted\'\r\n');
63
+
64
+ await withCleanEnv(['BOOT_CRLF', 'BOOT_QUOTED'], async () => {
65
+ bootstrapEnv({ packageEnvPath: pkg, userEnvPath: path.join(dir, 'none.env'), force: true });
66
+ assert.equal(process.env.BOOT_CRLF, 'crlf value');
67
+ assert.equal(process.env.BOOT_QUOTED, 'quoted');
68
+ });
69
+ });
70
+
71
+ test('isCookieFileValid rejects empty, short, and non-Netscape cookie files', () => {
72
+ const dir = tmpDir();
73
+ assert.equal(isCookieFileValid(path.join(dir, 'missing.txt')), false);
74
+
75
+ const empty = path.join(dir, 'empty.txt');
76
+ fs.writeFileSync(empty, '');
77
+ assert.equal(isCookieFileValid(empty), false);
78
+
79
+ const garbage = path.join(dir, 'garbage.txt');
80
+ fs.writeFileSync(garbage, 'not a cookie file at all\njust words\n');
81
+ assert.equal(isCookieFileValid(garbage), false);
82
+
83
+ const netscape = path.join(dir, 'cookies.txt');
84
+ fs.writeFileSync(netscape, '# Netscape HTTP Cookie File\n.youtube.com\tTRUE\t/\tTRUE\t0\tKEY\tvalue\n');
85
+ assert.equal(isCookieFileValid(netscape), true);
86
+ fs.rmSync(dir, { recursive: true, force: true });
87
+ });
@@ -0,0 +1,284 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import {
7
+ resolveEnvTargets,
8
+ readEnvFile,
9
+ readMergedEnvMap,
10
+ upsertEnvKey,
11
+ removeEnvKey,
12
+ maskValue,
13
+ isSecretKey,
14
+ buildEnvTemplate,
15
+ generateEnvFile,
16
+ detectAutoDefaults,
17
+ } from '../envManager.js';
18
+ import { routeCommand } from '../commands.js';
19
+
20
+ function tmpDir() {
21
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'kiku-env-'));
22
+ }
23
+
24
+ test('envManager: upsert preserves comments and replaces active and commented keys', () => {
25
+ const dir = tmpDir();
26
+ const file = path.join(dir, '.env');
27
+ fs.writeFileSync(file, '# header comment\nLMSTUDIO_BASE_URL=http://a/\n# KIKU_VRAM_TOTAL_GB=\nKIKU_YTDLP=yt-dlp\n');
28
+
29
+ upsertEnvKey(file, 'KIKU_CONTEXT_LIMIT', '131072');
30
+ upsertEnvKey(file, 'LMSTUDIO_BASE_URL', 'http://127.0.0.1:1234/v1');
31
+ upsertEnvKey(file, 'KIKU_VRAM_TOTAL_GB', '6');
32
+
33
+ const map = readEnvFile(file);
34
+ assert.equal(map.LMSTUDIO_BASE_URL, 'http://127.0.0.1:1234/v1');
35
+ assert.equal(map.KIKU_CONTEXT_LIMIT, '131072');
36
+ assert.equal(map.KIKU_VRAM_TOTAL_GB, '6');
37
+ assert.equal(map.KIKU_YTDLP, 'yt-dlp');
38
+
39
+ const raw = fs.readFileSync(file, 'utf8');
40
+ assert.ok(raw.includes('# header comment'), 'comments must survive upserts');
41
+ assert.ok(!/^KIKU_VRAM_TOTAL_GB=$/m.test(raw), 'no leftover empty duplicate');
42
+
43
+ removeEnvKey(file, 'KIKU_CONTEXT_LIMIT');
44
+ assert.equal(readEnvFile(file).KIKU_CONTEXT_LIMIT, undefined);
45
+ fs.rmSync(dir, { recursive: true, force: true });
46
+ });
47
+
48
+ test('envManager: masks secrets but leaves plain configuration readable', () => {
49
+ assert.ok(isSecretKey('OPENAI_API_KEY'));
50
+ assert.ok(isSecretKey('KIKU_MASTER_KEYS'));
51
+ assert.ok(isSecretKey('KIKU_YT_COOKIES'));
52
+ assert.ok(!isSecretKey('LMSTUDIO_BASE_URL'));
53
+ assert.ok(!isSecretKey('KIKU_CONTEXT_LIMIT'));
54
+ assert.ok(maskValue('OPENAI_API_KEY', 'sk-abcdefghijklmnopqrs').includes('…'));
55
+ assert.equal(maskValue('LMSTUDIO_BASE_URL', 'http://127.0.0.1:1234/v1'), 'http://127.0.0.1:1234/v1');
56
+ assert.equal(maskValue('OPENAI_API_KEY', ''), '(not set)');
57
+ });
58
+
59
+ test('envManager: generated template sets only necessary values and comments the rest', () => {
60
+ const content = buildEnvTemplate({
61
+ values: { LMSTUDIO_BASE_URL: 'http://127.0.0.1:1234/v1', KIKU_CONTEXT_LIMIT: '131072' },
62
+ detected: { loadedModel: 'google/gemma-4-e4b', contextLength: 131072 },
63
+ });
64
+
65
+ assert.ok(/^LMSTUDIO_BASE_URL=http:\/\/127\.0\.0\.1:1234\/v1$/m.test(content));
66
+ assert.ok(/^KIKU_CONTEXT_LIMIT=131072$/m.test(content));
67
+ assert.ok(/^# LMSTUDIO_MODELS_DIR=/m.test(content), 'recommended vars without values stay commented');
68
+ assert.ok(/^# KIKU_YT_COOKIES=/m.test(content), 'optional vars stay commented');
69
+ assert.ok(/^# OPENAI_API_KEY=/m.test(content), 'cloud keys stay commented');
70
+ assert.ok(/never set by fans\/CLI users/.test(content), 'server-only group is explicitly excluded');
71
+ assert.ok(/already configured by the platform developer/.test(content), 'server credentials are attributed to the hosted deployment');
72
+ for (const serverKey of ['UPSTASH_REDIS_REST_URL', 'UPSTASH_REDIS_REST_TOKEN', 'SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_ANON_KEY']) {
73
+ assert.ok(!new RegExp(`^${serverKey}=`).test(content), `${serverKey} must never be set active by the template`);
74
+ }
75
+ assert.ok(/gemma-4-e4b/.test(content), 'detection provenance is recorded');
76
+ });
77
+
78
+ test('envManager: generateEnvFile refuses to overwrite silently', () => {
79
+ const dir = tmpDir();
80
+ const file = path.join(dir, '.env');
81
+ generateEnvFile({ targetPath: file, values: { LMSTUDIO_BASE_URL: 'http://x/' } });
82
+ assert.throws(
83
+ () => generateEnvFile({ targetPath: file, values: {}, allowOverwrite: false }),
84
+ /already exists/
85
+ );
86
+ fs.rmSync(dir, { recursive: true, force: true });
87
+ });
88
+
89
+ test('envManager: resolveEnvTargets prefers the package file and falls back to the user file', () => {
90
+ const dir = tmpDir();
91
+ const pkg = path.join(dir, 'package.env');
92
+ const user = path.join(dir, 'user.env');
93
+
94
+ const without = resolveEnvTargets({ packageEnvPath: pkg, userEnvPath: user });
95
+ assert.equal(without.mode, 'user');
96
+ assert.equal(without.primaryPath, user);
97
+
98
+ fs.writeFileSync(pkg, 'A=1\n');
99
+ const withPkg = resolveEnvTargets({ packageEnvPath: pkg, userEnvPath: user });
100
+ assert.equal(withPkg.mode, 'repo');
101
+ assert.equal(withPkg.primaryPath, pkg);
102
+ fs.rmSync(dir, { recursive: true, force: true });
103
+ });
104
+
105
+ test('/env: bare status renders without touching files', async () => {
106
+ assert.equal(await routeCommand('/env', { envProbe: false }), 'HANDLED');
107
+ });
108
+
109
+ test('/env: path subcommand renders', async () => {
110
+ assert.equal(await routeCommand('/env path', {}), 'HANDLED');
111
+ });
112
+
113
+ test('/env: init writes defaults to an override target without prompting', async () => {
114
+ const dir = tmpDir();
115
+ const target = path.join(dir, '.env');
116
+ assert.equal(await routeCommand('/env init', { envOverridePath: target, envProbe: false }), 'HANDLED');
117
+ const map = readEnvFile(target);
118
+ assert.equal(map.LMSTUDIO_BASE_URL, 'http://127.0.0.1:1234/v1');
119
+ assert.equal(map.OPENAI_API_KEY, undefined, 'cloud keys are never set unless the user asks');
120
+ assert.equal(map.KIKU_MASTER_KEYS, undefined, 'server-only vars are never set by the wizard');
121
+ fs.rmSync(dir, { recursive: true, force: true });
122
+ });
123
+
124
+ function withInteractiveStdin(fn) {
125
+ const previousTty = process.stdin.isTTY;
126
+ process.stdin.isTTY = true;
127
+ return Promise.resolve(fn()).finally(() => {
128
+ if (previousTty === undefined) delete process.stdin.isTTY;
129
+ else process.stdin.isTTY = previousTty;
130
+ });
131
+ }
132
+
133
+ test('/env: init routes wizard questions through ctx.readInput (TUI handoff)', async () => {
134
+ const dir = tmpDir();
135
+ const target = path.join(dir, '.env');
136
+
137
+ const asked = [];
138
+ const answers = ['', '32768', '', 'n'];
139
+ const ctx = {
140
+ sub: 'init',
141
+ parts: ['/env', 'init'],
142
+ extra: '',
143
+ envOverridePath: target,
144
+ envProbe: false,
145
+ readInput: async (options) => {
146
+ asked.push(options);
147
+ return answers.shift() ?? '';
148
+ },
149
+ };
150
+
151
+ await withInteractiveStdin(() => routeCommand('/env init', ctx));
152
+
153
+ assert.equal(asked.length, 4, 'four wizard questions — no answer leaks into a chat query');
154
+ assert.ok(asked.every(q => q.history === false), 'wizard answers never enter command history');
155
+ const map = readEnvFile(target);
156
+ assert.equal(map.LMSTUDIO_BASE_URL, 'http://127.0.0.1:1234/v1', 'empty answer accepts the default');
157
+ assert.equal(map.KIKU_CONTEXT_LIMIT, '32768');
158
+ assert.equal(map.LMSTUDIO_MODELS_DIR, undefined);
159
+ assert.equal(map.OPENAI_API_KEY, undefined);
160
+ fs.rmSync(dir, { recursive: true, force: true });
161
+ });
162
+
163
+ test('/env: init overwrite question goes through ctx.readInput and regenerates the file', async () => {
164
+ const dir = tmpDir();
165
+ const packageFile = path.join(dir, 'package.env');
166
+ const userFile = path.join(dir, 'user.env');
167
+ fs.writeFileSync(packageFile, 'LMSTUDIO_BASE_URL=http://127.0.0.1:9999/v1\n');
168
+
169
+ const asked = [];
170
+ const answers = ['y', 'http://127.0.0.1:1234/v1', '', '', 'n'];
171
+ const ctx = {
172
+ sub: 'init',
173
+ parts: ['/env', 'init'],
174
+ extra: '',
175
+ envTargets: { packageEnvPath: packageFile, userEnvPath: userFile },
176
+ envProbe: false,
177
+ readInput: async (options) => {
178
+ asked.push(options);
179
+ return answers.shift() ?? '';
180
+ },
181
+ };
182
+
183
+ await withInteractiveStdin(() => routeCommand('/env init', ctx));
184
+
185
+ assert.ok(asked.length >= 1);
186
+ assert.match(String(asked[0].prompt), /Overwrite/, 'first question is the overwrite confirmation');
187
+ const map = readEnvFile(packageFile);
188
+ assert.equal(map.LMSTUDIO_BASE_URL, 'http://127.0.0.1:1234/v1', 'existing file is regenerated after confirm');
189
+ assert.equal(readEnvFile(userFile), null, 'user env file stays untouched');
190
+ fs.rmSync(dir, { recursive: true, force: true });
191
+ });
192
+
193
+ test('/env: set/get/unset round-trips through the override target', async () => {
194
+ const dir = tmpDir();
195
+ const target = path.join(dir, '.env');
196
+ assert.equal(await routeCommand('/env set KIKU_CONTEXT_LIMIT 32768', { envOverridePath: target }), 'HANDLED');
197
+ assert.equal(readEnvFile(target).KIKU_CONTEXT_LIMIT, '32768');
198
+ assert.equal(await routeCommand('/env get KIKU_CONTEXT_LIMIT', { envOverridePath: target }), 'HANDLED');
199
+ assert.equal(await routeCommand('/env unset KIKU_CONTEXT_LIMIT', { envOverridePath: target }), 'HANDLED');
200
+ assert.equal(readEnvFile(target).KIKU_CONTEXT_LIMIT, undefined);
201
+ fs.rmSync(dir, { recursive: true, force: true });
202
+ });
203
+
204
+ test('/env: unset with an override target never touches the real package or user files', async () => {
205
+ const dir = tmpDir();
206
+ const target = path.join(dir, 'override.env');
207
+ const pkg = path.join(dir, 'pkg.env');
208
+ const user = path.join(dir, 'user.env');
209
+ fs.writeFileSync(target, 'KIKU_CONTEXT_LIMIT=8192\n');
210
+ fs.writeFileSync(pkg, 'KIKU_CONTEXT_LIMIT=131072\n');
211
+ fs.writeFileSync(user, 'OTHER=1\n');
212
+
213
+ await routeCommand('/env unset KIKU_CONTEXT_LIMIT', {
214
+ envOverridePath: target,
215
+ envTargets: { packageEnvPath: pkg, userEnvPath: user },
216
+ });
217
+
218
+ assert.equal(readEnvFile(target).KIKU_CONTEXT_LIMIT, undefined, 'override file must be cleared');
219
+ assert.equal(readEnvFile(pkg).KIKU_CONTEXT_LIMIT, '131072', 'real package file must stay untouched');
220
+ assert.equal(readEnvFile(user).OTHER, '1', 'real user file must stay untouched');
221
+ fs.rmSync(dir, { recursive: true, force: true });
222
+ });
223
+
224
+ test('envManager: readMergedEnvMap resolves package-first with user fallback and tracks sources', () => {
225
+ const dir = tmpDir();
226
+ const pkg = path.join(dir, 'package.env');
227
+ const user = path.join(dir, 'user.env');
228
+ fs.writeFileSync(pkg, 'LMSTUDIO_BASE_URL=http://pkg:1234/v1\nONLY_PKG=1\n');
229
+ fs.writeFileSync(user, 'LMSTUDIO_BASE_URL=http://user:1234/v1\nONLY_USER=2\n');
230
+
231
+ const { map, sources } = readMergedEnvMap({ packageEnvPath: pkg, userEnvPath: user });
232
+ assert.equal(map.LMSTUDIO_BASE_URL, 'http://pkg:1234/v1', 'package file must win');
233
+ assert.equal(map.ONLY_PKG, '1');
234
+ assert.equal(map.ONLY_USER, '2', 'user file must fill unset keys');
235
+ assert.equal(sources.LMSTUDIO_BASE_URL, pkg);
236
+ assert.equal(sources.ONLY_USER, user);
237
+ fs.rmSync(dir, { recursive: true, force: true });
238
+ });
239
+
240
+ test('/env: get reports keys set only in the user file and unset clears both files', async () => {
241
+ const dir = tmpDir();
242
+ const pkg = path.join(dir, 'package.env');
243
+ const user = path.join(dir, 'user.env');
244
+ fs.writeFileSync(pkg, 'ONLY_PKG=1\nSHARED=pkg\n');
245
+ fs.writeFileSync(user, 'ONLY_USER=2\nSHARED=user\n');
246
+
247
+ const output = await captureConsoleOutput(() => routeCommand('/env get ONLY_USER', { envTargets: { packageEnvPath: pkg, userEnvPath: user } }));
248
+ assert.match(output, /ONLY_USER=2/);
249
+ assert.match(output, /user\.env/, 'must reveal which file the value comes from');
250
+
251
+ await routeCommand('/env unset SHARED', { envTargets: { packageEnvPath: pkg, userEnvPath: user } });
252
+ assert.equal(readEnvFile(pkg).SHARED, undefined, 'package copy must be removed');
253
+ assert.equal(readEnvFile(user).SHARED, undefined, 'user copy must be removed');
254
+ fs.rmSync(dir, { recursive: true, force: true });
255
+ });
256
+
257
+ test('envManager: removeEnvKey rejects regex-unsafe keys instead of throwing opaque errors', () => {
258
+ const dir = tmpDir();
259
+ const file = path.join(dir, '.env');
260
+ fs.writeFileSync(file, 'A=1\n');
261
+ assert.throws(() => removeEnvKey(file, '*'), /Invalid environment variable name/);
262
+ assert.throws(() => removeEnvKey(file, '('), /Invalid environment variable name/);
263
+ assert.equal(readEnvFile(file).A, '1');
264
+ fs.rmSync(dir, { recursive: true, force: true });
265
+ });
266
+
267
+ test('envManager: detectAutoDefaults with probe:false performs no network access', async () => {
268
+ const detected = await detectAutoDefaults({ probe: false, baseUrl: 'http://127.0.0.1:1/v1', timeoutMs: 100 });
269
+ assert.equal(detected.lmStudioReachable, false);
270
+ assert.equal(detected.loadedModel, null);
271
+ assert.ok('modelsDir' in detected, 'filesystem-only model dir detection still runs');
272
+ });
273
+
274
+ async function captureConsoleOutput(fn) {
275
+ const chunks = [];
276
+ const original = console.log;
277
+ console.log = (...args) => chunks.push(args.map(a => String(a)).join(' '));
278
+ try {
279
+ await fn();
280
+ } finally {
281
+ console.log = original;
282
+ }
283
+ return chunks.join('\n').replace(/\x1b\[[0-9;]*m/g, '');
284
+ }
@@ -0,0 +1,80 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import {
5
+ parseApiErrorText,
6
+ parseExecutionError,
7
+ formatExecutionErrorCard,
8
+ createAbortError,
9
+ isAbortError,
10
+ } from '../errors.js';
11
+
12
+ test('parseApiErrorText extracts clean human-readable error messages from JSON payloads', () => {
13
+ const openRouterRaw = JSON.stringify({
14
+ error: {
15
+ message: 'Insufficient credits. This account never purchased credits. Make sure your key is on the correct account or org, and if so, purchase more at https://openrouter.ai/settings/credits',
16
+ code: 402,
17
+ metadata: { limit_source: 'credits' }
18
+ }
19
+ });
20
+
21
+ const parsed = parseApiErrorText(openRouterRaw);
22
+ assert.equal(parsed.includes('Insufficient credits.'), true);
23
+ assert.equal(parsed.includes('{"error":'), false);
24
+
25
+ const stringError = JSON.stringify({ error: 'Rate limit exceeded: TPM limit 6000 reached' });
26
+ assert.equal(parseApiErrorText(stringError), 'Rate limit exceeded: TPM limit 6000 reached');
27
+
28
+ const plainText = 'Gateway Timeout 504';
29
+ assert.equal(parseApiErrorText(plainText), 'Gateway Timeout 504');
30
+ });
31
+
32
+ test('parseExecutionError generates structured diagnostics for HTTP 402 Insufficient Credits', () => {
33
+ const err = new Error('OpenRouter Cloud error (402): {"error":{"message":"Insufficient credits. Please add balance."}}');
34
+ err.status = 402;
35
+ const diag = parseExecutionError(err, { provider: 'openrouter', providerName: 'OpenRouter Cloud' });
36
+
37
+ assert.equal(diag.type, 'insufficient_credits');
38
+ assert.equal(diag.title.includes('402'), true);
39
+ assert.equal(diag.headline.includes('out of credits'), true);
40
+ assert.equal(diag.recoveryOptions.some(opt => opt.cmd === '/provider use lmstudio'), true);
41
+ });
42
+
43
+ test('parseExecutionError generates structured diagnostics for HTTP 429 Rate Limit', () => {
44
+ const err = new Error('Groq Cloud error (429): Rate limit reached for model');
45
+ err.status = 429;
46
+ const diag = parseExecutionError(err, { provider: 'groq', providerName: 'Groq Cloud', model: 'llama-3.3-70b' });
47
+
48
+ assert.equal(diag.type, 'rate_limit');
49
+ assert.equal(diag.title.includes('429'), true);
50
+ assert.equal(diag.recoveryOptions.some(opt => opt.cmd === '/provider use lmstudio'), true);
51
+ });
52
+
53
+ test('parseExecutionError handles connection refused and local vs cloud providers', () => {
54
+ const localErr = new Error('fetch failed: connect ECONNREFUSED 127.0.0.1:1234');
55
+ const localDiag = parseExecutionError(localErr, { provider: 'lmstudio', providerName: 'LM Studio' });
56
+ assert.equal(localDiag.type, 'network');
57
+ assert.equal(localDiag.title.includes('LM Studio'), true);
58
+ assert.equal(localDiag.recoveryOptions.some(opt => opt.cmd === '/model reconnect'), true);
59
+
60
+ const cloudErr = new Error('fetch failed: connect ENOTFOUND api.groq.com');
61
+ const cloudDiag = parseExecutionError(cloudErr, { provider: 'groq', providerName: 'Groq Cloud' });
62
+ assert.equal(cloudDiag.type, 'network');
63
+ assert.equal(cloudDiag.recoveryOptions.some(opt => opt.cmd === '/provider use lmstudio'), true);
64
+ });
65
+
66
+ test('formatExecutionErrorCard creates a clean rich card options object', () => {
67
+ const err = new Error('OpenRouter Cloud error (402): {"error":{"message":"Insufficient credits."}}');
68
+ const card = formatExecutionErrorCard(err, { provider: 'openrouter', providerName: 'OpenRouter Cloud' });
69
+
70
+ assert.equal(card.status, 'error');
71
+ assert.equal(card.title.includes('OpenRouter Cloud'), true);
72
+ assert.equal(card.lines.length > 0, true);
73
+ assert.equal(card.lines.some(l => l.includes('/provider use lmstudio')), true);
74
+ });
75
+
76
+ test('createAbortError and isAbortError work properly', () => {
77
+ const abortErr = createAbortError();
78
+ assert.equal(isAbortError(abortErr), true);
79
+ assert.equal(isAbortError(new Error('regular error')), false);
80
+ });
@@ -0,0 +1,13 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { createAbortError, isAbortError } from '../errors.js';
5
+
6
+ test('cancellation preserves AbortError identity across layers', () => {
7
+ const error = createAbortError();
8
+
9
+ assert.equal(error.name, 'AbortError');
10
+ assert.equal(error.message, 'Query cancelled by user.');
11
+ assert.equal(isAbortError(error), true);
12
+ assert.equal(isAbortError(new Error('failure')), false);
13
+ });
@@ -0,0 +1,54 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const here = path.dirname(fileURLToPath(import.meta.url));
9
+ const repoRoot = path.resolve(here, '../../..');
10
+ const script = path.join(repoRoot, 'server/agent/eval/analyze.py');
11
+
12
+ function runPython(args) {
13
+ for (const bin of ['python', 'python3', 'py']) {
14
+ const extra = bin === 'py' ? ['-3'] : [];
15
+ const result = spawnSync(bin, [...extra, script, ...args], { cwd: repoRoot, encoding: 'utf8', timeout: 60000 });
16
+ if (result.error?.code === 'ENOENT') continue;
17
+ return result;
18
+ }
19
+ throw new Error('no python interpreter found');
20
+ }
21
+
22
+ test('analyze.py exists and keeps eval no-comment rule', () => {
23
+ assert.equal(fs.existsSync(script), true);
24
+ const text = fs.readFileSync(script, 'utf8');
25
+ const hashLines = text.split('\n').filter((l) => l.includes('#'));
26
+ assert.deepEqual(hashLines, []);
27
+ });
28
+
29
+ test('analyze codebase mode returns whole-repo inventory and drift', () => {
30
+ const result = runPython(['--mode', 'codebase', '--skip-corpus']);
31
+ assert.equal(result.status, 0, result.stderr);
32
+ const payload = JSON.parse(result.stdout);
33
+ assert.ok(payload.codebase.fileCount > 200);
34
+ assert.ok(payload.codebase.totalLines > 50000);
35
+ assert.ok(payload.catalogDrift.pairCount === 8);
36
+ assert.ok(Array.isArray(payload.catalogDrift.pairs));
37
+ });
38
+
39
+ test('analyze report mode handles live report and gates', () => {
40
+ const report = path.join(repoRoot, 'artifacts/eval/live.json');
41
+ if (!fs.existsSync(report)) return;
42
+ const result = runPython(['--mode', 'report', '--report', 'artifacts/eval/live.json', '--skip-corpus']);
43
+ assert.equal(result.status, 0, result.stderr);
44
+ const payload = JSON.parse(result.stdout);
45
+ assert.equal(payload.report.status, 'ok');
46
+ assert.equal(payload.gates.available, true);
47
+ });
48
+
49
+ test('eval CLI exposes analyze subcommand and npm shortcut', () => {
50
+ const cliText = fs.readFileSync(path.join(repoRoot, 'server/agent/eval/modules/core/cli.js'), 'utf8');
51
+ assert.match(cliText, /analyze/);
52
+ const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
53
+ assert.equal(pkg.scripts['eval:analyze'], 'node server/agent/eval/modules/core/cli.js analyze');
54
+ });
@@ -0,0 +1,54 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { performance } from 'node:perf_hooks';
4
+ import { handlers } from '../commands.js';
5
+
6
+ function makeCtx(command) {
7
+ const parts = command.split(/\s+/);
8
+ return {
9
+ parts,
10
+ sub: parts[1] || '',
11
+ extra: parts.slice(2).join(' '),
12
+ client: { provider: 'lmstudio', providerName: 'LM Studio', model: 'google/gemma-4-e4b', baseUrl: 'http://127.0.0.1:1234/v1' },
13
+ agent: {},
14
+ };
15
+ }
16
+
17
+ async function captureConsole(fn) {
18
+ const chunks = [];
19
+ const original = console.log;
20
+ console.log = (...args) => chunks.push(args.map(a => String(a)).join(' '));
21
+ try {
22
+ const result = await fn();
23
+ return { result, text: chunks.join('\n') };
24
+ } finally {
25
+ console.log = original;
26
+ }
27
+ }
28
+
29
+ test('/eval status summarizes the live report in-process and never spawns a run', async () => {
30
+ const started = performance.now();
31
+ const { result, text } = await captureConsole(() => handlers['/eval'](makeCtx('/eval status')));
32
+ const elapsed = performance.now() - started;
33
+
34
+ assert.equal(result, 'HANDLED');
35
+ assert.ok(elapsed < 1000, `/eval status took ${elapsed.toFixed(0)}ms — a spawn-based regression would take seconds`);
36
+ assert.ok(
37
+ text.includes('Pass@1') || text.includes('No evaluation report found yet'),
38
+ 'must show the live report summary or the empty-state card'
39
+ );
40
+ });
41
+
42
+ test('/eval with an unknown subcommand shows the help card instead of forwarding to the eval CLI', async () => {
43
+ const { result, text } = await captureConsole(() => handlers['/eval'](makeCtx('/eval foobar')));
44
+ assert.equal(result, 'HANDLED');
45
+ assert.ok(text.includes('Subcommands'), 'unknown subcommand must render the help card');
46
+ assert.ok(!text.includes('exited with code'), 'unknown subcommand must not spawn the eval CLI');
47
+ });
48
+
49
+ test('/eval validate verifies the frozen corpus in-process', async () => {
50
+ const { result, text } = await captureConsole(() => handlers['/eval'](makeCtx('/eval validate')));
51
+ assert.equal(result, 'HANDLED');
52
+ assert.ok(text.includes('Corpus Valid'), 'validation must succeed');
53
+ assert.ok(text.includes('catalog'), 'panel must include the catalog split');
54
+ });