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
@@ -5,8 +5,12 @@ import {
5
5
  setActiveProvider,
6
6
  savePersistedProviderBaseUrl,
7
7
  getProvider,
8
+ getPersistedInferenceSettings,
9
+ savePersistedInferenceSettings,
10
+ withSettingsWriteQueue,
8
11
  } from '../../agent/config.js';
9
12
  import { currentAgent, setCurrentAgent } from '../core/connection.js';
13
+ import { validateInferenceBaseUrl, isTrustedRequest, sanitizeInferenceError } from '../core/urlGuard.js';
10
14
  import { LLMClient } from '../../agent/llmClient.js';
11
15
 
12
16
  export async function handleProviderGetRoute(req, res) {
@@ -14,6 +18,7 @@ export async function handleProviderGetRoute(req, res) {
14
18
  const catalog = getUnifiedProviderCatalog();
15
19
  sendJson(res, 200, {
16
20
  status: 'ok',
21
+ inference: getPersistedInferenceSettings(),
17
22
  activeProvider: catalog.activeProvider,
18
23
  activeBaseUrl: catalog.activeBaseUrl,
19
24
  activeModel: catalog.activeModel,
@@ -21,11 +26,12 @@ export async function handleProviderGetRoute(req, res) {
21
26
  providers: catalog.providers,
22
27
  });
23
28
  } catch (err) {
24
- sendError(res, 500, err.message);
29
+ sendError(res, 500, sanitizeInferenceError(err, isTrustedRequest(req)));
25
30
  }
26
31
  }
27
32
 
28
33
  export async function handleProviderUpdateRoute(req, res) {
34
+ if (!isTrustedRequest(req)) return sendError(res, 403, 'Changing server provider settings requires local or admin access');
29
35
  let body;
30
36
  try {
31
37
  body = await parseJsonBody(req);
@@ -33,40 +39,65 @@ export async function handleProviderUpdateRoute(req, res) {
33
39
  return sendError(res, 400, `Invalid JSON body: ${err.message}`);
34
40
  }
35
41
  try {
36
- const provider = String(body.provider || body.activeProvider || '').toLowerCase().trim() || null;
37
- const model = typeof body.model === 'string' && body.model.trim() ? body.model.trim() : null;
42
+ if (body.inference !== undefined && (!body.inference || typeof body.inference !== 'object' || Array.isArray(body.inference))) return sendError(res, 400, 'Invalid inference settings');
43
+ let provider = String(body.provider || body.activeProvider || '').toLowerCase().trim() || null;
44
+ // 'auto' means CLI/Server sync mode: no explicit provider switch, just return catalog.
45
+ if (provider === 'auto') provider = null;
46
+ let model = typeof body.model === 'string' && body.model.trim() ? body.model.trim() : null;
47
+ if (model && model.toLowerCase() === 'auto') model = null;
48
+ if (model && !/^[A-Za-z0-9._\-/: ]{1,200}$/.test(model)) {
49
+ return sendError(res, 400, `Invalid model id "${model.slice(0, 50)}"`);
50
+ }
38
51
  const baseUrl = typeof body.baseUrl === 'string' && body.baseUrl.trim() ? body.baseUrl.trim().replace(/\/+$/, '') : null;
39
52
 
40
- if (baseUrl && !provider) {
41
- return sendError(res, 400, 'Provider is required when setting a baseUrl');
53
+ if (!provider) {
54
+ // Auto (CLI/Server sync) mode: text-field leftovers (baseUrl/model) are
55
+ // display-only — ignore them and just report the active catalog.
56
+ if (body.inference) await withSettingsWriteQueue(() => savePersistedInferenceSettings(body.inference));
57
+ const catalog = getUnifiedProviderCatalog();
58
+ return sendJson(res, 200, {
59
+ status: 'ok',
60
+ inference: getPersistedInferenceSettings(),
61
+ activeProvider: catalog.activeProvider,
62
+ activeBaseUrl: catalog.activeBaseUrl,
63
+ activeModel: catalog.activeModel,
64
+ localEndpoints: catalog.localEndpoints,
65
+ providers: catalog.providers,
66
+ });
42
67
  }
43
68
 
44
69
  if (baseUrl && provider) {
45
70
  const pDef = getProvider(provider);
46
71
  if (!pDef) return sendError(res, 400, `Unknown provider "${provider}"`);
47
- try {
48
- new URL(baseUrl);
49
- } catch {
50
- return sendError(res, 400, `Invalid baseUrl "${baseUrl}"`);
51
- }
52
- savePersistedProviderBaseUrl(provider, baseUrl);
72
+ const guard = await validateInferenceBaseUrl(baseUrl, { callerIsTrusted: isTrustedRequest(req) });
73
+ if (!guard.ok) return sendError(res, 400, guard.reason);
74
+ await withSettingsWriteQueue(() => savePersistedProviderBaseUrl(provider, guard.normalized));
53
75
  }
54
76
 
55
77
  let catalog;
78
+ let switchError = null;
56
79
  if (provider) {
57
80
  const pDef = getProvider(provider);
58
81
  if (!pDef) return sendError(res, 400, `Unknown provider "${provider}"`);
59
- setActiveProvider(provider, { model, baseUrl });
82
+ await withSettingsWriteQueue(() => {
83
+ setActiveProvider(provider, { model, baseUrl });
84
+ if (body.inference) savePersistedInferenceSettings(body.inference);
85
+ });
60
86
  if (currentAgent?.client) {
61
87
  try {
62
- currentAgent.client.switchProvider(provider, { model: model || undefined, baseUrl: baseUrl || undefined });
88
+ await currentAgent.client.switchProvider(provider, { model: model || undefined, baseUrl: baseUrl || undefined });
63
89
  setCurrentAgent(currentAgent);
64
- } catch {}
90
+ } catch (err) {
91
+ // Persisted, but the live agent could not hot-swap — surface instead of swallowing.
92
+ switchError = err?.message || 'Failed to hot-swap the active agent client';
93
+ }
65
94
  } else if (baseUrl || model) {
66
95
  try {
67
96
  const client = new LLMClient({ provider, model: model || undefined, baseUrl: baseUrl || getProviderBaseUrl(provider) });
68
97
  void client;
69
- } catch {}
98
+ } catch (err) {
99
+ switchError = err?.message || 'Failed to construct a client for the requested provider';
100
+ }
70
101
  }
71
102
  catalog = getUnifiedProviderCatalog();
72
103
  } else {
@@ -75,6 +106,8 @@ export async function handleProviderUpdateRoute(req, res) {
75
106
 
76
107
  sendJson(res, 200, {
77
108
  status: 'ok',
109
+ inference: getPersistedInferenceSettings(),
110
+ switchError: switchError ? sanitizeInferenceError(switchError, isTrustedRequest(req)) : null,
78
111
  activeProvider: catalog.activeProvider,
79
112
  activeBaseUrl: catalog.activeBaseUrl,
80
113
  activeModel: catalog.activeModel,
@@ -82,6 +115,6 @@ export async function handleProviderUpdateRoute(req, res) {
82
115
  providers: catalog.providers,
83
116
  });
84
117
  } catch (err) {
85
- sendError(res, 500, err.message);
118
+ sendError(res, 500, sanitizeInferenceError(err, isTrustedRequest(req)));
86
119
  }
87
120
  }
@@ -1,7 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import zlib from 'node:zlib';
4
+ import crypto from 'node:crypto';
3
5
  import { fileURLToPath } from 'node:url';
4
6
  import { getWebChatHtml } from '../ui/htmlBuilder.js';
7
+ import { getSettingsHtml } from '../ui/settingsBuilder.js';
8
+ import { getDocsHtml } from '../ui/docsBuilder.js';
5
9
 
6
10
  const __filename = fileURLToPath(import.meta.url);
7
11
  const __dirname = path.dirname(__filename);
@@ -25,6 +29,161 @@ const MIME_TYPES = {
25
29
  '.txt': 'text/plain; charset=utf-8'
26
30
  };
27
31
 
32
+ const COMPRESSIBLE_EXTS = new Set(['.css', '.js', '.mjs', '.json', '.txt', '.svg']);
33
+
34
+
35
+ let webChatCache = null;
36
+ let webChatCacheTimestamp = 0;
37
+ let settingsCache = null;
38
+ let settingsCacheTimestamp = 0;
39
+ let docsCache = null;
40
+ let docsCacheTimestamp = 0;
41
+ const staticCompressCache = new Map();
42
+
43
+ function getCachedWebChatBundle(port) {
44
+ const now = Date.now();
45
+ const ttl = process.env.NODE_ENV === 'production' ? Infinity : 2000;
46
+ if (webChatCache && webChatCache.port === port && (now - webChatCacheTimestamp) < ttl) {
47
+ return webChatCache;
48
+ }
49
+ const html = getWebChatHtml(port);
50
+ const raw = Buffer.from(html, 'utf8');
51
+ const hash = crypto.createHash('md5').update(raw).digest('hex');
52
+ const etag = `W/"${hash}"`;
53
+ webChatCache = {
54
+ port,
55
+ raw,
56
+ etag,
57
+ br: null,
58
+ gzip: null,
59
+ deflate: null
60
+ };
61
+ webChatCacheTimestamp = now;
62
+ return webChatCache;
63
+ }
64
+
65
+ function getCachedSettingsBundle() {
66
+ const now = Date.now();
67
+ const ttl = process.env.NODE_ENV === 'production' ? Infinity : 2000;
68
+ if (settingsCache && (now - settingsCacheTimestamp) < ttl) {
69
+ return settingsCache;
70
+ }
71
+ const html = getSettingsHtml();
72
+ const raw = Buffer.from(html, 'utf8');
73
+ const hash = crypto.createHash('md5').update(raw).digest('hex');
74
+ const etag = `W/"${hash}"`;
75
+ settingsCache = {
76
+ raw,
77
+ etag,
78
+ br: null,
79
+ gzip: null,
80
+ deflate: null
81
+ };
82
+ settingsCacheTimestamp = now;
83
+ return settingsCache;
84
+ }
85
+
86
+ function getCachedDocsBundle() {
87
+ const now = Date.now();
88
+ const ttl = process.env.NODE_ENV === 'production' ? Infinity : 2000;
89
+ if (docsCache && (now - docsCacheTimestamp) < ttl) {
90
+ return docsCache;
91
+ }
92
+ const html = getDocsHtml();
93
+ const raw = Buffer.from(html, 'utf8');
94
+ const hash = crypto.createHash('md5').update(raw).digest('hex');
95
+ const etag = `W/"${hash}"`;
96
+ docsCache = {
97
+ raw,
98
+ etag,
99
+ br: null,
100
+ gzip: null,
101
+ deflate: null
102
+ };
103
+ docsCacheTimestamp = now;
104
+ return docsCache;
105
+ }
106
+
107
+ function sendCompressedResponse(req, res, statusCode, baseHeaders, rawBuffer, etag, cacheEntry = null) {
108
+ const reqHeaders = req?.headers || {};
109
+ const acceptEncoding = typeof reqHeaders['accept-encoding'] === 'string'
110
+ ? reqHeaders['accept-encoding'].toLowerCase()
111
+ : '';
112
+
113
+
114
+ const ifNoneMatch = reqHeaders['if-none-match'];
115
+ if (etag && ifNoneMatch) {
116
+ const cleanEtag = etag.replace(/^W\//, '');
117
+ const cleanClient = ifNoneMatch.replace(/^W\//, '');
118
+ if (ifNoneMatch === etag || cleanClient === cleanEtag || ifNoneMatch.includes(cleanEtag)) {
119
+ const respHeaders = {
120
+ 'ETag': etag,
121
+ 'Cache-Control': baseHeaders['Cache-Control'] || 'no-cache',
122
+ 'Vary': 'Accept-Encoding'
123
+ };
124
+ res.writeHead(304, respHeaders);
125
+ res.end();
126
+ return;
127
+ }
128
+ }
129
+
130
+ const respHeaders = { ...baseHeaders, 'Vary': 'Accept-Encoding' };
131
+ if (etag) {
132
+ respHeaders['ETag'] = etag;
133
+ }
134
+
135
+
136
+ if (rawBuffer.length >= 1024) {
137
+ if (acceptEncoding.includes('br')) {
138
+ let brBuf = cacheEntry?.br;
139
+ if (!brBuf) {
140
+ brBuf = zlib.brotliCompressSync(rawBuffer, {
141
+ params: {
142
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 5
143
+ }
144
+ });
145
+ if (cacheEntry) cacheEntry.br = brBuf;
146
+ }
147
+ respHeaders['Content-Encoding'] = 'br';
148
+ respHeaders['Content-Length'] = brBuf.length;
149
+ res.writeHead(statusCode, respHeaders);
150
+ res.end(brBuf);
151
+ return;
152
+ }
153
+
154
+ if (acceptEncoding.includes('gzip')) {
155
+ let gzBuf = cacheEntry?.gzip;
156
+ if (!gzBuf) {
157
+ gzBuf = zlib.gzipSync(rawBuffer, { level: 6 });
158
+ if (cacheEntry) cacheEntry.gzip = gzBuf;
159
+ }
160
+ respHeaders['Content-Encoding'] = 'gzip';
161
+ respHeaders['Content-Length'] = gzBuf.length;
162
+ res.writeHead(statusCode, respHeaders);
163
+ res.end(gzBuf);
164
+ return;
165
+ }
166
+
167
+ if (acceptEncoding.includes('deflate')) {
168
+ let defBuf = cacheEntry?.deflate;
169
+ if (!defBuf) {
170
+ defBuf = zlib.deflateSync(rawBuffer);
171
+ if (cacheEntry) cacheEntry.deflate = defBuf;
172
+ }
173
+ respHeaders['Content-Encoding'] = 'deflate';
174
+ respHeaders['Content-Length'] = defBuf.length;
175
+ res.writeHead(statusCode, respHeaders);
176
+ res.end(defBuf);
177
+ return;
178
+ }
179
+ }
180
+
181
+
182
+ respHeaders['Content-Length'] = rawBuffer.length;
183
+ res.writeHead(statusCode, respHeaders);
184
+ res.end(rawBuffer);
185
+ }
186
+
28
187
  export function handleStaticRoute(req, res, pathname, port) {
29
188
  const rawUrl = req?.url || '';
30
189
  let currentPath = pathname;
@@ -52,6 +211,8 @@ export function handleStaticRoute(req, res, pathname, port) {
52
211
  return true;
53
212
  }
54
213
 
214
+ const reqHeaders = req?.headers || {};
215
+
55
216
  if (currentPath === '/favicon.ico') {
56
217
  let icoPath = path.join(PUBLIC_DIR, 'favicon.ico');
57
218
  if (!fs.existsSync(icoPath)) {
@@ -59,8 +220,20 @@ export function handleStaticRoute(req, res, pathname, port) {
59
220
  if (fs.existsSync(cwdPath)) icoPath = cwdPath;
60
221
  }
61
222
  if (fs.existsSync(icoPath)) {
223
+ const stat = fs.statSync(icoPath);
224
+ const etag = `W/"${stat.size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`;
225
+ if (reqHeaders['if-none-match'] === etag) {
226
+ res.writeHead(304, {
227
+ 'ETag': etag,
228
+ 'Cache-Control': 'public, max-age=86400, immutable'
229
+ });
230
+ res.end();
231
+ return true;
232
+ }
62
233
  res.writeHead(200, {
63
234
  'Content-Type': 'image/x-icon',
235
+ 'Content-Length': stat.size,
236
+ 'ETag': etag,
64
237
  'Cache-Control': 'public, max-age=86400, immutable'
65
238
  });
66
239
  res.end(fs.readFileSync(icoPath));
@@ -74,15 +247,28 @@ export function handleStaticRoute(req, res, pathname, port) {
74
247
  const cwdPath = path.join(process.cwd(), 'public', 'sw.js');
75
248
  if (fs.existsSync(cwdPath)) swPath = cwdPath;
76
249
  }
77
- res.writeHead(200, {
78
- 'Content-Type': 'application/javascript; charset=utf-8',
79
- 'Service-Worker-Allowed': '/',
80
- 'Cache-Control': 'no-cache, no-store, must-revalidate'
81
- });
82
250
  if (fs.existsSync(swPath)) {
83
- fs.createReadStream(swPath).pipe(res);
251
+ const stat = fs.statSync(swPath);
252
+ const etag = `W/"${stat.size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`;
253
+ if (reqHeaders['if-none-match'] === etag) {
254
+ res.writeHead(304, {
255
+ 'ETag': etag,
256
+ 'Service-Worker-Allowed': '/',
257
+ 'Cache-Control': 'no-cache, must-revalidate',
258
+ 'Vary': 'Accept-Encoding'
259
+ });
260
+ res.end();
261
+ return true;
262
+ }
263
+ const raw = fs.readFileSync(swPath);
264
+ sendCompressedResponse(req, res, 200, {
265
+ 'Content-Type': 'application/javascript; charset=utf-8',
266
+ 'Service-Worker-Allowed': '/',
267
+ 'Cache-Control': 'no-cache, must-revalidate'
268
+ }, raw, etag);
269
+ return true;
84
270
  } else {
85
- res.end(`const CACHE_NAME = 'kiku-v1';
271
+ const defaultSw = `const CACHE_NAME = 'kiku-v1';
86
272
  const ASSETS = ['/', '/manifest.json'];
87
273
  self.addEventListener('install', (event) => {
88
274
  event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)).then(() => self.skipWaiting()));
@@ -91,6 +277,13 @@ self.addEventListener('activate', (event) => {
91
277
  event.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))).then(() => self.clients.claim()));
92
278
  });
93
279
  self.addEventListener('fetch', (event) => {
280
+ let url;
281
+ try {
282
+ url = new URL(event.request.url);
283
+ } catch {
284
+ return;
285
+ }
286
+ if (url.origin !== self.location.origin) return;
94
287
  if (event.request.url.includes('/api/')) return;
95
288
  event.respondWith(caches.match(event.request).then((cached) => cached || fetch(event.request).then((response) => {
96
289
  if (response.ok && event.request.method === 'GET') {
@@ -98,18 +291,26 @@ self.addEventListener('fetch', (event) => {
98
291
  caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
99
292
  }
100
293
  return response;
101
- }).catch(() => cached)));
102
- });`);
294
+ }).catch(() => cached || new Response('', { status: 504, statusText: 'Gateway Timeout' }))));
295
+ });`;
296
+ const raw = Buffer.from(defaultSw, 'utf8');
297
+ const hash = crypto.createHash('md5').update(raw).digest('hex');
298
+ sendCompressedResponse(req, res, 200, {
299
+ 'Content-Type': 'application/javascript; charset=utf-8',
300
+ 'Service-Worker-Allowed': '/',
301
+ 'Cache-Control': 'no-cache, must-revalidate'
302
+ }, raw, `W/"${hash}"`);
303
+ return true;
103
304
  }
104
- return true;
105
305
  }
106
306
 
107
- if (currentPath.startsWith('/fonts/') || currentPath.startsWith('/images/') || currentPath === '/manifest.json' || currentPath === '/robots.txt') {
307
+ const lookupPath = decodedPath || currentPath;
308
+ if (lookupPath.startsWith('/fonts/') || lookupPath.startsWith('/images/') || lookupPath === '/manifest.json' || lookupPath === '/robots.txt') {
108
309
  const targetDir = fs.existsSync(PUBLIC_DIR) ? PUBLIC_DIR : path.join(process.cwd(), 'public');
109
310
  const targetDirCanonical = path.resolve(targetDir);
110
311
  const targetDirPrefix = targetDirCanonical + path.sep;
111
312
 
112
- const relativePath = currentPath.replace(/^\/+/, '');
313
+ const relativePath = lookupPath.replace(/^\/+/, '');
113
314
  const resolvedPath = path.resolve(targetDirCanonical, relativePath);
114
315
 
115
316
  if (!resolvedPath.startsWith(targetDirPrefix) && resolvedPath !== targetDirCanonical) {
@@ -127,10 +328,80 @@ self.addEventListener('fetch', (event) => {
127
328
  }
128
329
  }
129
330
 
331
+ if (!fs.existsSync(filePath)) {
332
+ const altRel = relativePath.startsWith('images/general/')
333
+ ? 'images/' + relativePath.slice('images/general/'.length)
334
+ : relativePath.startsWith('images/')
335
+ ? 'images/general/' + relativePath.slice('images/'.length)
336
+ : null;
337
+ if (altRel) {
338
+ const altResolved = path.resolve(targetDirCanonical, altRel);
339
+ if (altResolved.startsWith(targetDirPrefix) && fs.existsSync(altResolved)) {
340
+ filePath = altResolved;
341
+ } else {
342
+ const cwdPublic = path.resolve(process.cwd(), 'public');
343
+ const altCwd = path.resolve(cwdPublic, altRel);
344
+ if (altCwd.startsWith(cwdPublic + path.sep) && fs.existsSync(altCwd)) {
345
+ filePath = altCwd;
346
+ }
347
+ }
348
+ }
349
+ }
350
+
130
351
  if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
352
+ const stat = fs.statSync(filePath);
131
353
  const ext = path.extname(filePath).toLowerCase();
354
+ const mime = MIME_TYPES[ext] || 'application/octet-stream';
355
+ const etag = `W/"${stat.size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`;
356
+
357
+
358
+ if (reqHeaders['if-none-match'] === etag) {
359
+ res.writeHead(304, {
360
+ 'ETag': etag,
361
+ 'Cache-Control': ext === '.json' || ext === '.txt' ? 'no-cache' : 'public, max-age=31536000, immutable',
362
+ 'Vary': 'Accept-Encoding'
363
+ });
364
+ res.end();
365
+ return true;
366
+ }
367
+ if (reqHeaders['if-modified-since']) {
368
+ const clientTime = new Date(reqHeaders['if-modified-since']).getTime();
369
+ if (!isNaN(clientTime) && Math.floor(stat.mtimeMs / 1000) <= Math.floor(clientTime / 1000)) {
370
+ res.writeHead(304, {
371
+ 'ETag': etag,
372
+ 'Cache-Control': ext === '.json' || ext === '.txt' ? 'no-cache' : 'public, max-age=31536000, immutable',
373
+ 'Vary': 'Accept-Encoding'
374
+ });
375
+ res.end();
376
+ return true;
377
+ }
378
+ }
379
+
380
+
381
+ if (COMPRESSIBLE_EXTS.has(ext) && stat.size >= 1024) {
382
+ let entry = staticCompressCache.get(filePath);
383
+ if (!entry || entry.mtimeMs !== stat.mtimeMs) {
384
+ const raw = fs.readFileSync(filePath);
385
+ entry = { mtimeMs: stat.mtimeMs, raw, etag, br: null, gzip: null, deflate: null };
386
+ if (staticCompressCache.size > 80) {
387
+ const oldest = staticCompressCache.keys().next().value;
388
+ staticCompressCache.delete(oldest);
389
+ }
390
+ staticCompressCache.set(filePath, entry);
391
+ }
392
+ sendCompressedResponse(req, res, 200, {
393
+ 'Content-Type': mime,
394
+ 'Cache-Control': ext === '.json' || ext === '.txt' ? 'no-cache' : 'public, max-age=31536000, immutable'
395
+ }, entry.raw, etag, entry);
396
+ return true;
397
+ }
398
+
399
+
132
400
  res.writeHead(200, {
133
- 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream',
401
+ 'Content-Type': mime,
402
+ 'Content-Length': stat.size,
403
+ 'ETag': etag,
404
+ 'Last-Modified': stat.mtime.toUTCString(),
134
405
  'Cache-Control': ext === '.json' || ext === '.txt' ? 'no-cache' : 'public, max-age=31536000, immutable'
135
406
  });
136
407
  fs.createReadStream(filePath).pipe(res);
@@ -138,12 +409,30 @@ self.addEventListener('fetch', (event) => {
138
409
  }
139
410
  }
140
411
 
412
+ if (currentPath === '/docs' || currentPath.startsWith('/docs/') || currentPath.startsWith('/docs?') || currentPath === '/settings/docs' || currentPath.startsWith('/settings/docs')) {
413
+ const bundle = getCachedDocsBundle();
414
+ sendCompressedResponse(req, res, 200, {
415
+ 'Content-Type': 'text/html; charset=utf-8',
416
+ 'Cache-Control': 'no-cache, must-revalidate'
417
+ }, bundle.raw, bundle.etag, bundle);
418
+ return true;
419
+ }
420
+
421
+ if (currentPath === '/settings' || currentPath.startsWith('/settings/') || currentPath.startsWith('/settings?')) {
422
+ const bundle = getCachedSettingsBundle();
423
+ sendCompressedResponse(req, res, 200, {
424
+ 'Content-Type': 'text/html; charset=utf-8',
425
+ 'Cache-Control': 'no-cache, must-revalidate'
426
+ }, bundle.raw, bundle.etag, bundle);
427
+ return true;
428
+ }
429
+
141
430
  if (currentPath === '/' || currentPath === '/chat' || currentPath.startsWith('/chat/') || currentPath === '/index.html') {
142
- res.writeHead(200, {
431
+ const bundle = getCachedWebChatBundle(port);
432
+ sendCompressedResponse(req, res, 200, {
143
433
  'Content-Type': 'text/html; charset=utf-8',
144
- 'Cache-Control': 'no-cache, no-store, must-revalidate'
145
- });
146
- res.end(getWebChatHtml(port));
434
+ 'Cache-Control': 'no-cache, must-revalidate'
435
+ }, bundle.raw, bundle.etag, bundle);
147
436
  return true;
148
437
  }
149
438