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
@@ -0,0 +1,339 @@
1
+ import { classifySource, extractArticleDate } from './newsIntelligence.js';
2
+ import { simhashText, hamming64 } from './websearch/extract.js';
3
+ import {
4
+ addDynamicEntry,
5
+ getDynamicEntries,
6
+ validateCatalogEntry,
7
+ DEFAULT_DYNAMIC_CATALOG_PATH
8
+ } from './dynamicCatalog.js';
9
+ import { searchNews } from './webSearch.js';
10
+
11
+ export const KNOWN_OFFICIAL_PLATFORMS = new Set([
12
+ 'weibo', 'weibo.com', 'weibo.cn',
13
+ 'douyin', 'douyin.com',
14
+ 'bilibili', 'bilibili.com',
15
+ 'youtube', 'youtube.com',
16
+ 'instagram', 'instagram.com',
17
+ 'xiaohongshu', 'xiaohongshu.com', 'xhs',
18
+ 'tiktok', 'tiktok.com'
19
+ ]);
20
+
21
+ export function isTrustedSource(source) {
22
+ if (!source || typeof source !== 'string') {
23
+ return { trusted: false, tier: 3, label: 'unknown' };
24
+ }
25
+ const sLower = source.toLowerCase().trim();
26
+
27
+ if (KNOWN_OFFICIAL_PLATFORMS.has(sLower)) {
28
+ return { trusted: true, tier: 1, label: 'official' };
29
+ }
30
+
31
+ if (/^https?:\/\//i.test(sLower)) {
32
+ const classified = classifySource(source);
33
+ return {
34
+ trusted: classified.tier <= 2,
35
+ tier: classified.tier,
36
+ label: classified.label
37
+ };
38
+ }
39
+
40
+ if (/(weibo|douyin|bilibili|youtube|instagram|xhs|xiaohongshu|tiktok|official|工作室)/i.test(sLower)) {
41
+ return { trusted: true, tier: 1, label: 'official' };
42
+ }
43
+ if (/(press|news|sohu|netease|sina|tencent|qq|thairath|sanook|kapook|pptv)/i.test(sLower)) {
44
+ return { trusted: true, tier: 2, label: 'press' };
45
+ }
46
+
47
+ return { trusted: false, tier: 3, label: 'community' };
48
+ }
49
+
50
+ export function computeEventSimHash(text) {
51
+ return simhashText(String(text || ''));
52
+ }
53
+
54
+ export function isDuplicateEvent(entry, existingList = [], { maxHamming = 8 } = {}) {
55
+ if (!entry || !Array.isArray(existingList) || existingList.length === 0) {
56
+ return false;
57
+ }
58
+
59
+ const entryText = `${entry.title || entry.brand || entry.brandNameEn || ''} ${entry.highlight || ''} ${entry.summary || ''}`.trim();
60
+ const entryHash = entryText ? computeEventSimHash(entryText) : null;
61
+
62
+ for (const existing of existingList) {
63
+ if (entry.id && existing.id && entry.id === existing.id) {
64
+ return true;
65
+ }
66
+
67
+ const entryBrand = (entry.brand || entry.brandNameEn || entry.brandNameCn || '').toLowerCase().trim();
68
+ const existingBrand = (existing.brand || existing.brandNameEn || existing.brandNameCn || '').toLowerCase().trim();
69
+ if (entryBrand && existingBrand && entryBrand === existingBrand && entry.year === existing.year) {
70
+ return true;
71
+ }
72
+
73
+ const entryTitle = (entry.title || entry.titleCn || entry.titleTh || entry.titleEn || '').toLowerCase().trim();
74
+ const existingTitle = (existing.title || existing.titleCn || existing.titleTh || existing.titleEn || '').toLowerCase().trim();
75
+ if (entryTitle && existingTitle && entryTitle === existingTitle && entry.year === existing.year) {
76
+ return true;
77
+ }
78
+
79
+ const eH = (entry.highlight || entry.summary || '').toLowerCase().trim();
80
+ const xH = (existing.highlight || existing.summary || '').toLowerCase().trim();
81
+ if (eH && xH && (eH.includes(xH) || xH.includes(eH))) {
82
+ return true;
83
+ }
84
+
85
+ if (entryHash != null) {
86
+ const existText = `${existing.title || existing.brand || existing.brandNameEn || ''} ${existing.highlight || ''} ${existing.summary || ''}`.trim();
87
+ if (existText) {
88
+ const existHash = computeEventSimHash(existText);
89
+ if (hamming64(entryHash, existHash) <= maxHamming) {
90
+ return true;
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ return false;
97
+ }
98
+
99
+ export async function extractStructuredEvent(text, { client = null } = {}) {
100
+ const rawText = String(text || '').trim();
101
+ if (!rawText) {
102
+ throw new Error('Event text cannot be empty');
103
+ }
104
+
105
+ if (client && typeof client.chatCompletion === 'function') {
106
+ try {
107
+ const prompt = `Extract a structured event for artist Ju Jingyi (鞠婧祎) from this text into JSON:
108
+ Text: "${rawText}"
109
+
110
+ Return ONLY a JSON object with:
111
+ - category: one of ["brands", "timeline", "gallery", "works", "events"]
112
+ - year: integer year (e.g. 2026)
113
+ - date: ISO date string or "YYYY-MM-DD" if present
114
+ - title: concise title of the event / work
115
+ - titleCn: Chinese title or null
116
+ - titleTh: Thai title or null
117
+ - brand: brand name (if brand endorsement) or null
118
+ - brandNameCn: Chinese brand name or null
119
+ - brandNameEn: English brand name or null
120
+ - role: role/title (e.g. "Brand Ambassador", "Lead Actress", "Performer")
121
+ - highlight: key highlight sentence
122
+ - summary: full description
123
+ - imageUrl: image URL or null
124
+ - source: origin platform or media outlet`;
125
+
126
+ const response = await client.chatCompletion({
127
+ messages: [{ role: 'user', content: prompt }],
128
+ temperature: 0.1,
129
+ });
130
+
131
+ const jsonStr = response?.choices?.[0]?.message?.content || '';
132
+ const match = jsonStr.match(/\{[\s\S]*\}/);
133
+ if (match) {
134
+ const parsed = JSON.parse(match[0]);
135
+ if (parsed && (parsed.title || parsed.brand || parsed.brandNameEn)) {
136
+ return parsed;
137
+ }
138
+ }
139
+ } catch {
140
+
141
+ }
142
+ }
143
+
144
+ const currentYear = new Date().getFullYear();
145
+ const dateInfo = extractArticleDate({ snippet: rawText, title: rawText });
146
+ const eventYear = dateInfo?.date ? dateInfo.date.getUTCFullYear() : currentYear;
147
+ let isoDate = dateInfo?.date ? dateInfo.date.toISOString().slice(0, 10) : null;
148
+
149
+ if (!isoDate) {
150
+ const THAI_MONTHS_MAP = {
151
+ 'มกราคม': 1, 'กุมภาพันธ์': 2, 'มีนาคม': 3, 'เมษายน': 4, 'พฤษภาคม': 5, 'มิถุนายน': 6,
152
+ 'กรกฎาคม': 7, 'สิงหาคม': 8, 'กันยายน': 9, 'ตุลาคม': 10, 'พฤศจิกายน': 11, 'ธันวาคม': 12,
153
+ 'มค': 1, 'กพ': 2, 'มีค': 3, 'เมย': 4, 'พค': 5, 'มิย': 6, 'กค': 7, 'สค': 8, 'กย': 9, 'ตค': 10, 'พย': 11, 'ธค': 12,
154
+ };
155
+ const thMatch = rawText.match(/(?:วันที่\s*)?(\d{1,2})\s*(มกราคม|กุมภาพันธ์|มีนาคม|เมษายน|พฤษภาคม|มิถุนายน|กรกฎาคม|สิงหาคม|กันยายน|ตุลาคม|พฤศจิกายน|ธันวาคม|ม\.?ค\.?|ก\.?พ\.?|มี\.?ค\.?|เม\.?ย\.?|พ\.?ค\.?|มิ\.?ย\.?|ก\.?ค\.?|ส\.?ค\.?|ก\.?ย\.?|ต\.?ค\.?|พ\.?ย\.?|ธ\.?ค\.?)/i);
156
+ if (thMatch) {
157
+ const day = String(thMatch[1]).padStart(2, '0');
158
+ const mKey = thMatch[2].replace(/\./g, '');
159
+ const monthNum = THAI_MONTHS_MAP[mKey] || 1;
160
+ const month = String(monthNum).padStart(2, '0');
161
+ isoDate = `${eventYear}-${month}-${day}`;
162
+ } else {
163
+ isoDate = `${eventYear}-01-01`;
164
+ }
165
+ }
166
+
167
+ let category = 'events';
168
+ const isBrand = /(แบรนด์|พรีเซ็นเตอร์|โฆษณา|brand|ambassador|endorsement|CF|Commercial|代言|Lafang|拉芳|FRED|TuTuCare)/i.test(rawText);
169
+ const isGallery = /(ลุค|ชุดเดรส|ภาพ|รูปถ่าย|photoshoot|look|dress|gallery|fashion)/i.test(rawText);
170
+ const isWork = /(ซีรีส์|ละคร|เพลง|อัลบั้ม|ซิงเกิล|OST|drama|album|single|series|film)/i.test(rawText);
171
+ const isTimeline = /(milestone|เปิดตัว|ครบรอบ|debut|graduation|timeline)/i.test(rawText);
172
+
173
+ if (isBrand) category = 'brands';
174
+ else if (isWork) category = 'works';
175
+ else if (isGallery) category = 'gallery';
176
+ else if (isTimeline) category = 'timeline';
177
+
178
+ let brandNameEn = '';
179
+ let brandNameCn = '';
180
+ if (isBrand) {
181
+ if (/lafang|拉芳/i.test(rawText)) {
182
+ brandNameEn = 'Lafang';
183
+ brandNameCn = '拉芳';
184
+ } else if (/fred/i.test(rawText)) {
185
+ brandNameEn = 'FRED Paris';
186
+ brandNameCn = '斐登';
187
+ } else if (/tutucare/i.test(rawText)) {
188
+ brandNameEn = 'TuTuCare';
189
+ brandNameCn = '途途呵护';
190
+ } else {
191
+ const bMatch = rawText.match(/แบรนด์\s+([A-Za-z0-9\u4e00-\u9fa5]+)/i) || rawText.match(/brand\s+([A-Za-z0-9\u4e00-\u9fa5]+)/i);
192
+ if (bMatch) {
193
+ brandNameEn = bMatch[1];
194
+ }
195
+ }
196
+ }
197
+
198
+ let title = '';
199
+ const quoteMatch = rawText.match(/["“'「]([^"”'」]{3,50})["”'」]/);
200
+ if (quoteMatch) {
201
+ title = quoteMatch[1];
202
+ } else {
203
+ const firstSentence = rawText.split(/[.\n!?|]/)[0]?.trim();
204
+ title = firstSentence && firstSentence.length <= 80 ? firstSentence : `${brandNameEn || 'Ju Jingyi'} Event ${eventYear}`;
205
+ }
206
+
207
+ return {
208
+ category,
209
+ year: eventYear,
210
+ date: isoDate,
211
+ title,
212
+ titleCn: brandNameCn || null,
213
+ titleTh: title,
214
+ brand: brandNameEn || (isBrand ? title : null),
215
+ brandNameEn: brandNameEn || null,
216
+ brandNameCn: brandNameCn || null,
217
+ role: isBrand ? 'Brand Ambassador' : 'Artist',
218
+ highlight: rawText.slice(0, 200).trim(),
219
+ summary: rawText,
220
+ source: 'extracted_text'
221
+ };
222
+ }
223
+
224
+ export async function ingestRawEvent(text, {
225
+ category = null,
226
+ source = 'manual',
227
+ trustTier = null,
228
+ client = null,
229
+ catalogPath = DEFAULT_DYNAMIC_CATALOG_PATH
230
+ } = {}) {
231
+ try {
232
+ const trustInfo = isTrustedSource(source);
233
+ const finalTrustTier = trustTier != null ? Number(trustTier) : trustInfo.tier;
234
+
235
+ if (finalTrustTier > 2 && !trustInfo.trusted && trustTier == null) {
236
+ return {
237
+ status: 'rejected',
238
+ reason: 'Untrusted source (Tier 3 community/unverified). Provide explicit trustTier to override.',
239
+ source,
240
+ tier: finalTrustTier
241
+ };
242
+ }
243
+
244
+ const structured = await extractStructuredEvent(text, { client });
245
+ const targetCategory = category || structured.category || 'events';
246
+
247
+ const entryToSave = {
248
+ ...structured,
249
+ source: source || structured.source || 'manual',
250
+ trustTier: finalTrustTier
251
+ };
252
+
253
+ const existing = getDynamicEntries(targetCategory, null, catalogPath);
254
+ if (isDuplicateEvent(entryToSave, existing)) {
255
+ return {
256
+ status: 'duplicate',
257
+ message: `Event already exists in dynamic catalog category "${targetCategory}".`,
258
+ entry: entryToSave
259
+ };
260
+ }
261
+
262
+ const saved = addDynamicEntry(targetCategory, entryToSave, {
263
+ catalogPath,
264
+ trustTier: finalTrustTier,
265
+ source
266
+ });
267
+
268
+ return {
269
+ status: 'success',
270
+ message: `Successfully ingested event into category "${targetCategory}".`,
271
+ entry: saved
272
+ };
273
+ } catch (err) {
274
+ return {
275
+ status: 'error',
276
+ message: `Ingestion failed: ${err.message}`
277
+ };
278
+ }
279
+ }
280
+
281
+ export async function runIngestionScan({
282
+ topic = 'Ju Jingyi 鞠婧祎',
283
+ fetcher = null,
284
+ client = null,
285
+ catalogPath = DEFAULT_DYNAMIC_CATALOG_PATH,
286
+ minTrustTier = 2,
287
+ maxItems = 8
288
+ } = {}) {
289
+ let items = [];
290
+ if (typeof fetcher === 'function') {
291
+ items = await fetcher(topic);
292
+ } else {
293
+ const res = await searchNews(topic, maxItems);
294
+ items = res?.results || [];
295
+ }
296
+
297
+ let totalScanned = items.length;
298
+ let ingestedCount = 0;
299
+ let skippedDuplicates = 0;
300
+ let skippedUntrusted = 0;
301
+ const entries = [];
302
+
303
+ for (const item of items) {
304
+ const url = item.url || '';
305
+ const trust = isTrustedSource(url);
306
+
307
+ if (trust.tier > minTrustTier) {
308
+ skippedUntrusted++;
309
+ continue;
310
+ }
311
+
312
+ const combinedText = `${item.title || ''}\n${item.snippet || ''}`.trim();
313
+ if (!combinedText) continue;
314
+
315
+ const res = await ingestRawEvent(combinedText, {
316
+ source: url || item.source || 'news_scan',
317
+ trustTier: trust.tier,
318
+ client,
319
+ catalogPath
320
+ });
321
+
322
+ if (res.status === 'success') {
323
+ ingestedCount++;
324
+ entries.push(res.entry);
325
+ } else if (res.status === 'duplicate') {
326
+ skippedDuplicates++;
327
+ } else if (res.status === 'rejected') {
328
+ skippedUntrusted++;
329
+ }
330
+ }
331
+
332
+ return {
333
+ totalScanned,
334
+ ingestedCount,
335
+ skippedDuplicates,
336
+ skippedUntrusted,
337
+ entries
338
+ };
339
+ }
@@ -0,0 +1,348 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { Box, Text, Static, useInput, useApp, useStdin } from 'ink';
3
+ import { PromptInput } from './PromptInput.jsx';
4
+ import { ThinkingPanel } from './ThinkingPanel.jsx';
5
+ import { ToolCompleted, ToolSummary, ToolRunning } from './ToolResult.jsx';
6
+ import { TaskPlanPanel } from './TaskPlanPanel.jsx';
7
+ import { Panel, Separator } from './Panel.jsx';
8
+ import { useAgentState, useStreamingText, useThinking, useToolResults, useTaskPlan, useEvent } from './useEventBus.js';
9
+ import { eventBus, Events } from '../eventBus.js';
10
+ import { getInkPalette } from '../theme.js';
11
+ import { PACKAGE_VERSION } from '../version.js';
12
+
13
+ const FAN_NAMES = ['Mìjú / 蜜橘'];
14
+ function randomFanName() {
15
+ return FAN_NAMES[Math.floor(Math.random() * FAN_NAMES.length)];
16
+ }
17
+
18
+ function Header() {
19
+ const theme = getInkPalette();
20
+
21
+ return (
22
+ <Box flexDirection="column">
23
+ <Text color={theme.primary}>
24
+ {' ██╗ ██╗██╗██╗ ██╗██╗ ██╗ █████╗ ██╗'}
25
+ </Text>
26
+ <Text color={theme.primary}>
27
+ {' ██║ ██╔╝██║██║ ██╔╝██║ ██║ ██╔══██╗██║'}
28
+ </Text>
29
+ <Text color={theme.secondary}>
30
+ {' █████═╝ ██║█████═╝ ██║ ██║ ███████║██║'}
31
+ </Text>
32
+ <Text color={theme.secondary}>
33
+ {' ██╔═██╗ ██║██╔═██╗ ██║ ██║ ██╔══██║██║'}
34
+ </Text>
35
+ <Text color={theme.primary}>
36
+ {' ██║ ╚██╗██║██║ ╚██╗╚██████╔╝ ██║ ██║██║'}
37
+ </Text>
38
+ <Text color={theme.primary}>
39
+ {' ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝'}
40
+ </Text>
41
+ <Text />
42
+ <Box>
43
+ <Text color={theme.muted}>Architecture: </Text>
44
+ <Text bold color={theme.highlight}>Next-Gen v{PACKAGE_VERSION} </Text>
45
+ <Text color={theme.muted}>• SSOT Catalog • Dynamic Tool Router • Semantic Memory</Text>
46
+ </Box>
47
+ <Text />
48
+ <Text bold color={theme.secondary}>Example Questions & Capabilities:</Text>
49
+ <Text color={theme.text}> <Text color={theme.primary}>[Dramas 2026]</Text> <Text>"What are Ju Jingyi's upcoming 2026 drama series (Veil of Shadows, Eternal Fragrance)?"</Text></Text>
50
+ <Text color={theme.text}> <Text color={theme.primary}>[Characters ]</Text> <Text>"Tell me about Yang Caiwei in In Blossom and Han Yunxi in Legend of Yunxi."</Text></Text>
51
+ <Text color={theme.text}> <Text color={theme.info}>[Music & OST]</Text> <Text>"What OST songs are from The Blooms at Ruyi Pavilion? /play Rain of Ginkgo"</Text></Text>
52
+ <Text color={theme.text}> <Text color={theme.info}>[Global Brand]</Text> <Text>"Tell me about her Global Fragrance Ambassador role for Versace and luxury brands."</Text></Text>
53
+ <Text color={theme.text}> <Text color={theme.success}>[Fact-Check ]</Text> <Text>"/verify Check Star48 contract status or /research 2026 broadcast licenses"</Text></Text>
54
+ <Text color={theme.text}> <Text color={theme.success}>[Persona/Mem]</Text> <Text>"/skill learn Always answer politely as a passionate Miju fan club member"</Text></Text>
55
+ <Text color={theme.text}> <Text color={theme.tertiary}>[Multi-Agent]</Text> <Text>"/subagent Investigate her upcoming drama production timeline and Weibo updates"</Text></Text>
56
+ <Text color={theme.text}> <Text color={theme.tertiary}>[System Stats]</Text> <Text>"/stats or /doctor to inspect database records, models, and audio engine"</Text></Text>
57
+ <Text />
58
+ <Text>
59
+ <Text color={theme.tertiary}>Quick Commands: </Text>
60
+ <Text color={theme.info}>/help</Text>
61
+ <Text dimColor> • </Text>
62
+ <Text color={theme.success}>/subagent</Text>
63
+ <Text dimColor> • </Text>
64
+ <Text color={theme.success}>/research</Text>
65
+ <Text dimColor> • </Text>
66
+ <Text color={theme.success}>/verify</Text>
67
+ <Text dimColor> • </Text>
68
+ <Text color={theme.success}>/skill</Text>
69
+ <Text dimColor> • </Text>
70
+ <Text color={theme.success}>/exit</Text>
71
+ <Text dimColor> (Tab or ↑/↓ to navigate)</Text>
72
+ </Text>
73
+ <Text />
74
+ </Box>
75
+ );
76
+ }
77
+
78
+ function UserMessage({ text }) {
79
+ const theme = getInkPalette();
80
+
81
+ return (
82
+ <Box flexDirection="column">
83
+ <Text>
84
+ <Text color={theme.secondary} bold>{randomFanName()}: </Text>
85
+ <Text color={theme.text}>{text}</Text>
86
+ </Text>
87
+ </Box>
88
+ );
89
+ }
90
+
91
+ function StreamingResponse({ text, isStreaming }) {
92
+ if (!text && !isStreaming) return null;
93
+ const theme = getInkPalette();
94
+
95
+ const lines = text.split('\n');
96
+
97
+ return (
98
+ <Box flexDirection="column">
99
+ {lines.map((line, i) => (
100
+ <Box key={i}>
101
+ <Text> </Text>
102
+ <Text color={theme.text}>{line || ' '}</Text>
103
+ </Box>
104
+ ))}
105
+ {isStreaming && (
106
+ <Box>
107
+ <Text color={theme.muted} dimColor> ▌</Text>
108
+ </Box>
109
+ )}
110
+ </Box>
111
+ );
112
+ }
113
+
114
+ function CompletedResponse({ text }) {
115
+ if (!text) return null;
116
+ const theme = getInkPalette();
117
+
118
+ const lines = text.split('\n');
119
+
120
+ return (
121
+ <Box flexDirection="column">
122
+ {lines.map((line, i) => (
123
+ <Box key={i}>
124
+ <Text> </Text>
125
+ <Text color={theme.text}>{line || ' '}</Text>
126
+ </Box>
127
+ ))}
128
+ </Box>
129
+ );
130
+ }
131
+
132
+ function MusicStatusBar({ musicStatus }) {
133
+ if (!musicStatus) return null;
134
+ const theme = getInkPalette();
135
+
136
+ return (
137
+ <Box>
138
+ <Text color={theme.info}>{musicStatus} </Text>
139
+ </Box>
140
+ );
141
+ }
142
+
143
+ export function App({ agent, client, baseUrl, printHelp, printAd }) {
144
+ const { exit } = useApp();
145
+ const theme = getInkPalette();
146
+ const [outputLines, setOutputLines] = useState([]);
147
+ const [isProcessing, setIsProcessing] = useState(false);
148
+ const [lastQuery, setLastQuery] = useState(null);
149
+ const [finalAnswer, setFinalAnswer] = useState('');
150
+ const [, setThemeRevision] = useState(0);
151
+
152
+ const agentState = useAgentState();
153
+ const { text: streamingText, isStreaming, reset: resetStreaming } = useStreamingText();
154
+ const thinking = useThinking();
155
+ const { tools, clearTools } = useToolResults();
156
+ const { plan: taskPlan, clearPlan } = useTaskPlan();
157
+ const musicStatus = useEvent('music:state:change', '');
158
+
159
+ const agentStateLabel = isProcessing ? agentState.label : '';
160
+ const showStreaming = isStreaming && streamingText.length > 0;
161
+
162
+ useEffect(() => {
163
+ const unsub = eventBus.on(Events.AGENT_FINAL_ANSWER, (data) => {
164
+ setFinalAnswer(data?.answer || '');
165
+ setIsProcessing(false);
166
+ clearTools();
167
+ });
168
+ return unsub;
169
+ }, []);
170
+
171
+ useEffect(() => {
172
+ const unsub = eventBus.on(Events.AGENT_QUERY_ERROR, (data) => {
173
+ setOutputLines(prev => [...prev, {
174
+ type: 'error',
175
+ text: data?.error || 'Unknown error',
176
+ id: Date.now(),
177
+ }]);
178
+ setIsProcessing(false);
179
+ });
180
+ return unsub;
181
+ }, []);
182
+
183
+ const handleSubmit = useCallback(async (input) => {
184
+ const trimmed = input.trim();
185
+ if (!trimmed) return;
186
+
187
+ setOutputLines(prev => [...prev, {
188
+ type: 'user',
189
+ text: trimmed,
190
+ id: Date.now(),
191
+ }]);
192
+
193
+ if (trimmed.startsWith('/')) {
194
+ const { routeCommand } = await import('../commands.js');
195
+ const cmdCtx = {
196
+ agent,
197
+ client,
198
+ baseUrl,
199
+ sub: trimmed.split(/\s+/)[1] || '',
200
+ extra: trimmed.split(/\s+/).slice(2).join(' '),
201
+ parts: trimmed.split(/\s+/),
202
+ lastQuery,
203
+ printHelp,
204
+ printWelcome: () => {},
205
+ printAd,
206
+ enqueueQuery: () => {},
207
+ clearQueryQueue: () => {},
208
+ getQueryQueueLength: () => 0,
209
+ };
210
+
211
+ const result = await routeCommand(trimmed, cmdCtx);
212
+ const commandName = trimmed.split(/\s+/, 1)[0].toLowerCase();
213
+ if (commandName === '/theme' || commandName === '/themes') {
214
+ setThemeRevision(revision => revision + 1);
215
+ }
216
+ if (result === 'EXIT') {
217
+ exit();
218
+ }
219
+ return;
220
+ }
221
+
222
+ setLastQuery(trimmed);
223
+ setIsProcessing(true);
224
+ setFinalAnswer('');
225
+ resetStreaming();
226
+ clearTools();
227
+ clearPlan();
228
+
229
+ try {
230
+ const { sanitizeQuery } = await import('../sanitizer.js');
231
+ const { sanitized } = sanitizeQuery(trimmed);
232
+ const promptForLLM = sanitized || trimmed;
233
+
234
+ await agent.ask(promptForLLM, {
235
+ onStepStart: () => {},
236
+ onReasoningStart: () => {},
237
+ onReasoningDelta: () => {},
238
+ onReasoningEnd: () => {},
239
+ onToolStart: () => {},
240
+ onToolResult: () => {},
241
+ onTokenStart: () => {},
242
+ onTokenDelta: () => {},
243
+ onFinalAnswer: (answer) => {
244
+ setOutputLines(prev => [...prev, {
245
+ type: 'assistant',
246
+ text: answer,
247
+ tools: [...tools],
248
+ thinking: thinking.thinking,
249
+ thinkingDuration: thinking.duration,
250
+ id: Date.now(),
251
+ }]);
252
+ setIsProcessing(false);
253
+ setFinalAnswer('');
254
+ resetStreaming();
255
+ clearTools();
256
+ },
257
+ onError: (err) => {
258
+ setOutputLines(prev => [...prev, {
259
+ type: 'error',
260
+ text: err.message,
261
+ id: Date.now(),
262
+ }]);
263
+ setIsProcessing(false);
264
+ },
265
+ });
266
+ } catch (err) {
267
+ setOutputLines(prev => [...prev, {
268
+ type: 'error',
269
+ text: err.message,
270
+ id: Date.now(),
271
+ }]);
272
+ setIsProcessing(false);
273
+ }
274
+ }, [agent, client, baseUrl, lastQuery, tools, thinking]);
275
+
276
+ return (
277
+ <Box flexDirection="column" width="100%">
278
+ <Static items={[0]}>
279
+ {() => <Header key="header" />}
280
+ </Static>
281
+
282
+ <Box flexDirection="column">
283
+ {outputLines.map((line) => {
284
+ if (line.type === 'user') {
285
+ return <UserMessage key={line.id} text={line.text} />;
286
+ }
287
+ if (line.type === 'assistant') {
288
+ return (
289
+ <Box key={line.id} flexDirection="column">
290
+ {line.thinking && line.thinking.length > 0 && (
291
+ <ThinkingPanel
292
+ text={line.thinking}
293
+ isActive={false}
294
+ duration={line.thinkingDuration}
295
+ />
296
+ )}
297
+ {line.tools && line.tools.length > 0 && (
298
+ <ToolSummary tools={line.tools} />
299
+ )}
300
+ <CompletedResponse text={line.text} />
301
+ </Box>
302
+ );
303
+ }
304
+ if (line.type === 'error') {
305
+ return (
306
+ <Box key={line.id}>
307
+ <Text> </Text>
308
+ <Text color={theme.error}>[ERROR] {line.text}</Text>
309
+ </Box>
310
+ );
311
+ }
312
+ return null;
313
+ })}
314
+ </Box>
315
+
316
+ {isProcessing && (
317
+ <Box flexDirection="column">
318
+ {taskPlan && <TaskPlanPanel plan={taskPlan} />}
319
+
320
+ {thinking.isActive && (
321
+ <ThinkingPanel
322
+ text={thinking.thinking}
323
+ isActive={true}
324
+ duration={0}
325
+ />
326
+ )}
327
+
328
+ {tools.map((t, idx) => (
329
+ t.status === 'running'
330
+ ? <ToolRunning key={t.id || idx} name={t.name} args={t.args} />
331
+ : <ToolCompleted key={t.id || idx} name={t.name} result={t.result} duration={t.duration} />
332
+ ))}
333
+
334
+ {showStreaming && (
335
+ <StreamingResponse text={streamingText} isStreaming={true} />
336
+ )}
337
+ </Box>
338
+ )}
339
+
340
+ <PromptInput
341
+ onSubmit={handleSubmit}
342
+ musicStatus={musicStatus}
343
+ isProcessing={isProcessing}
344
+ agentStateLabel={agentStateLabel}
345
+ />
346
+ </Box>
347
+ );
348
+ }