kiku-agent 1.2.0 → 1.3.1

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 (131) hide show
  1. package/README.md +459 -407
  2. package/package.json +32 -3
  3. package/server/agent/agentCore.js +635 -267
  4. package/server/agent/appPaths.js +2 -0
  5. package/server/agent/catalogEntityResolver.js +637 -0
  6. package/server/agent/citations.js +0 -21
  7. package/server/agent/clarificationAnalytics.js +1 -1
  8. package/server/agent/clarifyingQuestions.js +286 -216
  9. package/server/agent/cli.js +532 -73
  10. package/server/agent/commandFeatures.js +123 -24
  11. package/server/agent/commandRegistry.js +128 -62
  12. package/server/agent/commands.js +1410 -95
  13. package/server/agent/config.js +253 -163
  14. package/server/agent/contextCompactor.js +239 -25
  15. package/server/agent/dynamicCatalog.js +192 -0
  16. package/server/agent/envBootstrap.js +46 -0
  17. package/server/agent/envManager.js +317 -0
  18. package/server/agent/eventBus.js +3 -3
  19. package/server/agent/feedbackLearning.js +0 -8
  20. package/server/agent/gpuMetrics.js +188 -55
  21. package/server/agent/ingestionWorker.js +339 -0
  22. package/server/agent/ink/App.jsx +348 -0
  23. package/server/agent/ink/Panel.jsx +54 -0
  24. package/server/agent/ink/PromptInput.jsx +321 -0
  25. package/server/agent/ink/TaskPlanPanel.jsx +88 -0
  26. package/server/agent/ink/ThinkingPanel.jsx +68 -0
  27. package/server/agent/ink/ToolResult.jsx +133 -0
  28. package/server/agent/ink/main.jsx +195 -0
  29. package/server/agent/ink/useEventBus.js +218 -0
  30. package/server/agent/lanDiscovery.js +369 -0
  31. package/server/agent/llmClient.js +111 -39
  32. package/server/agent/lmStudioWakeup.js +144 -0
  33. package/server/agent/mcpClient.js +2 -2
  34. package/server/agent/memory/contextAllocator.js +214 -0
  35. package/server/agent/memory/hierarchicalStore.js +523 -0
  36. package/server/agent/memory/reflectionWorker.js +110 -0
  37. package/server/agent/memory.js +635 -494
  38. package/server/agent/modelConfig.js +487 -0
  39. package/server/agent/newsIntelligence.js +219 -0
  40. package/server/agent/newsWatch.js +85 -0
  41. package/server/agent/prompts.js +146 -0
  42. package/server/agent/publicTools.js +7 -0
  43. package/server/agent/queryDetails.js +62 -1
  44. package/server/agent/research.js +314 -35
  45. package/server/agent/routingInterceptor.js +77 -0
  46. package/server/agent/rumoredProjects.js +15 -0
  47. package/server/agent/sanitizer.js +117 -17
  48. package/server/agent/subagent.js +52 -9
  49. package/server/agent/suggestionTui.js +33 -13
  50. package/server/agent/suggestions.js +29 -13
  51. package/server/agent/telemetry/hardwareTiers.js +332 -0
  52. package/server/agent/telemetry/runtimeWatchdog.js +66 -0
  53. package/server/agent/telemetry/techPowerUpClient.js +682 -0
  54. package/server/agent/telemetry/techpowerup-mcp.json +22 -0
  55. package/server/agent/telemetry.js +0 -11
  56. package/server/agent/terminalImage.js +0 -1
  57. package/server/agent/textToolProtocol.js +190 -0
  58. package/server/agent/tokenTracker.js +220 -0
  59. package/server/agent/toolRouter.js +362 -216
  60. package/server/agent/tools.js +1155 -194
  61. package/server/agent/ui.js +144 -41
  62. package/server/agent/vramService.js +572 -0
  63. package/server/agent/webSearch.js +2279 -273
  64. package/server/agent/websearch/adapters/duckduckgo.js +97 -0
  65. package/server/agent/websearch/adapters/jina.js +77 -0
  66. package/server/agent/websearch/adapters/searxng.js +57 -0
  67. package/server/agent/websearch/core/canonical.js +89 -0
  68. package/server/agent/websearch/core/fusion.js +81 -0
  69. package/server/agent/websearch/core/types.js +29 -0
  70. package/server/agent/websearch/extract.js +605 -0
  71. package/server/agent/websearch/index.js +43 -0
  72. package/server/agent/websearch/orchestrator.js +266 -0
  73. package/server/agent/websearch/resilience/breaker.js +97 -0
  74. package/server/agent/websearch/resilience/cache.js +59 -0
  75. package/server/agent/websearch/resilience/limiter.js +93 -0
  76. package/server/agent/websearch/security/filter.js +36 -0
  77. package/server/agent/websearch/security/ssrf.js +289 -0
  78. package/server/agent/websearch/telemetry.js +71 -0
  79. package/server/agent/youtubeAuth.js +82 -73
  80. package/server/agent/youtubeTranscript.js +209 -0
  81. package/server/apiRouter.js +390 -190
  82. package/server/mcp/data/brands.js +5 -5
  83. package/server/mcp/data/discography.js +56 -45
  84. package/server/mcp/data/gallery.js +9 -0
  85. package/server/mcp/data/index.js +1 -0
  86. package/server/mcp/data/industryIndex.js +251 -0
  87. package/server/mcp/data/profile.js +5 -5
  88. package/server/mcp/data/timeline.js +2 -2
  89. package/server/mcp/data/works.js +2 -2
  90. package/server/mcp/handler.js +0 -2
  91. package/server/mcp/prompts.js +42 -2
  92. package/server/mcp/resources.js +5 -1
  93. package/server/mcp/security.js +25 -14
  94. package/server/mcp/server.js +92 -3
  95. package/server/mcp/tools.js +20 -11
  96. package/server/routes/agent.js +55 -0
  97. package/server/web-chat/core/connection.js +218 -194
  98. package/server/web-chat/core/conversationContext.js +1 -1
  99. package/server/web-chat/core/directCatalogEngine.js +190 -184
  100. package/server/web-chat/core/followUpEngine.js +88 -11
  101. package/server/web-chat/core/security.js +7 -4
  102. package/server/web-chat/router.js +54 -17
  103. package/server/web-chat/routes/bilibiliRoute.js +34 -0
  104. package/server/web-chat/routes/chatRoute.js +488 -397
  105. package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
  106. package/server/web-chat/routes/exportRoute.js +2 -1
  107. package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
  108. package/server/web-chat/routes/newsRoute.js +36 -6
  109. package/server/web-chat/routes/providerRoute.js +87 -0
  110. package/server/web-chat/routes/staticRoute.js +61 -13
  111. package/server/web-chat/routes/statusRoute.js +23 -2
  112. package/server/web-chat/routes/vramRoute.js +22 -0
  113. package/server/web-chat/server.js +135 -106
  114. package/server/web-chat/ui/htmlBuilder.js +8 -8
  115. package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
  116. package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
  117. package/server/web-chat/ui/scripts/client.js +10526 -8397
  118. package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
  119. package/server/web-chat/ui/styles/main.css +7471 -4246
  120. package/server/web-chat/ui/templates/body.html +305 -179
  121. package/src/data/brandsData.js +29 -6
  122. package/src/data/discographyData.js +918 -900
  123. package/src/data/galleryData.js +66 -0
  124. package/src/data/industryIndexData.js +292 -0
  125. package/src/data/musicVideoData.js +48 -5
  126. package/src/data/profileData.js +15 -1
  127. package/src/data/timelineData.js +4 -3
  128. package/src/data/worksData.js +536 -535
  129. package/src/lib/vram/estimator.js +464 -0
  130. package/src/lib/vram/modelRegistry.js +387 -0
  131. package/tokens.css +33 -2
@@ -1,6 +1,6 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import crypto from 'node:crypto';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
4
  import { PROVIDER_KEYS_FILE, SETTINGS_FILE } from './appPaths.js';
5
5
 
6
6
  const PROVIDER_MODEL_ALIASES = {
@@ -47,6 +47,14 @@ const PROVIDER_MODEL_ALIASES = {
47
47
  'gemma-4': 'google/gemma-4-e4b',
48
48
  'gemma-4-e4b': 'google/gemma-4-e4b',
49
49
  },
50
+ vllm: {
51
+ 'gemma': 'google/gemma-4-e4b',
52
+ 'gemma4': 'google/gemma-4-e4b',
53
+ 'gemma-4': 'google/gemma-4-e4b',
54
+ 'gemma-4-e4b': 'google/gemma-4-e4b',
55
+ 'qwen': 'Qwen/Qwen2.5-7B-Instruct',
56
+ 'llama': 'meta-llama/Llama-3.1-8B-Instruct',
57
+ },
50
58
  openai: {
51
59
  'o1-mini': 'gpt-5.6-luna',
52
60
  'o3-mini': 'gpt-5.6-luna',
@@ -146,6 +154,11 @@ export function normalizeProviderModel(providerId, modelId) {
146
154
  return aliasMatch || normalizedModel;
147
155
  }
148
156
 
157
+ export const DEFAULT_LOCAL_ENDPOINTS = Object.freeze({
158
+ lmstudio: 'http://127.0.0.1:1234/v1',
159
+ vllm: 'http://127.0.0.1:8000/v1',
160
+ });
161
+
149
162
  export const CLOUD_PROVIDERS = {
150
163
  lmstudio: {
151
164
  id: 'lmstudio',
@@ -166,6 +179,25 @@ export const CLOUD_PROVIDERS = {
166
179
  { id: 'google/gemma-4-e4b-Q8_0', name: 'Gemma 4 e4b (Q8_0 Quant)', tier: 'free', cost: '$0.00 (Local)', context: '8k', hasTools: true, description: '8-bit full quantization (Q8_0) for maximum accuracy and reasoning precision (~4.5GB RAM).' },
167
180
  ],
168
181
  },
182
+ vllm: {
183
+ id: 'vllm',
184
+ name: 'vLLM (Local / WSL)',
185
+ baseUrl: process.env.VLLM_BASE_URL || 'http://127.0.0.1:8000/v1',
186
+ apiKey: process.env.VLLM_API_KEY || 'token-kiku',
187
+ defaultModel: process.env.VLLM_MODEL || 'google/gemma-4-e4b',
188
+ isLocal: true,
189
+ envKey: 'VLLM_BASE_URL',
190
+ docUrl: 'https://docs.vllm.ai',
191
+ apiRefUrl: 'https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html',
192
+ pricingSummary: '100% Free / Local (High-throughput PagedAttention server via WSL/Linux)',
193
+ features: ['local', 'high-throughput', 'paged-attention', 'wsl-required', 'openai-compatible'],
194
+ platformNotice: 'You cannot install vLLM natively on Windows. Please install and run vLLM via WSL (Windows Subsystem for Linux, e.g. wsl -d kali-linux or Ubuntu) using "vllm serve <model> --port 8000".',
195
+ recommendedModels: [
196
+ { id: 'google/gemma-4-e4b', name: 'Gemma 4 e4b (Default)', tier: 'free', cost: '$0.00 (Local)', context: '8k', hasTools: true, description: 'Recommended default model served via vllm serve on port 8000.' },
197
+ { id: 'Qwen/Qwen2.5-7B-Instruct', name: 'Qwen 2.5 7B Instruct', tier: 'free', cost: '$0.00 (Local)', context: '32k', hasTools: true, description: 'High capability open model with function calling on vLLM.' },
198
+ { id: 'meta-llama/Llama-3.1-8B-Instruct', name: 'Llama 3.1 8B Instruct', tier: 'free', cost: '$0.00 (Local)', context: '128k', hasTools: true, description: 'Fast general-purpose open model hosted on vLLM.' },
199
+ ],
200
+ },
169
201
  openai: {
170
202
  id: 'openai',
171
203
  name: 'OpenAI Cloud',
@@ -274,7 +306,20 @@ export const CLOUD_PROVIDERS = {
274
306
  },
275
307
  };
276
308
 
277
- export function loadPersistedSettings() {
309
+ let cachedSettings = null;
310
+ let lastSettingsLoadTime = 0;
311
+ const SETTINGS_CACHE_TTL_MS = 2000;
312
+
313
+ export function invalidateSettingsCache() {
314
+ cachedSettings = null;
315
+ lastSettingsLoadTime = 0;
316
+ }
317
+
318
+ export function loadPersistedSettings(force = false) {
319
+ const now = Date.now();
320
+ if (!force && cachedSettings && (now - lastSettingsLoadTime < SETTINGS_CACHE_TTL_MS)) {
321
+ return cachedSettings;
322
+ }
278
323
  try {
279
324
  if (fs.existsSync(SETTINGS_FILE)) {
280
325
  const data = fs.readFileSync(SETTINGS_FILE, 'utf-8');
@@ -288,13 +333,75 @@ export function loadPersistedSettings() {
288
333
  }
289
334
  }
290
335
  }
336
+ if (parsed.providerBaseUrls && typeof parsed.providerBaseUrls === 'object') {
337
+ for (const [pId, url] of Object.entries(parsed.providerBaseUrls)) {
338
+ const providerDef = CLOUD_PROVIDERS[pId.toLowerCase()];
339
+ if (providerDef && typeof url === 'string' && url.trim()) {
340
+ providerDef.baseUrl = url.trim().replace(/\/+$/, '');
341
+ }
342
+ }
343
+ }
344
+ cachedSettings = parsed;
345
+ lastSettingsLoadTime = now;
291
346
  return parsed;
292
347
  }
293
348
  }
294
349
  } catch {}
350
+ cachedSettings = {};
351
+ lastSettingsLoadTime = now;
295
352
  return {};
296
353
  }
297
354
 
355
+ export function getProviderBaseUrl(providerId) {
356
+ const pId = String(providerId || '').toLowerCase().trim();
357
+ const settings = loadPersistedSettings();
358
+ if (settings.providerBaseUrls && typeof settings.providerBaseUrls[pId] === 'string' && settings.providerBaseUrls[pId].trim()) {
359
+ return settings.providerBaseUrls[pId].trim().replace(/\/+$/, '');
360
+ }
361
+ const providerDef = CLOUD_PROVIDERS[pId];
362
+ if (providerDef) {
363
+ if (providerDef.envKey && process.env[providerDef.envKey]) {
364
+ return process.env[providerDef.envKey].trim().replace(/\/+$/, '');
365
+ }
366
+ if (pId === 'lmstudio' && process.env.OPENAI_BASE_URL) {
367
+ return process.env.OPENAI_BASE_URL.trim().replace(/\/+$/, '');
368
+ }
369
+ if (DEFAULT_LOCAL_ENDPOINTS[pId]) {
370
+ return DEFAULT_LOCAL_ENDPOINTS[pId];
371
+ }
372
+ if (providerDef.baseUrl) {
373
+ return providerDef.baseUrl.trim().replace(/\/+$/, '');
374
+ }
375
+ }
376
+ return DEFAULT_LOCAL_ENDPOINTS.lmstudio;
377
+ }
378
+
379
+ export function savePersistedProviderBaseUrl(providerId, baseUrl) {
380
+ const pId = String(providerId || '').toLowerCase().trim();
381
+ const cleanUrl = String(baseUrl || '').trim().replace(/\/+$/, '');
382
+ if (!CLOUD_PROVIDERS[pId] || !cleanUrl) return cleanUrl;
383
+
384
+ const settings = loadPersistedSettings();
385
+ if (!settings.providerBaseUrls) settings.providerBaseUrls = {};
386
+ settings.providerBaseUrls[pId] = cleanUrl;
387
+ if (CLOUD_PROVIDERS[pId]) {
388
+ CLOUD_PROVIDERS[pId].baseUrl = cleanUrl;
389
+ }
390
+ if (config.activeProvider === pId && config.lmStudio) {
391
+ config.lmStudio.baseUrl = cleanUrl;
392
+ }
393
+
394
+ try {
395
+ const dir = path.dirname(SETTINGS_FILE);
396
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
397
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), { encoding: 'utf-8', mode: 0o600 });
398
+ invalidateSettingsCache();
399
+ } catch (err) {
400
+ console.error(`Failed to save baseUrl to ${SETTINGS_FILE}:`, err.message);
401
+ }
402
+ return cleanUrl;
403
+ }
404
+
298
405
  export function getPersistedActiveProvider() {
299
406
  const settings = loadPersistedSettings();
300
407
  if (settings.activeProvider && CLOUD_PROVIDERS[settings.activeProvider.toLowerCase()]) {
@@ -303,13 +410,14 @@ export function getPersistedActiveProvider() {
303
410
  return null;
304
411
  }
305
412
 
306
- export function savePersistedActiveProvider(providerId, model = null) {
413
+ export function savePersistedActiveProvider(providerId, model = null, baseUrl = null) {
307
414
  const pId = String(providerId || '').toLowerCase().trim();
308
415
  if (!CLOUD_PROVIDERS[pId]) return;
309
416
 
310
417
  const settings = loadPersistedSettings();
311
418
  settings.activeProvider = pId;
312
419
  if (!settings.providerModels) settings.providerModels = {};
420
+ if (!settings.providerBaseUrls) settings.providerBaseUrls = {};
313
421
 
314
422
  if (model) {
315
423
  const cleanModel = String(model).trim();
@@ -318,15 +426,80 @@ export function savePersistedActiveProvider(providerId, model = null) {
318
426
  if (CLOUD_PROVIDERS[pId]) CLOUD_PROVIDERS[pId].defaultModel = cleanModel;
319
427
  }
320
428
 
429
+ if (baseUrl) {
430
+ const cleanUrl = String(baseUrl).trim().replace(/\/+$/, '');
431
+ settings.providerBaseUrls[pId] = cleanUrl;
432
+ if (CLOUD_PROVIDERS[pId]) CLOUD_PROVIDERS[pId].baseUrl = cleanUrl;
433
+ }
434
+
321
435
  try {
322
436
  const dir = path.dirname(SETTINGS_FILE);
323
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
324
- fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), { encoding: 'utf-8', mode: 0o600 });
437
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
438
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), { encoding: 'utf-8', mode: 0o600 });
439
+ invalidateSettingsCache();
325
440
  } catch (err) {
326
441
  console.error(`Failed to save settings to ${SETTINGS_FILE}:`, err.message);
327
442
  }
328
443
  }
329
444
 
445
+ export function setActiveProvider(providerId, { model = null, baseUrl = null } = {}) {
446
+ const pId = String(providerId || '').toLowerCase().trim();
447
+ if (!CLOUD_PROVIDERS[pId]) {
448
+ throw new Error(`Unknown provider "${providerId}".`);
449
+ }
450
+ savePersistedActiveProvider(pId, model, baseUrl);
451
+ config.activeProvider = pId;
452
+ const def = CLOUD_PROVIDERS[pId];
453
+ if (config.lmStudio) {
454
+ config.lmStudio.baseUrl = def.baseUrl;
455
+ config.lmStudio.apiKey = def.apiKey;
456
+ config.lmStudio.model = def.defaultModel;
457
+ config.lmStudio.defaultModel = def.defaultModel;
458
+ }
459
+ return {
460
+ provider: pId,
461
+ name: def.name,
462
+ baseUrl: def.baseUrl,
463
+ model: def.defaultModel,
464
+ isLocal: Boolean(def.isLocal),
465
+ platformNotice: def.platformNotice || null,
466
+ };
467
+ }
468
+
469
+ export function getUnifiedProviderCatalog() {
470
+ const active = config.activeProvider || resolveDefaultProvider();
471
+ return {
472
+ activeProvider: active,
473
+ activeBaseUrl: getProviderBaseUrl(active),
474
+ activeModel: CLOUD_PROVIDERS[active]?.defaultModel || 'google/gemma-4-e4b',
475
+ localEndpoints: {
476
+ lmstudio: {
477
+ id: 'lmstudio',
478
+ name: CLOUD_PROVIDERS.lmstudio.name,
479
+ baseUrl: getProviderBaseUrl('lmstudio'),
480
+ defaultModel: CLOUD_PROVIDERS.lmstudio.defaultModel,
481
+ isActive: active === 'lmstudio',
482
+ },
483
+ vllm: {
484
+ id: 'vllm',
485
+ name: CLOUD_PROVIDERS.vllm.name,
486
+ baseUrl: getProviderBaseUrl('vllm'),
487
+ defaultModel: CLOUD_PROVIDERS.vllm.defaultModel,
488
+ platformNotice: CLOUD_PROVIDERS.vllm.platformNotice || null,
489
+ isActive: active === 'vllm',
490
+ }
491
+ },
492
+ providers: Object.values(CLOUD_PROVIDERS).map(p => ({
493
+ id: p.id,
494
+ name: p.name,
495
+ baseUrl: getProviderBaseUrl(p.id),
496
+ defaultModel: p.defaultModel,
497
+ isLocal: Boolean(p.isLocal),
498
+ isActive: p.id === active,
499
+ }))
500
+ };
501
+ }
502
+
330
503
  export function savePersistedProviderModel(providerId, model) {
331
504
  const pId = String(providerId || '').toLowerCase().trim();
332
505
  const cleanModel = String(model || '').trim();
@@ -342,8 +515,9 @@ export function savePersistedProviderModel(providerId, model) {
342
515
 
343
516
  try {
344
517
  const dir = path.dirname(SETTINGS_FILE);
345
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
346
- fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), { encoding: 'utf-8', mode: 0o600 });
518
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
519
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), { encoding: 'utf-8', mode: 0o600 });
520
+ invalidateSettingsCache();
347
521
  } catch (err) {
348
522
  console.error(`Failed to save model to ${SETTINGS_FILE}:`, err.message);
349
523
  }
@@ -385,66 +559,66 @@ export function getRecommendedModels(providerId) {
385
559
  return provider ? (provider.recommendedModels || []) : [];
386
560
  }
387
561
 
388
- let persistedProviderKeys = {};
389
- const providerKeyEncryptionSecret = process.env.KIKU_PROVIDER_KEY_ENCRYPTION_KEY ||
390
- (process.execArgv.includes('--test') ? 'kiku-agent-test-process-only-key' : '');
391
-
392
- export function isProviderKeyPersistenceEnabled() {
393
- return Boolean(providerKeyEncryptionSecret);
394
- }
395
-
396
- function encryptProviderKeys(keys) {
397
- if (!providerKeyEncryptionSecret) return null;
398
- const key = crypto.createHash('sha256').update(providerKeyEncryptionSecret).digest();
399
- const iv = crypto.randomBytes(12);
400
- const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
401
- const ciphertext = Buffer.concat([cipher.update(JSON.stringify(keys), 'utf8'), cipher.final()]);
402
- return {
403
- version: 1,
404
- algorithm: 'aes-256-gcm',
405
- iv: iv.toString('base64'),
406
- tag: cipher.getAuthTag().toString('base64'),
407
- ciphertext: ciphertext.toString('base64'),
408
- };
409
- }
410
-
411
- function decryptProviderKeys(envelope) {
412
- if (!providerKeyEncryptionSecret || envelope?.version !== 1 || envelope?.algorithm !== 'aes-256-gcm') return {};
413
- const key = crypto.createHash('sha256').update(providerKeyEncryptionSecret).digest();
414
- const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(envelope.iv, 'base64'));
415
- decipher.setAuthTag(Buffer.from(envelope.tag, 'base64'));
416
- const plaintext = Buffer.concat([
417
- decipher.update(Buffer.from(envelope.ciphertext, 'base64')),
418
- decipher.final(),
419
- ]).toString('utf8');
420
- const parsed = JSON.parse(plaintext);
421
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
422
- }
423
-
424
- export function loadPersistedProviderKeys() {
425
- try {
426
- if (fs.existsSync(PROVIDER_KEYS_FILE)) {
427
- const data = fs.readFileSync(PROVIDER_KEYS_FILE, 'utf-8');
428
- const parsed = JSON.parse(data);
429
- const keys = parsed?.ciphertext
430
- ? decryptProviderKeys(parsed)
431
- : (process.env.KIKU_ALLOW_PLAINTEXT_PROVIDER_KEYS === 'true' ? parsed : {});
432
- if (keys && typeof keys === 'object') {
433
- persistedProviderKeys = { ...keys };
434
- for (const [pId, key] of Object.entries(keys)) {
435
- const providerDef = CLOUD_PROVIDERS[pId.toLowerCase()];
562
+ let persistedProviderKeys = {};
563
+ const providerKeyEncryptionSecret = process.env.KIKU_PROVIDER_KEY_ENCRYPTION_KEY ||
564
+ (process.execArgv.includes('--test') ? 'kiku-agent-test-process-only-key' : '');
565
+
566
+ export function isProviderKeyPersistenceEnabled() {
567
+ return Boolean(providerKeyEncryptionSecret);
568
+ }
569
+
570
+ function encryptProviderKeys(keys) {
571
+ if (!providerKeyEncryptionSecret) return null;
572
+ const key = crypto.createHash('sha256').update(providerKeyEncryptionSecret).digest();
573
+ const iv = crypto.randomBytes(12);
574
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
575
+ const ciphertext = Buffer.concat([cipher.update(JSON.stringify(keys), 'utf8'), cipher.final()]);
576
+ return {
577
+ version: 1,
578
+ algorithm: 'aes-256-gcm',
579
+ iv: iv.toString('base64'),
580
+ tag: cipher.getAuthTag().toString('base64'),
581
+ ciphertext: ciphertext.toString('base64'),
582
+ };
583
+ }
584
+
585
+ function decryptProviderKeys(envelope) {
586
+ if (!providerKeyEncryptionSecret || envelope?.version !== 1 || envelope?.algorithm !== 'aes-256-gcm') return {};
587
+ const key = crypto.createHash('sha256').update(providerKeyEncryptionSecret).digest();
588
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(envelope.iv, 'base64'));
589
+ decipher.setAuthTag(Buffer.from(envelope.tag, 'base64'));
590
+ const plaintext = Buffer.concat([
591
+ decipher.update(Buffer.from(envelope.ciphertext, 'base64')),
592
+ decipher.final(),
593
+ ]).toString('utf8');
594
+ const parsed = JSON.parse(plaintext);
595
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
596
+ }
597
+
598
+ export function loadPersistedProviderKeys() {
599
+ try {
600
+ if (fs.existsSync(PROVIDER_KEYS_FILE)) {
601
+ const data = fs.readFileSync(PROVIDER_KEYS_FILE, 'utf-8');
602
+ const parsed = JSON.parse(data);
603
+ const keys = parsed?.ciphertext
604
+ ? decryptProviderKeys(parsed)
605
+ : (process.env.KIKU_ALLOW_PLAINTEXT_PROVIDER_KEYS === 'true' ? parsed : {});
606
+ if (keys && typeof keys === 'object') {
607
+ persistedProviderKeys = { ...keys };
608
+ for (const [pId, key] of Object.entries(keys)) {
609
+ const providerDef = CLOUD_PROVIDERS[pId.toLowerCase()];
436
610
  if (providerDef && !providerDef.isLocal && !providerDef.apiKey && typeof key === 'string' && key.trim()) {
437
611
  providerDef.apiKey = key.trim();
438
612
  }
439
613
  }
440
- return { ...persistedProviderKeys };
614
+ return { ...persistedProviderKeys };
441
615
  }
442
616
  }
443
617
  } catch {}
444
618
  return {};
445
619
  }
446
620
 
447
- export function savePersistedProviderKey(providerId, apiKey) {
621
+ export function savePersistedProviderKey(providerId, apiKey) {
448
622
  const pId = String(providerId || '').toLowerCase().trim();
449
623
  const providerDef = CLOUD_PROVIDERS[pId];
450
624
  if (!providerDef) {
@@ -453,7 +627,7 @@ export function savePersistedProviderKey(providerId, apiKey) {
453
627
  const cleanKey = String(apiKey || '').trim();
454
628
  providerDef.apiKey = cleanKey;
455
629
 
456
- const existing = { ...persistedProviderKeys };
630
+ const existing = { ...persistedProviderKeys };
457
631
 
458
632
  if (cleanKey) {
459
633
  existing[pId] = cleanKey;
@@ -461,13 +635,13 @@ export function savePersistedProviderKey(providerId, apiKey) {
461
635
  delete existing[pId];
462
636
  }
463
637
 
464
- persistedProviderKeys = existing;
465
- try {
466
- const envelope = encryptProviderKeys(existing);
467
- if (!envelope) return { provider: pId, hasKey: Boolean(cleanKey), persisted: false };
468
- const dir = path.dirname(PROVIDER_KEYS_FILE);
469
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
470
- fs.writeFileSync(PROVIDER_KEYS_FILE, JSON.stringify(envelope, null, 2), { encoding: 'utf-8', mode: 0o600 });
638
+ persistedProviderKeys = existing;
639
+ try {
640
+ const envelope = encryptProviderKeys(existing);
641
+ if (!envelope) return { provider: pId, hasKey: Boolean(cleanKey), persisted: false };
642
+ const dir = path.dirname(PROVIDER_KEYS_FILE);
643
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
644
+ fs.writeFileSync(PROVIDER_KEYS_FILE, JSON.stringify(envelope, null, 2), { encoding: 'utf-8', mode: 0o600 });
471
645
  } catch (err) {
472
646
  console.error(`Failed to save provider key to ${PROVIDER_KEYS_FILE}:`, err.message);
473
647
  }
@@ -475,9 +649,9 @@ export function savePersistedProviderKey(providerId, apiKey) {
475
649
  return { provider: pId, hasKey: Boolean(cleanKey) };
476
650
  }
477
651
 
478
- export function getPersistedProviderKeys() {
479
- return { ...persistedProviderKeys };
480
- }
652
+ export function getPersistedProviderKeys() {
653
+ return { ...persistedProviderKeys };
654
+ }
481
655
 
482
656
  loadPersistedSettings();
483
657
  loadPersistedProviderKeys();
@@ -503,98 +677,14 @@ export const config = {
503
677
  maxIterations: 6,
504
678
  slidingWindowSize: 12,
505
679
  language: 'auto',
506
- systemPersona: `You are "Kiku AI" (จวีจิ้งอี AI ผู้ช่วยอัจฉริยะ / 鞠婧祎智能AI助手), an expert AI specialist and ultimate comprehensive encyclopedia on the Chinese singer, actress, and artist Ju Jingyi (鞠婧祎 - จวีจิ้งอี).
507
- You have access to a verified API database of her dramas, songs, character roles, brand endorsements, timeline milestones, awards, and music videos, as well as LIVE Internet Search (web_search, fetch_webpage, web_extract, fetch_json).
508
-
509
- === CRITICAL INSTRUCTIONS ===
510
-
511
- 1. THOROUGH, COMPREHENSIVE & RICH FAN RESPONSES:
512
- - Provide rich, in-depth, and comprehensive responses for fans with full context, storylines, character analysis, trivia, discography details, behind-the-scenes facts, and career history.
513
- - Do NOT artificially truncate, summarize away key details, or cut answers short — provide full, richly structured answers with markdown headers, tables, bullet points, and working media links.
514
- - For simple greetings ("Hi", "Hello", "สวัสดี", "你好"): Reply warmly and politely in 1-2 sentences.
515
-
516
- 2. DYNAMIC LANGUAGE MIRRORING:
517
- - THAI INPUT ➔ Reply in polite Thai (ใช้ ค่ะ/นะคะ เสมอ)
518
- - CHINESE INPUT ➔ Reply in fluent, polite Chinese (简体/繁体中文)
519
- - ENGLISH INPUT ➔ Reply in polished, professional English
520
- - MIXED INPUT ➔ Match dominant language while preserving bilingual titles
521
-
522
- 3. RESPONSE FORMATTING (CRITICAL — NO SYSTEM LEAK):
523
- - NEVER include "Thinking Process", "Tool Calling", "Action:", "Action Input:", or any internal reasoning steps in your response text.
524
- - NEVER include tool call JSON, function names, or parameter schemas in your response.
525
- - NEVER include advertisements, sponsor mentions, or promotional content in your response.
526
- - Emoji are optional and must be semantic: use at most one relevant emoji per response, never decorate every heading or bullet.
527
- - Your response must be CLEAN user-facing content ONLY — formatted with Markdown headers (###), bold (**text**), bullet points (•), and clickable links.
528
- - Think silently using tools, then present ONLY the final answer to the user.
529
-
530
- 4. REAL YOUTUBE & MEDIA LINKS (WHEN DISCUSSING MEDIA):
531
- - Provide REAL, WORKING, CLICKABLE links in Markdown format:
532
- • Direct Watch Link: [Watch on YouTube](https://www.youtube.com/watch?v=VIDEO_ID)
533
- • Search Link: https://www.youtube.com/results?search_query=QUERY
534
- - NEVER use fake placeholder links like "watch?v=xxx".
535
-
536
- 5. SMART TOOL SELECTION (MANDATORY LOCAL DATABASE FIRST):
537
- - ALWAYS prioritize LOCAL in-process verified tools FIRST. They query the rich, local catalog database directly and are the primary source of truth.
538
- - Music videos, stages & audio ➔ search_discography, get_music_videos, get_media_links, play_music
539
- - Songs, OSTs, Singles ➔ search_discography
540
- - Drama works, roles & trailers ➔ search_works, get_work_detail, get_character_roles
541
- - Profile & fandom details ➔ get_profile_and_fandom
542
- - Brand endorsements & ambassadorships ➔ get_brand_endorsements
543
- - Awards & honors ➔ get_awards
544
- - Career milestones & timeline ➔ get_career_timeline
545
- - Local files & stats ➔ read_local_file, list_directory, get_system_stats
546
- - MCP external tools (mcp_*) ➔ Secondary only; use when explicitly requested by user or when specialized external tools are needed.
547
- - Breaking news & external facts (when local tools return insufficient/not_found data) ➔ ALWAYS fall back to web_search or web_extract.
548
-
549
- 6. FACT RETRIEVAL & CHRONOLOGICAL ACCURACY:
550
- - THE CURRENT YEAR IS 2026.
551
- - Always use tools to verify facts before answering. Do NOT hallucinate dates, titles, or links.
552
- - Sort chronologically (newest first). Never repeat the same statement.
553
- - Ju Jingyi's key 2026 projects:
554
- • "Veil of Shadows (月鳞绮纪)" — Role: Lu Wuyi (露芜衣) on Youku
555
- • "The Eternal Fragrance (千香)" — Role: Jiang Lifei (姜黎非) with Song Weilong on Youku
556
- • "Wanhua Sin (万华世界)" — Promo OST: "Set Fire to the Silent World (着火的沉默世界)"
557
- • 2026 Solo Single: "FIND THE WAY" (YouTube: 9AeErG03SZw)
558
- • 2026 Special Live Stage: "Yu Ai / 雨爱" (YouTube: OMLpNyp8T7Q)
559
- - Classic hits: "In Blossom (2024)", "Sword and Fairy 4 (2024)", "Beauty of Resilience (2023)", "Rebirth for You (2021)", "The Blooms at Ruyi Pavilion (2020)", "The Legend of White Snake (2019)", "Legend of Yunxi (2018)".
560
-
561
- 7. COMPREHENSIVE KNOWLEDGE — LEGAL & CAREER DETAILS:
562
- When discussing Ju Jingyi's career or controversies, be accurate and specific:
563
- - CAREER PATH: SNH48 2nd gen (2013) → "4000-year idol" nickname (2014) → General Election winner 2 consecutive years (2016-2017) → Star Hall of Fame → Solo career → Acting breakthrough with "Legend of Yunxi" (2018).
564
- - CONTRACT DISPUTE (Star48/Siba Media): The supplementary agreement dispute. JJY's side claims main contract ended June 2024, alleges forged signatures. Star48 claims contract extends to 2033. Tax evasion tip-off report was filed by Star48's side; JJY denies all allegations and confirms tax compliance.
565
- - PORTRAIT/COPYRIGHT INFRINGEMENT CASES: Ju Jingyi has filed lawsuits against beauty clinics and brands for unauthorized use of her image/likeness (Right of Portrait / 肖像权纠纷). This is a separate legal matter from the Star48 dispute.
566
- - When asked about legal issues, use web_search or web_extract to get the latest verified information.
567
-
568
- 8. RICH, IN-DEPTH & COMPREHENSIVE FAN ENCYCLOPEDIA QUALITY:
569
- - Give complete, detailed explanations covering background stories, character development, musical composition, lyrics significance, and milestone achievements.
570
- - Use structured Markdown formatting with tables, bullet lists, and bold headings to make deep information easy to digest for fans.
571
- - Separate VERIFIED FACTS from INTERPRETATION. Plot, cast, role, platform, award, and rating claims must come from tool output. Label acting observations as analysis unless a cited review supports them.
572
- - Do not turn chronology into a value judgment. A newer role is not automatically deeper, more realistic, or better acted than an older role.
573
- - Avoid unsupported superlatives and career claims such as "top-tier", "unrivalled", "completely transcended idol status", or "won many awards".
574
- - A catalog score without a named source and scoring scale is editorial metadata, not evidence of quality; omit it from analytical conclusions.
575
- - For In Blossom (花间令), describe the premise precisely: Ju Jingyi portrays Yang Caiwei's identity living in Shangguan Zhi's appearance after the face-swap premise. Do not simplify this into two unrelated roles performed throughout the series.
576
- - For The Legend of White Snake (新白娘子传奇), Bai Suzhen is the white-snake spirit who takes human form and Xu Xian is the human man. Never reverse their species or genders.
577
- - For follow-ups beginning with "ดำเนินการต่อ" / "continue", answer the newly requested angle and do not reproduce the previous answer.
578
-
579
- 9. PROACTIVE MUSIC INVITATION (RELEVANCE GUARD):
580
- - ONLY invite the user to listen to music or open the Mini Player when the conversation is SPECIFICALLY and DIRECTLY about music, songs, singing, or OST playback.
581
- - Example: "เพลง FIND THE WAY จังหวะทรงพลังมากเลยค่ะ! อยากให้ Kiku เปิดให้ฟังมั้ยคะ?"
582
- - NEVER offer to play music or ask to open the Mini Player when answering questions about drama plots, rumors, casting news, actors, awards, or general topics.
583
- - If user agrees → call play_music. If user says "Stop" → call stop_music.
584
-
585
- 10. ENTITY DISAMBIGUATION & RUMOR VERIFICATION (CRITICAL GUARDRAIL):
586
- - STRICT DISAMBIGUATION (NO PHONETIC CONFUSION):
587
- • NEVER confuse phonetically similar pinyin or names.
588
- • Specifically: 《七星彩》 (Qī Xīng Cǎi / Qi Xiang Cai / ชีซิงไฉ่) is an upcoming/rumored S+ costume romance drama from Youku x Stellar Media (恒星引力), adapted from Mingyue Dang's (明月珰) novel starring characters Ji Cheng (纪澄) and Shen Che (沈彻).
589
- • 《七星彩》 is COMPLETELY UNRELATED to 《千香》 (Qiān Xiāng / The Eternal Fragrance / พันกลิ่นบุปผา).
590
- • NEVER claim that an unconfirmed title or rumor is "associated with" or "is found to be" an existing OST or song in Ju Jingyi's catalog just because syllables sound alike.
591
- - HANDLING UNCONFIRMED DRAMAS & MELON ACCOUNTS (吃瓜 / 选角传闻):
592
- • When asked why netizens are interested in an unconfirmed project like 《七星彩》, explain the actual facts:
593
- 1) Production & Origin: Big S+ budget by Youku and Stellar Media (the acclaimed studio behind "Love Between Fairy and Devil").
594
- 2) The Buzz / Why Netizens Care: High-profile Casting Rumors (选角瓜) circulating on Weibo, Douban, and Xiaohongshu discussing multiple top visual actresses (often floating Ju Jingyi, Lu Yuxiao, Song Zu'er, etc.) for the clever and enchanting female lead Ji Cheng.
595
- 3) Status: Still in preparation / unconfirmed by official studio or platform announcements.
596
- 4) Clear distinction: Remind the user that this is an unconfirmed industry project and distinct from her confirmed works like "The Eternal Fragrance (千香)" and "Veil of Shadows (月鳞绮纪)".
597
- - NEVER FABRICATE FICTIONAL CONNECTIONS:
598
- • If an entity is not found in the verified catalog, use web_search to find true facts or tell the user clearly that it is not in the verified official works. NEVER invent a relationship between two unrelated titles.`,
680
+
681
+ autoResearch: ['on', 'off', 'auto'].includes(String(process.env.KIKU_AUTO_RESEARCH || '').toLowerCase())
682
+ ? String(process.env.KIKU_AUTO_RESEARCH).toLowerCase()
683
+ : 'auto',
684
+ systemPersona: `You are KIKU AGENT (菊), the authoritative AI encyclopedia agent and concierge for international artist Ju Jingyi (鞠婧祎).
685
+ Speak with elegance, utmost respect, professional warmth, and meticulous factual rigor. You deliver concise, polite answers for simple facts, and structured, in-depth breakdowns for rich cultural and artistic inquiries.
686
+
687
+ MULTI-ANGLE RETRIEVAL & SMART MULTI-TOOL PARALLELISM:
688
+ Always retrieve verified facts through parallel multi-angle tool calls when addressing complex or cross-cutting queries.`,
599
689
  }
600
690
  };