kiku-agent 1.2.0 → 1.3.0

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 (100) hide show
  1. package/README.md +459 -407
  2. package/package.json +27 -3
  3. package/server/agent/agentCore.js +635 -267
  4. package/server/agent/appPaths.js +2 -0
  5. package/server/agent/catalogEntityResolver.js +629 -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 +528 -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/lanDiscovery.js +369 -0
  23. package/server/agent/llmClient.js +111 -39
  24. package/server/agent/lmStudioWakeup.js +144 -0
  25. package/server/agent/mcpClient.js +2 -2
  26. package/server/agent/memory.js +635 -494
  27. package/server/agent/modelConfig.js +487 -0
  28. package/server/agent/newsIntelligence.js +219 -0
  29. package/server/agent/newsWatch.js +85 -0
  30. package/server/agent/prompts.js +146 -0
  31. package/server/agent/publicTools.js +7 -0
  32. package/server/agent/queryDetails.js +62 -1
  33. package/server/agent/research.js +314 -35
  34. package/server/agent/routingInterceptor.js +77 -0
  35. package/server/agent/rumoredProjects.js +15 -0
  36. package/server/agent/sanitizer.js +117 -17
  37. package/server/agent/subagent.js +52 -9
  38. package/server/agent/suggestionTui.js +33 -13
  39. package/server/agent/suggestions.js +29 -13
  40. package/server/agent/telemetry.js +0 -11
  41. package/server/agent/terminalImage.js +0 -1
  42. package/server/agent/textToolProtocol.js +190 -0
  43. package/server/agent/tokenTracker.js +220 -0
  44. package/server/agent/toolRouter.js +362 -216
  45. package/server/agent/tools.js +1155 -194
  46. package/server/agent/ui.js +144 -41
  47. package/server/agent/vramService.js +572 -0
  48. package/server/agent/webSearch.js +2279 -273
  49. package/server/agent/youtubeAuth.js +82 -73
  50. package/server/agent/youtubeTranscript.js +209 -0
  51. package/server/apiRouter.js +383 -179
  52. package/server/mcp/data/brands.js +5 -5
  53. package/server/mcp/data/discography.js +56 -45
  54. package/server/mcp/data/gallery.js +9 -0
  55. package/server/mcp/data/index.js +1 -0
  56. package/server/mcp/data/industryIndex.js +251 -0
  57. package/server/mcp/data/profile.js +5 -5
  58. package/server/mcp/data/timeline.js +2 -2
  59. package/server/mcp/data/works.js +2 -2
  60. package/server/mcp/handler.js +0 -2
  61. package/server/mcp/prompts.js +42 -2
  62. package/server/mcp/resources.js +5 -1
  63. package/server/mcp/security.js +25 -14
  64. package/server/mcp/server.js +92 -3
  65. package/server/mcp/tools.js +20 -11
  66. package/server/web-chat/core/connection.js +218 -194
  67. package/server/web-chat/core/conversationContext.js +1 -1
  68. package/server/web-chat/core/directCatalogEngine.js +190 -184
  69. package/server/web-chat/core/followUpEngine.js +88 -11
  70. package/server/web-chat/core/security.js +7 -4
  71. package/server/web-chat/router.js +54 -17
  72. package/server/web-chat/routes/bilibiliRoute.js +34 -0
  73. package/server/web-chat/routes/chatRoute.js +488 -397
  74. package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
  75. package/server/web-chat/routes/exportRoute.js +2 -1
  76. package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
  77. package/server/web-chat/routes/newsRoute.js +36 -6
  78. package/server/web-chat/routes/providerRoute.js +87 -0
  79. package/server/web-chat/routes/staticRoute.js +61 -13
  80. package/server/web-chat/routes/statusRoute.js +23 -2
  81. package/server/web-chat/routes/vramRoute.js +22 -0
  82. package/server/web-chat/server.js +135 -106
  83. package/server/web-chat/ui/htmlBuilder.js +8 -8
  84. package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
  85. package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
  86. package/server/web-chat/ui/scripts/client.js +10526 -8397
  87. package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
  88. package/server/web-chat/ui/styles/main.css +7471 -4246
  89. package/server/web-chat/ui/templates/body.html +305 -179
  90. package/src/data/brandsData.js +29 -6
  91. package/src/data/discographyData.js +918 -900
  92. package/src/data/galleryData.js +66 -0
  93. package/src/data/industryIndexData.js +292 -0
  94. package/src/data/musicVideoData.js +48 -5
  95. package/src/data/profileData.js +15 -1
  96. package/src/data/timelineData.js +4 -3
  97. package/src/data/worksData.js +536 -535
  98. package/src/lib/vram/estimator.js +464 -0
  99. package/src/lib/vram/modelRegistry.js +387 -0
  100. package/tokens.css +33 -2
@@ -1,7 +1,7 @@
1
1
  import { timingSafeEqual } from 'node:crypto';
2
2
 
3
3
  const SECURITY_CONFIG = {
4
- apiKeyRequired: !!process.env.MCP_API_KEY,
4
+ apiKeyRequired: Boolean(process.env.MCP_API_KEY || process.env.NODE_ENV === 'production'),
5
5
 
6
6
  rateLimitWindowMs: 60_000,
7
7
  rateLimitMaxRequests: 30,
@@ -10,7 +10,7 @@ const SECURITY_CONFIG = {
10
10
  sessionTtlMs: 30 * 60_000,
11
11
  maxMessagesPerSession: 500,
12
12
 
13
- corsAllowedOrigins: (process.env.MCP_CORS_ORIGINS || '').split(',').map(s => s.trim()).filter(Boolean),
13
+ corsAllowedOrigins: (process.env.MCP_CORS_ORIGINS || 'https://jujingyi-kikuuu.vercel.app').split(',').map(s => s.trim()).filter(Boolean),
14
14
  maxRequestBodyBytes: 1_048_576,
15
15
  };
16
16
 
@@ -19,13 +19,16 @@ export const MAX_REQUEST_BODY_SIZE = SECURITY_CONFIG.maxRequestBodyBytes;
19
19
  export function validateApiKey(req) {
20
20
  if (!SECURITY_CONFIG.apiKeyRequired) return { valid: true };
21
21
 
22
- let key = req.headers['x-api-key'];
22
+ let key = req?.headers?.['x-api-key'];
23
23
  if (!key) {
24
- const authHeader = req.headers['authorization'];
24
+ const authHeader = req?.headers?.['authorization'];
25
25
  if (authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
26
26
  key = authHeader.slice(7).trim();
27
27
  }
28
28
  }
29
+ if (!process.env.MCP_API_KEY) {
30
+ return { valid: false, error: 'MCP API key is not configured on the server. Please set MCP_API_KEY.' };
31
+ }
29
32
  if (!key) return { valid: false, error: 'Missing API key. Set X-API-Key header or Authorization: Bearer <key>.' };
30
33
  const expected = Buffer.from(process.env.MCP_API_KEY || '');
31
34
  const received = Buffer.from(String(key));
@@ -88,16 +91,24 @@ export function validateSessionId(sessionId) {
88
91
  return /^[a-zA-Z0-9_-]{1,100}$/.test(sessionId);
89
92
  }
90
93
 
91
- export function setCorsHeaders(res, origin) {
92
- const allowed = SECURITY_CONFIG.corsAllowedOrigins;
93
- const isLocalhost = process.env.NODE_ENV !== 'production' &&
94
- (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:') || origin === 'http://localhost' || origin === 'http://127.0.0.1');
95
- const isAllowed = !origin || allowed.includes(origin) || isLocalhost;
96
-
97
- if (origin && isAllowed) {
98
- res.setHeader('Access-Control-Allow-Origin', origin);
99
- res.setHeader('Vary', 'Origin');
100
- }
94
+ export function setCorsHeaders(res, origin) {
95
+ const allowed = SECURITY_CONFIG.corsAllowedOrigins;
96
+ let isLocalhost = false;
97
+ if (process.env.NODE_ENV !== 'production' && origin) {
98
+ try {
99
+ const parsed = new URL(origin);
100
+ isLocalhost = (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') &&
101
+ (parsed.protocol === 'http:' || parsed.protocol === 'https:');
102
+ } catch {
103
+ isLocalhost = false;
104
+ }
105
+ }
106
+ const isAllowed = !origin || allowed.includes(origin) || isLocalhost;
107
+
108
+ if (origin && isAllowed) {
109
+ res.setHeader('Access-Control-Allow-Origin', origin);
110
+ res.setHeader('Vary', 'Origin');
111
+ }
101
112
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
102
113
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-API-Key, Accept');
103
114
  res.setHeader('Access-Control-Max-Age', '86400');
@@ -5,6 +5,27 @@ import { toolDefinitions, handleToolCall } from './tools.js';
5
5
  import { resourceDefinitions, readResource } from './resources.js';
6
6
  import { getPrompt } from './prompts.js';
7
7
 
8
+ const WRITE_TOOLS = new Set([
9
+ 'manage_skill',
10
+ 'play_music', 'stop_music', 'pause_music', 'resume_music',
11
+ 'mute_music', 'unmute_music', 'next_track', 'prev_track',
12
+ 'add_to_queue', 'remove_from_queue', 'clear_queue',
13
+ ]);
14
+
15
+
16
+ const NON_IDEMPOTENT_TOOLS = new Set([
17
+ 'manage_skill', 'play_music', 'add_to_queue', 'remove_from_queue',
18
+ 'clear_queue', 'next_track', 'prev_track', 'run_quiz',
19
+ ]);
20
+
21
+ const OPEN_WORLD_TOOLS = new Set([
22
+ 'web_search', 'web_extract', 'fetch_webpage', 'fetch_api', 'fetch_json',
23
+ 'screenshot_webpage', 'search_news', 'fetch_rss', 'search_encyclopedia',
24
+ 'search_music_platforms', 'search_chinese_web', 'search_bilibili',
25
+ 'search_youtube', 'search_web_images', 'run_quiz', 'run_subagent',
26
+ 'run_deep_research', 'verify_claim', 'suggest_topics',
27
+ ]);
28
+
8
29
  export function createMcpServer(options = {}) {
9
30
  const toolOptions = {
10
31
  transport: options.transport || 'stdio',
@@ -52,12 +73,19 @@ export function createMcpServer(options = {}) {
52
73
  });
53
74
 
54
75
  for (const tool of toolDefinitions) {
76
+ const description = tool.name === 'get_api_key'
77
+ ? `${tool.description} (SENSITIVE: returns a stored Developer VIP API key — do not expose it publicly.)`
78
+ : tool.description;
55
79
  server.registerTool(
56
80
  tool.name,
57
81
  {
58
- description: tool.description,
82
+ description,
59
83
  inputSchema: tool.inputSchema,
60
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
84
+ annotations: {
85
+ readOnlyHint: !WRITE_TOOLS.has(tool.name),
86
+ idempotentHint: !NON_IDEMPOTENT_TOOLS.has(tool.name),
87
+ openWorldHint: OPEN_WORLD_TOOLS.has(tool.name),
88
+ },
61
89
  },
62
90
  async (args) => {
63
91
  try {
@@ -85,11 +113,13 @@ export function createMcpServer(options = {}) {
85
113
  'kiku://discography': 'kiku-discography',
86
114
  'kiku://brands': 'kiku-brands',
87
115
  'kiku://social': 'kiku-social',
116
+ 'kiku://industry-index': 'kiku-industry-index',
88
117
  };
89
118
 
90
119
  for (const def of resourceDefinitions) {
120
+ const resourceName = resourceMap[def.uri] || def.uri.replace('kiku://', 'kiku-');
91
121
  server.registerResource(
92
- resourceMap[def.uri],
122
+ resourceName,
93
123
  def.uri,
94
124
  { description: def.description, mimeType: def.mimeType },
95
125
  async (uri) => readResource(uri.href)
@@ -131,5 +161,64 @@ export function createMcpServer(options = {}) {
131
161
  async (args) => getPrompt('career_overview', args)
132
162
  );
133
163
 
164
+ server.registerPrompt(
165
+ 'draft_fan_fiction',
166
+ {
167
+ description: 'Draft creative fan fiction scenarios incorporating authentic Ju Jingyi characters and lore.',
168
+ argsSchema: {
169
+ character: z.string().min(1).describe('Character name (e.g. "Yang Caiwei", "Han Yunxi", "Fu Rong")'),
170
+ theme: z.string().optional().describe('Story theme or mood (mystery, romance, fantasy adventure)'),
171
+ setting: z.string().optional().describe('Story backdrop or scenario'),
172
+ },
173
+ },
174
+ async (args) => getPrompt('draft_fan_fiction', args)
175
+ );
176
+
177
+ server.registerPrompt(
178
+ 'analyze_drama_rating',
179
+ {
180
+ description: 'Provide an analytical overview of a drama\'s reception, milestones, and iconic performances.',
181
+ argsSchema: {
182
+ title: z.string().min(1).describe('Drama title (e.g. "In Blossom", "Legend of Yunxi")'),
183
+ },
184
+ },
185
+ async (args) => getPrompt('analyze_drama_rating', args)
186
+ );
187
+
188
+ server.registerPrompt(
189
+ 'compare_discography_era',
190
+ {
191
+ description: 'Compare musical styles and evolution across Ju Jingyi\'s idol, solo, and studio eras.',
192
+ argsSchema: {
193
+ era_a: z.string().optional().describe('First era (e.g. "snh48", "solo_early", "studio_era")'),
194
+ era_b: z.string().optional().describe('Second era (e.g. "modern", "solo")'),
195
+ },
196
+ },
197
+ async (args) => getPrompt('compare_discography_era', args)
198
+ );
199
+
200
+ server.registerPrompt(
201
+ 'investigate_viral_claim',
202
+ {
203
+ description: 'Investigate a viral claim, set leak, casting rumor, or news report about Ju Jingyi using multi-tool evidence and catalog cross-referencing.',
204
+ argsSchema: {
205
+ claim: z.string().min(1).describe('The viral rumor, set leak, or statement to investigate'),
206
+ platform_hint: z.string().optional().describe('Platform where the claim went viral (e.g. "weibo", "douyin", "baidu")'),
207
+ },
208
+ },
209
+ async (args) => getPrompt('investigate_viral_claim', args)
210
+ );
211
+
212
+ server.registerPrompt(
213
+ 'deep_research_topic',
214
+ {
215
+ description: 'Perform an exhaustive deep research investigation on any Ju Jingyi entertainment topic across catalog, live Chinese media, news, and images.',
216
+ argsSchema: {
217
+ topic: z.string().min(1).describe('The topic, milestone, or controversy to research in depth'),
218
+ },
219
+ },
220
+ async (args) => getPrompt('deep_research_topic', args)
221
+ );
222
+
134
223
  return server;
135
224
  }
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { executeTool, toolsSchema } from '../agent/tools.js';
3
+ import { sanitizeInput } from './security.js';
3
4
 
4
- // Rich catalog/database tools — safe to expose over public MCP.
5
5
  const PUBLIC_TOOL_NAMES = new Set([
6
6
  'get_profile_and_fandom',
7
7
  'get_quotes',
@@ -18,26 +18,34 @@ const PUBLIC_TOOL_NAMES = new Set([
18
18
  'get_gallery_photos',
19
19
  'global_search',
20
20
  'get_system_stats',
21
+ 'get_current_datetime',
22
+ 'get_youtube_transcript',
23
+ 'search_news',
24
+ 'fetch_rss',
25
+ 'search_encyclopedia',
26
+ 'search_music_platforms',
27
+ 'get_entertainment_metrics',
28
+ 'get_industry_index',
21
29
  ]);
22
30
 
23
- // Web tools — gated behind KIKU_MCP_ENABLE_WEB_TOOLS flag.
24
- const OPTIONAL_WEB_TOOL_NAMES = new Set(['web_search', 'fetch_webpage', 'fetch_api', 'web_extract']);
31
+ const OPTIONAL_WEB_TOOL_NAMES = new Set(['web_search', 'fetch_webpage', 'fetch_api', 'web_extract', 'search_web_images']);
25
32
  const webToolsEnabled = process.env.KIKU_MCP_ENABLE_WEB_TOOLS === 'true';
26
33
 
27
- // Anonymous callers may search public music metadata, but cannot mutate playback state.
34
+ const OPTIONAL_CN_TOOL_NAMES = new Set(['search_chinese_web', 'search_bilibili', 'search_web_images']);
35
+ const cnToolsEnabled = process.env.KIKU_ENABLE_CN_WEB_TOOLS === 'true';
36
+
28
37
  const MUSIC_TOOL_NAMES = new Set([
29
38
  'search_youtube',
30
39
  ]);
31
40
 
32
- // Stateless engagement tools are safe to expose.
33
41
  const RESEARCH_TOOL_NAMES = new Set([
34
42
  'suggest_topics',
35
43
  'run_quiz',
44
+ 'checkpoint_step',
45
+ 'record_failure',
46
+ 'load_active_run',
36
47
  ]);
37
48
 
38
- // Privileged tools — they touch the filesystem, server-local resources, secrets, or
39
- // mutated server state. Exposed over MCP but REQUIRE authorization (a valid MCP_API_KEY
40
- // / X-API-Key / Bearer header). Never callable anonymously on the public HTTP endpoint.
41
49
  const PRIVILEGED_TOOL_NAMES = new Set([
42
50
  'read_local_file',
43
51
  'list_directory',
@@ -64,12 +72,13 @@ const PRIVILEGED_TOOL_NAMES = new Set([
64
72
  'fetch_json',
65
73
  ]);
66
74
 
67
- // Any tool that is not privileged is public.
68
75
  const isPrivileged = (name) => PRIVILEGED_TOOL_NAMES.has(name);
69
76
 
70
77
  function isAllowedTool(name) {
71
78
  if (PUBLIC_TOOL_NAMES.has(name) || MUSIC_TOOL_NAMES.has(name) || RESEARCH_TOOL_NAMES.has(name)) return true;
72
- if (OPTIONAL_WEB_TOOL_NAMES.has(name)) return webToolsEnabled;
79
+ if (OPTIONAL_WEB_TOOL_NAMES.has(name) && webToolsEnabled) return true;
80
+ if (OPTIONAL_CN_TOOL_NAMES.has(name) && cnToolsEnabled) return true;
81
+ if (OPTIONAL_WEB_TOOL_NAMES.has(name) || OPTIONAL_CN_TOOL_NAMES.has(name)) return false;
73
82
  return PRIVILEGED_TOOL_NAMES.has(name);
74
83
  }
75
84
 
@@ -164,7 +173,7 @@ export async function handleToolCall(toolName, args = {}, options = {}) {
164
173
  }
165
174
  }
166
175
 
167
- const adaptedArgs = { ...(args || {}) };
176
+ const adaptedArgs = { ...sanitizeInput(args || {}) };
168
177
  if (toolName === 'search_gallery') delete adaptedArgs.query;
169
178
  if (adaptedArgs.category && definition.targetName === 'search_discography' && !adaptedArgs.type) {
170
179
  adaptedArgs.type = adaptedArgs.category;