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,217 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { routeCommand } from '../commands.js';
5
+
6
+ test('TUI prompt reflects offline and online state accurately', async () => {
7
+
8
+ let catalogOnly = true;
9
+ const mockPrompt = {
10
+ isConnected: false,
11
+ modelName: 'gemma-4-e4b',
12
+ renderCalls: 0,
13
+ render() {
14
+ this.renderCalls += 1;
15
+ },
16
+ };
17
+
18
+ const mockModelManager = {
19
+ async resolve() {
20
+ return { connected: true, selectedModel: 'google/gemma-4-e4b' };
21
+ },
22
+ };
23
+
24
+ const ctx = {
25
+ sub: 'reconnect',
26
+ modelManager: mockModelManager,
27
+ tuiPrompt: mockPrompt,
28
+ client: { provider: 'lmstudio', providerName: 'LM Studio' },
29
+ setCatalogOnlyMode(val) {
30
+ catalogOnly = val;
31
+ },
32
+ };
33
+
34
+ const res = await routeCommand('/model reconnect', ctx);
35
+ assert.equal(res, 'HANDLED');
36
+ assert.equal(catalogOnly, false);
37
+ assert.equal(mockPrompt.isConnected, true);
38
+ assert.equal(mockPrompt.modelName, 'google/gemma-4-e4b');
39
+ assert.ok(mockPrompt.renderCalls > 0);
40
+ });
41
+
42
+ test('TUI prompt stays offline when /model reconnect fails', async () => {
43
+ let catalogOnly = false;
44
+ const mockPrompt = {
45
+ isConnected: true,
46
+ modelName: 'gemma-4-e4b',
47
+ renderCalls: 0,
48
+ render() {
49
+ this.renderCalls += 1;
50
+ },
51
+ };
52
+
53
+ const mockModelManager = {
54
+ async resolve() {
55
+ return { connected: false, error: 'Connection refused at http://127.0.0.1:1234' };
56
+ },
57
+ };
58
+
59
+ const ctx = {
60
+ sub: 'reconnect',
61
+ modelManager: mockModelManager,
62
+ tuiPrompt: mockPrompt,
63
+ client: { provider: 'lmstudio', providerName: 'LM Studio' },
64
+ setCatalogOnlyMode(val) {
65
+ catalogOnly = val;
66
+ },
67
+ };
68
+
69
+ const res = await routeCommand('/model reconnect', ctx);
70
+ assert.equal(res, 'HANDLED');
71
+ assert.equal(catalogOnly, true);
72
+ assert.equal(mockPrompt.isConnected, false);
73
+ assert.ok(mockPrompt.renderCalls > 0);
74
+ });
75
+
76
+ test('Provider switch updates tuiPrompt isConnected and provider info', async () => {
77
+ let catalogOnly = true;
78
+ const mockPrompt = {
79
+ isConnected: false,
80
+ modelName: 'gemma-4-e4b',
81
+ provider: 'LM Studio',
82
+ render() {},
83
+ };
84
+
85
+ const mockClient = {
86
+ provider: 'lmstudio',
87
+ providerName: 'LM Studio',
88
+ switchProvider(target, opts) {
89
+ return {
90
+ provider: target,
91
+ providerName: 'OpenAI',
92
+ model: opts?.model || 'gpt-4o',
93
+ baseUrl: 'https://api.openai.com/v1',
94
+ hasApiKey: true,
95
+ isLocal: false,
96
+ };
97
+ },
98
+ };
99
+
100
+ const mockModelManager = {
101
+ client: mockClient,
102
+ requestedModel: 'gpt-4o',
103
+ async resolve() {
104
+ return { connected: true, selectedModel: 'gpt-4o' };
105
+ },
106
+ };
107
+
108
+ const ctx = {
109
+ sub: 'use',
110
+ extra: 'openai gpt-4o',
111
+ parts: ['/provider', 'use', 'openai', 'gpt-4o'],
112
+ client: mockClient,
113
+ modelManager: mockModelManager,
114
+ tuiPrompt: mockPrompt,
115
+ setCatalogOnlyMode(val) {
116
+ catalogOnly = val;
117
+ },
118
+ };
119
+
120
+ const res = await routeCommand('/provider use openai gpt-4o', ctx);
121
+ assert.equal(res, 'HANDLED');
122
+ assert.equal(catalogOnly, false);
123
+ assert.equal(mockPrompt.isConnected, true);
124
+ assert.equal(mockPrompt.provider, 'OpenAI');
125
+ assert.equal(mockPrompt.modelName, 'gpt-4o');
126
+ });
127
+
128
+ test('/model list automatically connects when model is loaded in VRAM without /model reconnect', async () => {
129
+ let catalogOnly = true;
130
+ const mockPrompt = {
131
+ isConnected: false,
132
+ modelName: 'gemma-4-e4b',
133
+ renderCalls: 0,
134
+ render() {
135
+ this.renderCalls += 1;
136
+ },
137
+ };
138
+
139
+ const mockModelManager = {
140
+ async inspect() {
141
+ return {
142
+ connected: true,
143
+ models: ['google/gemma-4-e4b', 'text-embedding-nomic-embed-text-v1.5'],
144
+ activeLoadedModel: 'google/gemma-4-e4b',
145
+ };
146
+ },
147
+ async resolve() {
148
+ return {
149
+ connected: true,
150
+ selectedModel: 'google/gemma-4-e4b',
151
+ };
152
+ },
153
+ };
154
+
155
+ const ctx = {
156
+ sub: 'list',
157
+ modelManager: mockModelManager,
158
+ tuiPrompt: mockPrompt,
159
+ client: { provider: 'lmstudio', providerName: 'LM Studio', model: 'google/gemma-4-e4b' },
160
+ setCatalogOnlyMode(val) {
161
+ catalogOnly = val;
162
+ },
163
+ };
164
+
165
+ const res = await routeCommand('/model list', ctx);
166
+ assert.equal(res, 'HANDLED');
167
+ assert.equal(catalogOnly, false, 'catalogOnlyMode should automatically become false');
168
+ assert.equal(mockPrompt.isConnected, true, 'tuiPrompt should automatically become connected');
169
+ assert.equal(mockPrompt.modelName, 'google/gemma-4-e4b');
170
+ assert.ok(mockPrompt.renderCalls > 0);
171
+ });
172
+
173
+ test('/model status automatically connects when model is loaded in VRAM', async () => {
174
+ let catalogOnly = true;
175
+ const mockPrompt = {
176
+ isConnected: false,
177
+ modelName: 'gemma-4-e4b',
178
+ renderCalls: 0,
179
+ render() {
180
+ this.renderCalls += 1;
181
+ },
182
+ };
183
+
184
+ const mockModelManager = {
185
+ async inspect() {
186
+ return {
187
+ connected: true,
188
+ models: ['google/gemma-4-e4b'],
189
+ activeLoadedModel: 'google/gemma-4-e4b',
190
+ };
191
+ },
192
+ async resolve() {
193
+ return {
194
+ connected: true,
195
+ selectedModel: 'google/gemma-4-e4b',
196
+ };
197
+ },
198
+ };
199
+
200
+ const ctx = {
201
+ sub: 'status',
202
+ modelManager: mockModelManager,
203
+ tuiPrompt: mockPrompt,
204
+ client: { provider: 'lmstudio', providerName: 'LM Studio', model: 'google/gemma-4-e4b' },
205
+ setCatalogOnlyMode(val) {
206
+ catalogOnly = val;
207
+ },
208
+ };
209
+
210
+ const res = await routeCommand('/model status', ctx);
211
+ assert.equal(res, 'HANDLED');
212
+ assert.equal(catalogOnly, false);
213
+ assert.equal(mockPrompt.isConnected, true);
214
+ assert.equal(mockPrompt.modelName, 'google/gemma-4-e4b');
215
+ assert.ok(mockPrompt.renderCalls > 0);
216
+ });
217
+
@@ -0,0 +1,197 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ buildSystemPrompt,
5
+ buildOpenDomainSystemPrompt,
6
+ buildInDomainSystemPrompt,
7
+ buildDynamicRoutingDirective
8
+ } from '../prompts.js';
9
+ import {
10
+ classifyQuerySpectrum,
11
+ classifyQueryDomain,
12
+ isQueryInDomain
13
+ } from '../catalogEntityResolver.js';
14
+ import { KikuAgent } from '../agentCore.js';
15
+
16
+ test('buildOpenDomainSystemPrompt includes coding, math, science, and awakening directives', () => {
17
+ const prompt = buildOpenDomainSystemPrompt(['web_search', 'search_encyclopedia']);
18
+
19
+
20
+ assert.ok(prompt.includes('SOFTWARE ENGINEERING & CODING EXCELLENCE'));
21
+ assert.ok(prompt.includes('markdown fenced code blocks'));
22
+ assert.ok(prompt.includes('time complexity $O(N)$'));
23
+
24
+
25
+ assert.ok(prompt.includes('MATHEMATICS & SCIENTIFIC RIGOR'));
26
+ assert.ok(prompt.includes('LaTeX / KaTeX'));
27
+ assert.ok(prompt.includes('$ ... $'));
28
+ assert.ok(prompt.includes('$$ ... $$'));
29
+ assert.ok(prompt.includes('step-by-step reasoning'));
30
+
31
+
32
+ assert.ok(prompt.includes('CONTEXT AWAKENING PROTOCOL'));
33
+ assert.ok(prompt.includes('ตื่นรู้เข้าสู่บริบท'));
34
+ assert.ok(prompt.includes('search_works'));
35
+ assert.ok(prompt.includes('search_discography'));
36
+
37
+
38
+ assert.ok(prompt.includes('OPEN-DOMAIN TOOL RETRIEVAL & INTEGRITY'));
39
+ assert.ok(prompt.includes('web_search'));
40
+
41
+
42
+ assert.ok(prompt.includes('Fibonacci'));
43
+ assert.ok(prompt.includes('แคลคูลัส'));
44
+ assert.ok(prompt.includes('เทอร์โมไดนามิกส์'));
45
+ assert.ok(prompt.includes('[CONTEXT AWAKENING ACTIVATED]'));
46
+ });
47
+
48
+ test('buildInDomainSystemPrompt retains full Ju Jingyi concierge persona', () => {
49
+ const prompt = buildInDomainSystemPrompt(['search_works', 'search_discography']);
50
+
51
+ assert.ok(prompt.includes('KIKU AGENT (菊)'));
52
+ assert.ok(prompt.includes('authoritative AI encyclopedia agent and concierge for international artist Ju Jingyi'));
53
+ assert.ok(prompt.includes('MANDATORY RETRIEVAL RULE'));
54
+ assert.ok(prompt.includes('DUAL-TIER RETRIEVAL ARCHITECTURE'));
55
+ assert.ok(prompt.includes('ENTITY & UNIT DISAMBIGUATION'));
56
+ });
57
+
58
+ test('buildSystemPrompt dispatcher honors options and maintains backward compatibility', () => {
59
+
60
+ const defaultPrompt = buildSystemPrompt();
61
+ assert.ok(defaultPrompt.includes('concierge for international artist Ju Jingyi'));
62
+
63
+ const inDomainPrompt = buildSystemPrompt([], { inDomain: true });
64
+ assert.ok(inDomainPrompt.includes('concierge for international artist Ju Jingyi'));
65
+
66
+ const inDomainExplicit = buildSystemPrompt([], { domain: 'IN_DOMAIN' });
67
+ assert.ok(inDomainExplicit.includes('concierge for international artist Ju Jingyi'));
68
+
69
+
70
+ const openDomainPrompt = buildSystemPrompt([], { inDomain: false });
71
+ assert.ok(openDomainPrompt.includes('OPEN-DOMAIN TECHNICAL & PROBLEM-SOLVING MODE'));
72
+ assert.ok(openDomainPrompt.includes('CONTEXT AWAKENING PROTOCOL'));
73
+
74
+
75
+ const openDomainExplicit = buildSystemPrompt([], { domain: 'OPEN_DOMAIN' });
76
+ assert.ok(openDomainExplicit.includes('OPEN-DOMAIN TECHNICAL & PROBLEM-SOLVING MODE'));
77
+ });
78
+
79
+ test('buildDynamicRoutingDirective includes open-domain and general tools', () => {
80
+ const directive = buildDynamicRoutingDirective([
81
+ 'web_search',
82
+ 'search_encyclopedia',
83
+ 'get_weather',
84
+ 'search_location_map',
85
+ 'read_local_file',
86
+ ]);
87
+
88
+ assert.ok(directive.includes('Web Search & News -> web_search'));
89
+ assert.ok(directive.includes('Encyclopedia & General Knowledge -> search_encyclopedia'));
90
+ assert.ok(directive.includes('Real-time Weather & Location -> get_weather'));
91
+ assert.ok(directive.includes('Maps & Navigation -> search_location_map'));
92
+ assert.ok(directive.includes('System & Files -> read_local_file'));
93
+ });
94
+
95
+ test('classifyQuerySpectrum identifies coding, mathematics, and science as OPEN_DOMAIN', () => {
96
+ const testCases = [
97
+
98
+ 'ช่วยเขียนโค้ด Python เชื่อมต่อ WebSocket ให้หน่อย',
99
+ 'สอนเขียนโปรแกรม Golang จัดการ Goroutines',
100
+ 'แก้บั๊ก React useEffect infinite loop',
101
+ 'เขียนฟังก์ชัน TypeScript สำหรับ debounce',
102
+ 'Write a quicksort algorithm in C++',
103
+
104
+
105
+ 'ช่วยแก้โจทย์คณิตศาสตร์หน่อย อินทิกรัล 2x dx',
106
+ 'แก้สมการ x^2 - 5x + 6 = 0 ให้ดูทีละขั้นตอน',
107
+ 'ช่วยคำนวณแคลคูลัสหาอนุพันธ์ของ f(x) = sin(x)*e^x',
108
+ 'อธิบายทฤษฎีบทพีทาโกรัสและวิธีพิสูจน์',
109
+ 'Calculate the eigenvalues of a 2x2 matrix',
110
+
111
+
112
+ 'อธิบายกฎข้อที่ 2 ของเทอร์โมไดนามิกส์',
113
+ 'กระบวนการสังเคราะห์ด้วยแสงในพืช light reaction กับ dark reaction แตกต่างกันอย่างไร',
114
+ 'ทำไมน้ำถึงมีพันธะไฮโดรเจนและมีความจุความร้อนสูง',
115
+ 'Explain the structure of an atom and electron orbitals',
116
+ 'ความแตกต่างระหว่างแรงโน้มถ่วงและแรงแม่เหล็กไฟฟ้า'
117
+ ];
118
+
119
+ for (const query of testCases) {
120
+ const domain = classifyQueryDomain(query);
121
+ const inDomain = isQueryInDomain(query);
122
+ assert.strictEqual(domain, 'OPEN_DOMAIN', `Expected OPEN_DOMAIN for query: "${query}"`);
123
+ assert.strictEqual(inDomain, false, `Expected inDomain=false for query: "${query}"`);
124
+ }
125
+ });
126
+
127
+ test('classifyQuerySpectrum awakens to IN_DOMAIN when Ju Jingyi or catalog entities are mentioned', () => {
128
+ const awakeningCases = [
129
+
130
+ 'ช่วยเขียนโค้ด Python ดึงรายชื่อผลงานซีรีส์ของจวีจิ้งอี',
131
+ 'คำนวณว่าจวีจิ้งอีเดบิวต์ในวงการมากี่ปีแล้ว',
132
+ 'เพลง 0.2s ของจวีจิ้งอีมีความยาวกี่นาที',
133
+ 'ซีรีส์เรื่อง In Blossom จวีจิ้งอีแสดงเป็นตัวละครอะไร',
134
+ 'จวีจิ้งอีเป็นแอมบาสเดอร์ของแบรนด์ FRED Paris ตั้งแต่ปีไหน',
135
+ 'ยอดขายกล่องของขวัญ TuTuCare ของจวีจิ้งอีขายหมดในกี่วินาที',
136
+ 'เพลง Dewdrops ประกอบซีรีส์เรื่องอะไร',
137
+ 'What dramas has Ju Jingyi starred in?'
138
+ ];
139
+
140
+ for (const query of awakeningCases) {
141
+ const domain = classifyQueryDomain(query);
142
+ const inDomain = isQueryInDomain(query);
143
+ assert.strictEqual(domain, 'IN_DOMAIN', `Expected IN_DOMAIN awakening for query: "${query}"`);
144
+ assert.strictEqual(inDomain, true, `Expected inDomain=true for query: "${query}"`);
145
+ }
146
+ });
147
+
148
+ test('KikuAgent dynamically adapts systemPrompt between open-domain and in-domain turns', async () => {
149
+ const agent = new KikuAgent();
150
+ await agent.ready();
151
+
152
+
153
+ const inDomainPrompt = agent.buildSystemPrompt(null, { inDomain: true });
154
+ assert.ok(inDomainPrompt.includes('concierge for international artist Ju Jingyi'));
155
+
156
+ const openDomainPrompt = agent.buildSystemPrompt(null, { inDomain: false });
157
+ assert.ok(openDomainPrompt.includes('OPEN-DOMAIN TECHNICAL & PROBLEM-SOLVING MODE'));
158
+ assert.ok(openDomainPrompt.includes('CONTEXT AWAKENING PROTOCOL'));
159
+ });
160
+
161
+ test('KikuAgent ask() triggers Context Awakening in multi-turn transition', async () => {
162
+ const capturedMessagesList = [];
163
+ const mockClient = {
164
+ model: 'mock-kiku',
165
+ baseUrl: 'http://127.0.0.1:1234',
166
+ provider: 'lmstudio',
167
+ providerName: 'lmstudio',
168
+ chat: async ({ messages }) => {
169
+ capturedMessagesList.push([...messages]);
170
+ return {
171
+ content: 'Response from mock LLM'
172
+ };
173
+ }
174
+ };
175
+
176
+ const agent = new KikuAgent({ client: mockClient });
177
+ await agent.ready();
178
+
179
+
180
+ await agent.ask('ช่วยเขียนฟังก์ชัน Python สำหรับคำนวณ Fibonacci หน่อย');
181
+ assert.strictEqual(capturedMessagesList.length, 1);
182
+ const turn1Messages = capturedMessagesList[0];
183
+ const turn1SystemPrompt = turn1Messages[0].content;
184
+ assert.ok(turn1SystemPrompt.includes('OPEN-DOMAIN TECHNICAL & PROBLEM-SOLVING MODE'), 'Turn 1 must use open-domain system prompt');
185
+ const turn1OpenDirective = turn1Messages.find(m => typeof m.content === 'string' && m.content.includes('OPEN-DOMAIN EXPERT DIRECTIVE'));
186
+ assert.ok(turn1OpenDirective, 'Turn 1 must include OPEN-DOMAIN EXPERT DIRECTIVE');
187
+
188
+
189
+ await agent.ask('แล้วจวีจิ้งอีมีผลงานซีรีส์เรื่องล่าสุดอะไรบ้าง?');
190
+ assert.strictEqual(capturedMessagesList.length, 2);
191
+ const turn2Messages = capturedMessagesList[1];
192
+ const turn2SystemPrompt = turn2Messages[0].content;
193
+ assert.ok(turn2SystemPrompt.includes('concierge for international artist Ju Jingyi'), 'Turn 2 must awaken to Ju Jingyi concierge persona');
194
+ const turn2AwakeningDirective = turn2Messages.find(m => typeof m.content === 'string' && m.content.includes('CONTEXT AWAKENING ACTIVATED'));
195
+ assert.ok(turn2AwakeningDirective, 'Turn 2 must include CONTEXT AWAKENING ACTIVATED directive');
196
+ });
197
+
@@ -0,0 +1,121 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ classifyQueryDomain,
5
+ isQueryInDomain,
6
+ QUERY_DOMAINS,
7
+ getContextualizedSearchQuery
8
+ } from '../catalogEntityResolver.js';
9
+ import { dynamicToolRouter } from '../toolRouter.js';
10
+ import { buildImageSearchQuery } from '../webSearch.js';
11
+ import { createCatalogMatcher, toolHandlers } from '../tools.js';
12
+
13
+ test('classifyQueryDomain and isQueryInDomain correctly classify in-domain and open-domain queries', () => {
14
+
15
+ const inDomainQueries = [
16
+ 'จวีจิ้งอี มีผลงานอะไรบ้าง',
17
+ 'Ju Jingyi dramas 2026',
18
+ '鞠婧祎 花间令',
19
+ 'เพลง 0.2s ใครร้อง',
20
+ 'เสี่ยวจวี เป็นแบรนด์แอมบาสเดอร์อะไรบ้าง',
21
+ 'Veil of Shadows พระเอกคือใคร',
22
+ 'ตำนานรักนางพญางูขาว เล่นเป็นใคร',
23
+ 'Kiku fanclub',
24
+ 'แบรนด์ FRED Paris จวีจิ้งอี'
25
+ ];
26
+
27
+ for (const q of inDomainQueries) {
28
+ assert.equal(
29
+ isQueryInDomain(q),
30
+ true,
31
+ `Expected "${q}" to be recognized as in-domain`
32
+ );
33
+ }
34
+
35
+
36
+ const openDomainQueries = [
37
+ 'วิธีทำต้มยำกุ้งน้ำข้น',
38
+ 'สภาพอากาศ โตเกียว วันนี้',
39
+ 'What is quantum computing?',
40
+ 'Who won the 2022 FIFA World Cup?',
41
+ 'Bitcoin price history and future outlook',
42
+ 'Taylor Swift The Eras Tour setlist',
43
+ 'ทฤษฎีสัมพัทธภาพของไอน์สไตน์คืออะไร',
44
+ 'ประวัติศาสตร์อยุธยาตอนต้น',
45
+ 'How to fix TypeError in JavaScript',
46
+ 'สูตรทำข้าวมันไก่'
47
+ ];
48
+
49
+ for (const q of openDomainQueries) {
50
+ assert.equal(
51
+ classifyQueryDomain(q),
52
+ QUERY_DOMAINS.OPEN_DOMAIN,
53
+ `Expected "${q}" to be classified as OPEN_DOMAIN`
54
+ );
55
+ assert.equal(
56
+ isQueryInDomain(q),
57
+ false,
58
+ `Expected "${q}" to NOT be in-domain`
59
+ );
60
+ }
61
+ });
62
+
63
+ test('getContextualizedSearchQuery preserves open-domain queries without injecting artist tokens', () => {
64
+ const openQuery = 'วิธีทำแกงเขียวหวานไก่';
65
+ const result = getContextualizedSearchQuery(openQuery, 'chinese_web');
66
+ assert.equal(result, openQuery, 'Open-domain query should not have artist injected');
67
+ assert.equal(result.includes('鞠婧祎'), false);
68
+ assert.equal(result.includes('จวีจิ้งอี'), false);
69
+
70
+ const inDomainQuery = 'ผลงานเพลง 0.2s';
71
+ const inDomainResult = getContextualizedSearchQuery(inDomainQuery, 'chinese_web');
72
+ assert.ok(
73
+ inDomainResult.includes('鞠婧祎') || inDomainResult.includes('0.2s') || inDomainResult.includes('0.2秒'),
74
+ 'In-domain query should be contextualized'
75
+ );
76
+ });
77
+
78
+ test('buildImageSearchQuery does not prepend artist tokens for open-domain queries', () => {
79
+ const openQuery = 'Eiffel Tower at night';
80
+ const imgQuery = buildImageSearchQuery(openQuery);
81
+ assert.equal(imgQuery, 'Eiffel Tower at night');
82
+ assert.equal(imgQuery.includes('鞠婧祎'), false);
83
+
84
+ const inDomainQuery = 'In Blossom drama poster';
85
+ const inDomainImgQuery = buildImageSearchQuery(inDomainQuery);
86
+ assert.ok(inDomainImgQuery.includes('鞠婧祎'), 'In-domain query should include artist name');
87
+ });
88
+
89
+ test('toolRouter detects OPEN_DOMAIN intent and routes general web/encyclopedia search tools', () => {
90
+ const openQuery = 'ทำไมท้องฟ้าถึงเป็นสีฟ้า';
91
+ const intents = dynamicToolRouter.detectIntents(openQuery).map(p => p.type);
92
+ assert.ok(
93
+ intents.includes('OPEN_DOMAIN'),
94
+ 'Should detect OPEN_DOMAIN intent for general science query'
95
+ );
96
+
97
+ const tools = dynamicToolRouter.routeToolsForQuery(openQuery);
98
+ const toolNames = tools.map(t => t.function?.name || t.name);
99
+ assert.ok(toolNames.includes('web_search'), 'Should include web_search for open-domain queries');
100
+ assert.ok(
101
+ !toolNames.includes('search_discography'),
102
+ 'Should not force search_discography on open-domain science question'
103
+ );
104
+ });
105
+
106
+ test('search_works matches plot concepts and synopses in Thai', async () => {
107
+
108
+ const res = await toolHandlers.search_works({ query: 'แฟนตาซี' });
109
+ assert.equal(res.status, 'success');
110
+ assert.ok(res.count > 0);
111
+ assert.ok(
112
+ res.works.some(w => (w.synopsis || '').includes('แฟนตาซี') || (w.titleEn || '').includes('Veil of Shadows'))
113
+ );
114
+ });
115
+
116
+ test('createCatalogMatcher handles unspaced Thai queries with stop phrases', () => {
117
+ const matcher = createCatalogMatcher('อยากดูซีรีส์แนวสืบคดี');
118
+
119
+ const haystack = 'ซีรีส์แฟนตาซีฟอร์มยักษ์ นำไปสู่การสืบคดีสไตล์หักเหลี่ยมซ่อนเงื่อน';
120
+ assert.equal(matcher(haystack), true);
121
+ });
@@ -0,0 +1,57 @@
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 { fileURLToPath } from 'node:url';
6
+ import {
7
+ CRITICAL_RUNTIME_FILES,
8
+ getPackagingIssues,
9
+ isCoveredByFiles,
10
+ matchesFilesPattern,
11
+ } from '../../../scripts/verifyPackaging.js';
12
+
13
+ const REPO_ROOT = fileURLToPath(new URL('../../../', import.meta.url));
14
+
15
+ test('packaging guard covers the websearch crash regression (webSearch.js -> websearch/index.js)', () => {
16
+ const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
17
+ assert.ok(Array.isArray(pkg.files), 'package.json must define a "files" whitelist');
18
+ assert.ok(
19
+ isCoveredByFiles('server/agent/websearch/index.js', pkg.files),
20
+ 'websearch/index.js must be packed or kiku-agent/kiku-mcp crash with ERR_MODULE_NOT_FOUND',
21
+ );
22
+ assert.ok(
23
+ isCoveredByFiles('server/agent/webSearch.js', pkg.files),
24
+ 'webSearch.js must be packed',
25
+ );
26
+ for (const rel of CRITICAL_RUNTIME_FILES) {
27
+ assert.ok(
28
+ fs.existsSync(path.join(REPO_ROOT, rel)),
29
+ `critical runtime file must exist on disk: ${rel}`,
30
+ );
31
+ assert.ok(isCoveredByFiles(rel, pkg.files), `critical runtime file must be packed: ${rel}`);
32
+ }
33
+ });
34
+
35
+ test('packaging guard uses future-proof globs instead of per-directory enumeration', () => {
36
+ const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
37
+ const hasBroad = pkg.files.some((p) => p === 'server/**/*' || p === 'server/**/*.js');
38
+ assert.ok(hasBroad, 'package.json "files" must contain "server/**/*" or "server/**/*.js" so new subdirectories are packed automatically');
39
+ assert.ok(
40
+ matchesFilesPattern('server/agent/futureModule/nested.js', 'server/**/*.js'),
41
+ 'broad glob must match future nested modules',
42
+ );
43
+ assert.ok(
44
+ matchesFilesPattern('server/agent/ink/App.jsx', 'server/**/*'),
45
+ 'broad glob must match .jsx entries',
46
+ );
47
+ });
48
+
49
+ test('packaging guard reports zero issues for the release tree', () => {
50
+ const { issues } = getPackagingIssues({ rootDir: REPO_ROOT });
51
+ assert.deepEqual(issues, []);
52
+ });
53
+
54
+ test('packaging guard detects an unpacked runtime file', () => {
55
+ assert.equal(isCoveredByFiles('server/agent/websearch/index.js', ['server/agent/*.js']), false);
56
+ assert.equal(isCoveredByFiles('server/agent/cli.js', ['server/agent/*.js']), true);
57
+ });
@@ -0,0 +1,61 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { readdirSync, readFileSync } from 'node:fs';
4
+ import { extname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const agentRoot = fileURLToPath(new URL('../', import.meta.url));
8
+ const ignoredDirectories = new Set(['docs', 'tests']);
9
+ const emojiPattern = /[\p{Extended_Pictographic}\p{Regional_Indicator}\uFE0F\u20E3]/gu;
10
+ const approvedEmoji = new Set(['📖', '🎵', '📢', '🌸', '✨', '💬', '🎨', '📸', '🖼', '🔗']);
11
+
12
+ function collectRuntimeSources(directory, output = []) {
13
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
14
+ if (entry.isDirectory() && ignoredDirectories.has(entry.name)) continue;
15
+ const fullPath = join(directory, entry.name);
16
+ if (entry.isDirectory()) collectRuntimeSources(fullPath, output);
17
+ else if (['.js', '.jsx'].includes(extname(entry.name))) output.push(fullPath);
18
+ }
19
+ return output;
20
+ }
21
+
22
+ test('Kiku Agent runtime presentation only uses approved semantic emoji', () => {
23
+ const offenders = [];
24
+ for (const file of collectRuntimeSources(agentRoot)) {
25
+ const matches = readFileSync(file, 'utf8').match(emojiPattern) || [];
26
+ const unapproved = matches.filter(character => !approvedEmoji.has(character));
27
+ if (unapproved.length > 0) {
28
+ offenders.push(`${file.slice(agentRoot.length)}: ${[...new Set(unapproved)].join(' ')}`);
29
+ }
30
+ }
31
+
32
+ assert.deepEqual(offenders, []);
33
+ });
34
+
35
+ test('VirtualScreenBuffer performs flicker-free differential row rendering', async () => {
36
+ const { VirtualScreenBuffer } = await import('../ui.js');
37
+ const writes = [];
38
+ const buffer = new VirtualScreenBuffer(40, 5, s => writes.push(s));
39
+ buffer.clear();
40
+ buffer.setRow(0, 'Header');
41
+ buffer.setRow(1, 'Content A');
42
+ buffer.setRow(2, 'Content B');
43
+
44
+
45
+ buffer.render();
46
+ assert.equal(buffer.forceFullRedraw, false);
47
+ assert.ok(writes.length > 0);
48
+
49
+
50
+ writes.length = 0;
51
+ buffer.setRow(1, 'Content A Updated');
52
+ assert.notEqual(buffer.rows[1], buffer.prevRows[1]);
53
+ assert.equal(buffer.rows[0], buffer.prevRows[0]);
54
+
55
+
56
+ buffer.render();
57
+ assert.equal(buffer.rows[1], buffer.prevRows[1]);
58
+ assert.ok(writes.length > 0);
59
+ assert.ok(!writes[0].includes('\x1b[2K'));
60
+ });
61
+