kiku-agent 1.0.11 → 1.2.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 (221) hide show
  1. package/README.md +300 -495
  2. package/package.json +12 -11
  3. package/public/fonts/GoogleSans-Bold.ttf +0 -0
  4. package/public/fonts/GoogleSans-Bold.woff2 +0 -0
  5. package/public/fonts/GoogleSans-BoldItalic.ttf +0 -0
  6. package/public/fonts/GoogleSans-Italic.ttf +0 -0
  7. package/public/fonts/GoogleSans-Latin-400.woff2 +0 -0
  8. package/public/fonts/GoogleSans-Latin-500.woff2 +0 -0
  9. package/public/fonts/GoogleSans-Latin-600.woff2 +0 -0
  10. package/public/fonts/GoogleSans-Latin-700.woff2 +0 -0
  11. package/public/fonts/GoogleSans-Medium.ttf +0 -0
  12. package/public/fonts/GoogleSans-Medium.woff2 +0 -0
  13. package/public/fonts/GoogleSans-Regular.ttf +0 -0
  14. package/public/fonts/GoogleSans-Regular.woff2 +0 -0
  15. package/public/fonts/GoogleSans-Thai-400.woff2 +0 -0
  16. package/public/fonts/GoogleSans-Thai-500.woff2 +0 -0
  17. package/public/fonts/GoogleSans-Thai-600.woff2 +0 -0
  18. package/public/fonts/GoogleSans-Thai-700.woff2 +0 -0
  19. package/public/fonts/NotoSansThai-Bold.woff2 +0 -0
  20. package/public/fonts/NotoSansThai-Regular.woff2 +0 -0
  21. package/public/fonts/google-sans-official.css +900 -0
  22. package/public/images/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  23. package/public/images/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  24. package/public/images/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  25. package/public/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  26. package/public/images/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  27. package/public/images/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  28. package/public/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  29. package/public/images/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  30. package/public/images/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  31. package/public/images/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  32. package/public/images/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  33. package/public/images/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  34. package/public/images/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  35. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-399915.jpg +0 -0
  36. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-424041.jpg +0 -0
  37. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-588166.jpg +0 -0
  38. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-672198.jpg +0 -0
  39. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-700522.jpg +0 -0
  40. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-839722.jpg +0 -0
  41. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026825-460586.jpg +0 -0
  42. package/public/images/douyin-update/thumbs//346/212/226/351/237/263202694-689407.jpg +0 -0
  43. package/public/images/douyin-update//346/212/226/351/237/2632026820-399915.mp4 +0 -0
  44. package/public/images/douyin-update//346/212/226/351/237/2632026820-424041.mp4 +0 -0
  45. package/public/images/douyin-update//346/212/226/351/237/2632026820-588166.mp4 +0 -0
  46. package/public/images/douyin-update//346/212/226/351/237/2632026820-672198.mp4 +0 -0
  47. package/public/images/douyin-update//346/212/226/351/237/2632026820-700522.mp4 +0 -0
  48. package/public/images/douyin-update//346/212/226/351/237/2632026820-839722.mp4 +0 -0
  49. package/public/images/douyin-update//346/212/226/351/237/2632026825-460586.mp4 +0 -0
  50. package/public/images/douyin-update//346/212/226/351/237/263202694-689407.mp4 +0 -0
  51. package/public/images/general/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  52. package/public/images/general/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  53. package/public/images/general/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  54. package/public/images/general/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  55. package/public/images/general/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  56. package/public/images/general/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  57. package/public/images/general/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  58. package/public/images/general/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  59. package/public/images/general/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  60. package/public/images/general/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  61. package/public/images/general/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  62. package/public/images/general/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  63. package/public/images/general/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  64. package/public/images/general/jujingyi_portrait.jpg +0 -0
  65. package/public/images/jujingyi_portrait.jpg +0 -0
  66. package/public/images/series/a-millenniums-wait/poster.jpg +0 -0
  67. package/public/images/series/beauty-of-resilience/poster.jpg +0 -0
  68. package/public/images/series/detective-samoyeds/poster.jpg +0 -0
  69. package/public/images/series/everyday/poster.jpg +0 -0
  70. package/public/images/series/fu-rong-ancient-painting/poster.jpg +0 -0
  71. package/public/images/series/in-a-class-of-her-own/poster.jpg +0 -0
  72. package/public/images/series/in-blossom/poster.jpg +0 -0
  73. package/public/images/series/legend-of-yunxi/poster.jpg +0 -0
  74. package/public/images/series/lets-fight/poster-2.jpg +0 -0
  75. package/public/images/series/lets-fight/poster.jpg +0 -0
  76. package/public/images/series/love-emergency/poster.jpg +0 -0
  77. package/public/images/series/love-under-the-full-moon/poster.jpg +0 -0
  78. package/public/images/series/money-bastion/poster.jpg +0 -0
  79. package/public/images/series/mr-swimmer/poster.jpg +0 -0
  80. package/public/images/series/novoland-castle-in-the-sky/poster.jpg +0 -0
  81. package/public/images/series/please-give-me-a-pair-of-wings/poster.jpg +0 -0
  82. package/public/images/series/rebirth-for-you/poster.jpg +0 -0
  83. package/public/images/series/sword-and-fairy-4/poster.jpg +0 -0
  84. package/public/images/series/the-blooms-at-ruyi-pavilion/poster.jpg +0 -0
  85. package/public/images/series/the-eternal-fragrance/poster-2.jpg +0 -0
  86. package/public/images/series/the-eternal-fragrance/poster.jpg +0 -0
  87. package/public/images/series/the-fallen-flowers/poster.jpg +0 -0
  88. package/public/images/series/the-legend-of-white-snake/poster.jpg +0 -0
  89. package/public/images/series/veil-of-shadows/poster.jpg +0 -0
  90. package/public/images/series/wanhua-sin/poster-2.jpg +0 -0
  91. package/public/images/series/wanhua-sin/poster.jpg +0 -0
  92. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj10yogj62u03s0npe02.jpg +0 -0
  93. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj2pdvyj61li25jkjl02.jpg +0 -0
  94. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj71177j62672w9e8302.jpg +0 -0
  95. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj7twpfj62192197rw02.jpg +0 -0
  96. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj8jfu8j62c03401kx02.jpg +0 -0
  97. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj9dhdsj622k2l77wh02.jpg +0 -0
  98. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkja31s4j62bw3407r802.jpg +0 -0
  99. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjcf331j61402ey1kx02.jpg +0 -0
  100. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjdx8gqj62602pihdt02.jpg +0 -0
  101. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjfvxnzj62432n57wh02.jpg +0 -0
  102. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixniv2yj627y4fxb2b02.jpg +0 -0
  103. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixsvbdaj625k2vfu1102.jpg +0 -0
  104. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixxxdaoj62c03501l202.jpg +0 -0
  105. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixzeef9j62c0340npd02.jpg +0 -0
  106. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy0vs6tj626t4rbhdu02.jpg +0 -0
  107. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy21wwuj62bt34hx6q02.jpg +0 -0
  108. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly4wr85j62z629y7wh02.jpg +0 -0
  109. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly67oilj623u2t5npd02.jpg +0 -0
  110. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly9qtw8j62c0340u0x02.jpg +0 -0
  111. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyem4m4j63402c01kz02.jpg +0 -0
  112. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlygkoq4j627s2yeavd02.jpg +0 -0
  113. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyjh0daj62bw340nm102.jpg +0 -0
  114. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlylejlhj62bw3401kx02.jpg +0 -0
  115. package/public/images/weibo-update/today/0040jbadgy1igaiaa8b7fj627j2y67wh02.jpg +0 -0
  116. package/public/images/weibo-update/today/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
  117. package/public/images/weibo-update/today/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
  118. package/public/images/weibo-update/today/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
  119. package/public/images/weibo-update/today/0040jbadgy1igaiapyartj62bw340hdt02.jpg +0 -0
  120. package/public/images/weibo-update/today/0040jbadgy1igaibkwu4ij61vy2iq1kx02.jpg +0 -0
  121. package/public/images/weibo-update/today/0040jbadgy1igaic1fvkpj61u92eo1ky02.jpg +0 -0
  122. package/public/images/weibo-update/today/0040jbadgy1igaic3cruhj62822ywb2902.jpg +0 -0
  123. package/public/manifest.json +25 -0
  124. package/public/mcp-logo-128.png +0 -0
  125. package/public/mcp-logo-64.png +0 -0
  126. package/public/mcp-logo.svg +16 -0
  127. package/server/agent/agentCore.js +542 -74
  128. package/server/agent/appPaths.js +1 -0
  129. package/server/agent/audioPlayer.js +4 -2
  130. package/server/agent/citations.js +294 -0
  131. package/server/agent/clarificationAnalytics.js +477 -0
  132. package/server/agent/clarifyingQuestions.js +1089 -0
  133. package/server/agent/cli.js +549 -125
  134. package/server/agent/commandFeatures.js +1 -0
  135. package/server/agent/commandRegistry.js +946 -879
  136. package/server/agent/commands.js +546 -272
  137. package/server/agent/config.js +581 -412
  138. package/server/agent/contextCompactor.js +2 -2
  139. package/server/agent/debate.js +235 -0
  140. package/server/agent/eventBus.js +3 -0
  141. package/server/agent/feedbackLearning.js +217 -0
  142. package/server/agent/llmClient.js +512 -394
  143. package/server/agent/mcpClient.js +182 -32
  144. package/server/agent/memory.js +283 -224
  145. package/server/agent/modelManager.js +79 -39
  146. package/server/agent/profiles.js +178 -0
  147. package/server/agent/publicTools.js +22 -0
  148. package/server/agent/queryDetails.js +68 -8
  149. package/server/agent/quizData.js +398 -384
  150. package/server/agent/research.js +121 -79
  151. package/server/agent/sanitizer.js +104 -13
  152. package/server/agent/session.js +319 -11
  153. package/server/agent/subagent.js +8 -3
  154. package/server/agent/suggestionTui.js +294 -0
  155. package/server/agent/suggestions.js +69 -3
  156. package/server/agent/telemetry.js +119 -0
  157. package/server/agent/terminalImage.js +146 -27
  158. package/server/agent/theme.js +17 -0
  159. package/server/agent/toolRouter.js +16 -1
  160. package/server/agent/tools.js +424 -225
  161. package/server/agent/ui.js +486 -64
  162. package/server/agent/version.js +2 -4
  163. package/server/agent/webSearch.js +111 -11
  164. package/server/agent/youtubeAuth.js +24 -6
  165. package/server/apiRouter.js +4222 -0
  166. package/server/mcp/data/discography.js +25 -16
  167. package/server/mcp/data/gallery.js +308 -20
  168. package/server/mcp/data/profile.js +74 -44
  169. package/server/mcp/data/timeline.js +1 -1
  170. package/server/mcp/data/works.js +51 -17
  171. package/server/mcp/dataAdapter.js +5 -3
  172. package/server/mcp/handler.js +5 -11
  173. package/server/mcp/prompts.js +86 -0
  174. package/server/mcp/security.js +18 -15
  175. package/server/mcp/server.js +7 -2
  176. package/server/mcp/tools.js +66 -2
  177. package/server/web-chat/core/browserLauncher.js +18 -0
  178. package/server/web-chat/core/connection.js +194 -0
  179. package/server/web-chat/core/conversationContext.js +82 -0
  180. package/server/web-chat/core/directCatalogEngine.js +196 -0
  181. package/server/web-chat/core/dramaKnowledge.js +142 -0
  182. package/server/web-chat/core/errorHandler.js +51 -0
  183. package/server/web-chat/core/followUpEngine.js +182 -0
  184. package/server/web-chat/core/futureEventGuard.js +80 -0
  185. package/server/web-chat/core/memorySnapshot.js +12 -0
  186. package/server/web-chat/core/security.js +146 -0
  187. package/server/web-chat/core/toolInspector.js +52 -0
  188. package/server/web-chat/router.js +120 -0
  189. package/server/web-chat/routes/chatRoute.js +397 -0
  190. package/server/web-chat/routes/clarifyConfigRoute.js +98 -0
  191. package/server/web-chat/routes/clarifyRoute.js +83 -0
  192. package/server/web-chat/routes/compareRoute.js +79 -0
  193. package/server/web-chat/routes/exportRoute.js +59 -0
  194. package/server/web-chat/routes/feedbackRoute.js +57 -0
  195. package/server/web-chat/routes/newsRoute.js +33 -0
  196. package/server/web-chat/routes/quizRoute.js +11 -0
  197. package/server/web-chat/routes/staticRoute.js +103 -0
  198. package/server/web-chat/routes/statusRoute.js +52 -0
  199. package/server/web-chat/routes/suggestRoute.js +77 -0
  200. package/server/web-chat/routes/timelineRoute.js +34 -0
  201. package/server/web-chat/routes/verifyRoute.js +57 -0
  202. package/server/web-chat/server.js +116 -0
  203. package/server/web-chat/ui/htmlBuilder.js +91 -0
  204. package/server/web-chat/ui/previews/agentBubble.preview.html +134 -0
  205. package/server/web-chat/ui/scripts/agentBubbleSystem.js +320 -0
  206. package/server/web-chat/ui/scripts/client.js +8397 -0
  207. package/server/web-chat/ui/scripts/kikuDb.js +177 -0
  208. package/server/web-chat/ui/scripts/suggestionsData.js +118 -0
  209. package/server/web-chat/ui/scripts/webchatI18n.js +291 -0
  210. package/server/web-chat/ui/styles/main.css +8568 -0
  211. package/server/web-chat/ui/templates/body.html +796 -0
  212. package/server/webChatHtml.js +1 -0
  213. package/server/webChatServer.js +6 -0
  214. package/src/data/discographyData.js +892 -866
  215. package/src/data/galleryData.js +1805 -561
  216. package/src/data/profileData.js +79 -2
  217. package/src/data/socialUpdatesData.js +106 -47
  218. package/src/data/timelineData.js +1 -1
  219. package/src/data/worksData.js +25 -4
  220. package/tokens.css +97 -0
  221. package/server/agent/updateManager.js +0 -456
@@ -0,0 +1,294 @@
1
+ import readline from 'node:readline';
2
+ import { C, getCategoryBadge, getStringDisplayWidth, getFullWidth } from './ui.js';
3
+ import { isColorDisabled } from './theme.js';
4
+
5
+ export const SUGGESTION_STUDIO_TABS = Object.freeze([
6
+ 'ALL',
7
+ 'DRAMA',
8
+ 'MUSIC',
9
+ 'FASHION',
10
+ 'MILESTONE',
11
+ 'ANALYSIS',
12
+ 'TRIVIA'
13
+ ]);
14
+
15
+ export function renderSuggestionStudioView({
16
+ suggestions = [],
17
+ selectedIndex = 0,
18
+ selectedIds = new Set(),
19
+ activeCategory = 'ALL',
20
+ termWidth = getFullWidth(),
21
+ } = {}) {
22
+ const width = Math.max(50, Math.min(termWidth - 2, 90));
23
+ const innerWidth = width - 4;
24
+ const rows = [];
25
+
26
+ const filtered = activeCategory === 'ALL'
27
+ ? suggestions
28
+ : suggestions.filter(s => String(s.category || '').toUpperCase() === activeCategory);
29
+
30
+ const headerBg = isColorDisabled() ? '' : '\x1b[48;2;65;20;42m';
31
+ const headerFg = isColorDisabled() ? '' : '\x1b[38;2;255;220;235m';
32
+
33
+ rows.push(` ${headerBg}${headerFg}${C.bold} 🌸 KIKU SUGGESTION STUDIO ${C.reset}`);
34
+ rows.push(`${C.dim} Interactive topic explorer • Select multiple with [Space] and launch with [Enter]${C.reset}`);
35
+ rows.push('');
36
+
37
+ const tabTokens = SUGGESTION_STUDIO_TABS.map(tab => {
38
+ const badge = tab === 'ALL' ? { icon: '✨', label: 'ALL', bg: '\x1b[48;2;60;25;40m', color: C.sakura } : getCategoryBadge(tab);
39
+ const isCurrent = tab === activeCategory;
40
+ if (isColorDisabled()) {
41
+ return isCurrent ? `[${badge.icon} ${badge.label}]` : `${badge.icon} ${badge.label}`;
42
+ }
43
+ if (isCurrent) {
44
+ return `\x1b[48;2;212;83;110m\x1b[38;2;255;255;255m\x1b[1m ${badge.icon} ${badge.label} \x1b[0m`;
45
+ }
46
+ return `${badge.bg || '\x1b[48;2;36;32;44m'}\x1b[38;2;180;180;200m ${badge.icon} ${badge.label} \x1b[0m`;
47
+ });
48
+ rows.push(` ${tabTokens.join(' ')}`);
49
+ rows.push('');
50
+
51
+ if (filtered.length === 0) {
52
+ rows.push(` ${C.dim}No suggestions found under ${activeCategory}. Press [Tab] to switch category.${C.reset}`);
53
+ rows.push('');
54
+ } else {
55
+ for (let i = 0; i < filtered.length; i++) {
56
+ const item = filtered[i];
57
+ const isSelected = i === selectedIndex;
58
+ const isChecked = selectedIds.has(item.id);
59
+ const badge = getCategoryBadge(item.category);
60
+
61
+ const pointer = isSelected ? `${C.bold}${C.gold}❯${C.reset}` : ' ';
62
+ const checkMark = isChecked
63
+ ? (isColorDisabled() ? '[◉]' : '\x1b[48;2;20;80;40m\x1b[38;2;80;240;140m\x1b[1m[◉]\x1b[0m')
64
+ : (isColorDisabled() ? '[ ]' : '\x1b[48;2;35;35;45m\x1b[38;2;160;160;180m[ ]\x1b[0m');
65
+ const numLabel = `${item.id}.`;
66
+ const catBadge = isColorDisabled()
67
+ ? `[${badge.icon} ${badge.label}]`
68
+ : `${badge.bg}${badge.color}${C.bold} ${badge.icon} ${badge.label} ${C.reset}`;
69
+
70
+ let qText = item.question;
71
+ const availableTextWidth = Math.max(10, innerWidth - 20);
72
+ if (getStringDisplayWidth(qText) > availableTextWidth) {
73
+ let truncated = '';
74
+ for (const char of qText) {
75
+ if (getStringDisplayWidth(truncated + char) > availableTextWidth - 2) break;
76
+ truncated += char;
77
+ }
78
+ qText = `${truncated}…`;
79
+ }
80
+
81
+ const qStyled = isSelected ? `${C.bold}${C.white}${qText}${C.reset}` : `${C.white}${qText}${C.reset}`;
82
+ rows.push(` ${pointer} ${checkMark} ${C.bold}${numLabel}${C.reset} ${catBadge} ${qStyled}`);
83
+
84
+ if (isSelected) {
85
+ const descText = item.description || 'Explore deep multi-agent insights and citations.';
86
+ const cleanDesc = descText.length > innerWidth - 6 ? `${descText.slice(0, innerWidth - 9)}…` : descText;
87
+ const focusTag = isColorDisabled() ? 'Focus:' : '\x1b[48;2;45;20;32m\x1b[38;2;255;190;210m Focus: \x1b[0m';
88
+ rows.push(` ${focusTag} ${C.slate}${cleanDesc}${C.reset}`);
89
+
90
+ const toolsStr = Array.isArray(item.tools) ? item.tools.join(', ') : 'search_works';
91
+ const depthStr = item.depth || 'Deep Analysis';
92
+ const estStr = `~${item.estimatedSeconds || 2}s`;
93
+
94
+ const toolTag = isColorDisabled() ? 'Tools:' : '\x1b[48;2;20;40;55m\x1b[38;2;120;220;255m Tools: \x1b[0m';
95
+ const estTag = isColorDisabled() ? 'Est:' : '\x1b[48;2;45;38;20m\x1b[38;2;255;210;80m Est: \x1b[0m';
96
+ const depthTag = isColorDisabled() ? 'Depth:' : '\x1b[48;2;20;50;35m\x1b[38;2;100;240;150m Depth: \x1b[0m';
97
+
98
+ rows.push(` ${toolTag} ${C.slate}${toolsStr}${C.reset} ${estTag} ${C.slate}${estStr}${C.reset} ${depthTag} ${C.slate}${depthStr}${C.reset}`);
99
+ }
100
+ }
101
+ }
102
+
103
+ rows.push('');
104
+ const checkedCount = selectedIds.size;
105
+ const launchLabel = checkedCount > 0 ? `Launch Batch (${checkedCount})` : 'Launch (1)';
106
+ const navBar = isColorDisabled()
107
+ ? `[↑/↓] Nav • [Space] Select • [Enter] ${launchLabel} • [Tab] Tab • [A] All • [Esc] Cancel`
108
+ : `\x1b[48;2;36;28;44m\x1b[38;2;225;210;245m [↑/↓] Nav • [Space] Select • [Enter] ${launchLabel} • [Tab] Tab • [A] All • [Esc] Cancel \x1b[0m`;
109
+ rows.push(` ${navBar}`);
110
+ rows.push(` ${C.dim}[1-9] Quick Toggle • [R] Refresh • [Q] Exit${C.reset}`);
111
+
112
+ return rows;
113
+ }
114
+
115
+
116
+ export async function promptInteractiveSuggestion(suggestions = [], options = {}) {
117
+ const stdin = options.stdin || process.stdin;
118
+ const stdout = options.stdout || process.stdout;
119
+
120
+
121
+ if (!stdin.isTTY || !stdout.isTTY || options.plain || process.env.KIKU_PLAIN === 'true') {
122
+ return { action: 'display', targets: null };
123
+ }
124
+
125
+ return new Promise((resolve) => {
126
+ let selectedIndex = 0;
127
+ const selectedIds = new Set();
128
+ let currentTabIdx = 0;
129
+ let activeCategory = SUGGESTION_STUDIO_TABS[0];
130
+ let isCleanedUp = false;
131
+ let renderedRowCount = 0;
132
+
133
+ const getFiltered = () => {
134
+ return activeCategory === 'ALL'
135
+ ? suggestions
136
+ : suggestions.filter(s => String(s.category || '').toUpperCase() === activeCategory);
137
+ };
138
+
139
+ const cleanup = () => {
140
+ if (isCleanedUp) return;
141
+ isCleanedUp = true;
142
+ stdin.removeListener('keypress', handleKey);
143
+ try { stdin.setRawMode(false); } catch {}
144
+
145
+ if (renderedRowCount > 0) {
146
+ readline.cursorTo(stdout, 0);
147
+ readline.moveCursor(stdout, 0, -renderedRowCount + 1);
148
+ readline.clearScreenDown(stdout);
149
+ }
150
+ };
151
+
152
+ const render = () => {
153
+ if (isCleanedUp) return;
154
+ const termWidth = stdout.columns || 80;
155
+ const frameRows = renderSuggestionStudioView({
156
+ suggestions,
157
+ selectedIndex,
158
+ selectedIds,
159
+ activeCategory,
160
+ termWidth,
161
+ });
162
+
163
+ if (renderedRowCount > 0) {
164
+ readline.cursorTo(stdout, 0);
165
+ readline.moveCursor(stdout, 0, -renderedRowCount + 1);
166
+ readline.clearScreenDown(stdout);
167
+ }
168
+
169
+ stdout.write(frameRows.join('\n') + '\n');
170
+ renderedRowCount = frameRows.length;
171
+ };
172
+
173
+ const handleKey = (str, key) => {
174
+ if (isCleanedUp) return;
175
+
176
+
177
+ if ((key?.ctrl && key?.name === 'c') || key?.name === 'escape' || str === 'q' || str === 'Q') {
178
+ cleanup();
179
+ return resolve({ action: 'cancel' });
180
+ }
181
+
182
+ const filtered = getFiltered();
183
+
184
+
185
+ if (key?.name === 'up' || str === 'k') {
186
+ if (filtered.length > 0) {
187
+ selectedIndex = (selectedIndex - 1 + filtered.length) % filtered.length;
188
+ render();
189
+ }
190
+ return;
191
+ }
192
+
193
+
194
+ if (key?.name === 'down' || str === 'j') {
195
+ if (filtered.length > 0) {
196
+ selectedIndex = (selectedIndex + 1) % filtered.length;
197
+ render();
198
+ }
199
+ return;
200
+ }
201
+
202
+
203
+ if (key?.name === 'tab' || key?.name === 'right') {
204
+ currentTabIdx = (currentTabIdx + 1) % SUGGESTION_STUDIO_TABS.length;
205
+ activeCategory = SUGGESTION_STUDIO_TABS[currentTabIdx];
206
+ selectedIndex = 0;
207
+ render();
208
+ return;
209
+ }
210
+ if (key?.name === 'left') {
211
+ currentTabIdx = (currentTabIdx - 1 + SUGGESTION_STUDIO_TABS.length) % SUGGESTION_STUDIO_TABS.length;
212
+ activeCategory = SUGGESTION_STUDIO_TABS[currentTabIdx];
213
+ selectedIndex = 0;
214
+ render();
215
+ return;
216
+ }
217
+
218
+
219
+ if (key?.name === 'space' || str === ' ') {
220
+ const item = filtered[selectedIndex];
221
+ if (item) {
222
+ if (selectedIds.has(item.id)) {
223
+ selectedIds.delete(item.id);
224
+ } else {
225
+ selectedIds.add(item.id);
226
+ }
227
+ render();
228
+ }
229
+ return;
230
+ }
231
+
232
+
233
+ if (str === 'a' || str === 'A') {
234
+ const allChecked = filtered.every(s => selectedIds.has(s.id));
235
+ if (allChecked) {
236
+ filtered.forEach(s => selectedIds.delete(s.id));
237
+ } else {
238
+ filtered.forEach(s => selectedIds.add(s.id));
239
+ }
240
+ render();
241
+ return;
242
+ }
243
+
244
+
245
+ if (str === 'r' || str === 'R') {
246
+ cleanup();
247
+ return resolve({ action: 'refresh' });
248
+ }
249
+
250
+
251
+ if (/^[1-9]$/.test(str)) {
252
+ const num = Number.parseInt(str, 10);
253
+ const matchIdx = filtered.findIndex(s => s.id === num);
254
+ if (matchIdx >= 0) {
255
+ selectedIndex = matchIdx;
256
+ if (selectedIds.has(num)) {
257
+ selectedIds.delete(num);
258
+ } else {
259
+ selectedIds.add(num);
260
+ }
261
+ render();
262
+ }
263
+ return;
264
+ }
265
+
266
+
267
+ if (key?.name === 'return' || str === '\r' || str === '\n') {
268
+ cleanup();
269
+ let targetStr = '';
270
+ if (selectedIds.size > 0) {
271
+ targetStr = Array.from(selectedIds).sort((a, b) => a - b).join(',');
272
+ } else {
273
+ const item = filtered[selectedIndex] || suggestions[0];
274
+ targetStr = item ? String(item.id) : '1';
275
+ }
276
+ return resolve({ action: 'run', targets: targetStr });
277
+ }
278
+ };
279
+
280
+
281
+ readline.emitKeypressEvents(stdin);
282
+ try { stdin.setRawMode(true); } catch {}
283
+ stdin.on('keypress', handleKey);
284
+
285
+ if (options.signal) {
286
+ options.signal.addEventListener('abort', () => {
287
+ cleanup();
288
+ resolve({ action: 'cancel' });
289
+ }, { once: true });
290
+ }
291
+
292
+ render();
293
+ });
294
+ }
@@ -16,44 +16,89 @@ export const CURATED_SUGGESTION_TEMPLATES = Object.freeze([
16
16
  category: 'DRAMA',
17
17
  question: "What are Ju Jingyi's upcoming 2026/2025 drama projects, characters, and release platforms?",
18
18
  description: "Detailed breakdown of upcoming dramas, character names, and broadcasting platforms.",
19
+ tools: ['search_works', 'web_search'],
20
+ depth: 'Deep Analysis',
21
+ estimatedSeconds: 3,
19
22
  },
20
23
  {
21
24
  category: 'MUSIC',
22
25
  question: "What are the most popular OSTs and solo singles released by Ju Jingyi, and what are their YouTube links?",
23
26
  description: "Discography highlights from In Blossom, Ruyi Pavilion, and Legend of Yunxi with video links.",
27
+ tools: ['search_discography', 'search_videos'],
28
+ depth: 'Media Scan',
29
+ estimatedSeconds: 2,
24
30
  },
25
31
  {
26
32
  category: 'FASHION',
27
33
  question: "What are Ju Jingyi's major luxury brand ambassadorships and global fashion partnerships in 2025-2026?",
28
34
  description: "Overview of Versace Global Brand Ambassador role, Maybelline, and top endorsements.",
35
+ tools: ['get_profile_and_fandom', 'search_works'],
36
+ depth: 'Portfolio Scan',
37
+ estimatedSeconds: 2,
29
38
  },
30
39
  {
31
40
  category: 'MILESTONE',
32
41
  question: "How did Ju Jingyi achieve back-to-back #1 victories in the SNH48 General Elections and transition to her solo studio?",
33
42
  description: "Chronological analysis of the SNH48 Hall of Fame elevation and independent studio milestones.",
43
+ tools: ['get_career_timeline', 'search_awards'],
44
+ depth: 'Historical Deep Dive',
45
+ estimatedSeconds: 3,
34
46
  },
35
47
  {
36
48
  category: 'ANALYSIS',
37
49
  question: "What acting awards and cultural influence honors has Ju Jingyi received across Chinese television and media?",
38
50
  description: "Awards summary spanning Weibo Night, Baidu Feast, Tencent Starlight, and drama recognitions.",
51
+ tools: ['search_awards', 'get_career_timeline'],
52
+ depth: 'Accolades Breakdown',
53
+ estimatedSeconds: 2,
39
54
  },
40
55
  {
41
56
  category: 'TRIVIA',
42
57
  question: "What is the meaning and background of Ju Jingyi's fan community name 'Mìjú' (蜜橘) and official fandom colors?",
43
58
  description: "Biographical details, fan community history, official colors, and community traditions.",
59
+ tools: ['get_profile_and_fandom'],
60
+ depth: 'Fandom Trivia',
61
+ estimatedSeconds: 1,
44
62
  },
45
63
  {
46
64
  category: 'DRAMA',
47
65
  question: "How did Ju Jingyi's performance in 'In Blossom' (花间令) as Yang Caiwei receive audience acclaim and streaming milestones?",
48
66
  description: "Deep dive into character duality, critical reception, and viewership records.",
67
+ tools: ['search_works', 'search_awards'],
68
+ depth: 'Performance Review',
69
+ estimatedSeconds: 2,
49
70
  },
50
71
  {
51
72
  category: 'MUSIC',
52
73
  question: "What are Ju Jingyi's live stage performances and music videos with the highest viewership?",
53
74
  description: "Top viral stage performances, traditional guofeng choreography, and music video releases.",
75
+ tools: ['search_discography', 'search_videos'],
76
+ depth: 'Visual Stages',
77
+ estimatedSeconds: 2,
54
78
  },
55
79
  ]);
56
80
 
81
+ export async function mapConcurrent(items, limit, fn) {
82
+ const results = new Array(items.length);
83
+ const maxLimit = Math.max(1, limit || 1);
84
+ let nextIndex = 0;
85
+
86
+ const workers = Array.from({ length: Math.min(maxLimit, items.length) }, async () => {
87
+ while (nextIndex < items.length) {
88
+ const currentIndex = nextIndex++;
89
+ try {
90
+ const val = await fn(items[currentIndex], currentIndex);
91
+ results[currentIndex] = { status: 'fulfilled', value: val };
92
+ } catch (reason) {
93
+ results[currentIndex] = { status: 'rejected', reason };
94
+ }
95
+ }
96
+ });
97
+
98
+ await Promise.all(workers);
99
+ return results;
100
+ }
101
+
57
102
  export class SuggestionEngine {
58
103
  constructor(options = {}) {
59
104
  this.client = options.client || null;
@@ -89,6 +134,9 @@ export class SuggestionEngine {
89
134
  category: item.category,
90
135
  question: item.question,
91
136
  description: item.description,
137
+ tools: item.tools || ['search_works'],
138
+ depth: item.depth || 'Standard',
139
+ estimatedSeconds: item.estimatedSeconds || 2,
92
140
  source: 'catalog',
93
141
  }));
94
142
 
@@ -98,6 +146,11 @@ export class SuggestionEngine {
98
146
  async generateSuggestions(options = {}) {
99
147
  const limit = Math.min(Math.max(options.limit || 5, 2), 8);
100
148
  const topic = (options.topic || '').trim();
149
+ const lang = String(options.lang || 'en').toLowerCase();
150
+ const langNames = { th: 'Thai', zh: 'Simplified Chinese', en: 'English', ja: 'Japanese', ko: 'Korean' };
151
+ const langInstruction = langNames[lang]
152
+ ? `Write every question and description entirely in ${langNames[lang]} (no English except proper nouns like drama titles).`
153
+ : '';
101
154
 
102
155
  if (!this.client || typeof this.client.chat !== 'function') {
103
156
  return this.generateCatalogSuggestions(options);
@@ -111,6 +164,7 @@ export class SuggestionEngine {
111
164
  const systemPrompt = `You are the Ju Jingyi intelligence coordinator for Kiku Agent.
112
165
  Generate ${limit} distinct, highly specific, and exciting research questions about Ju Jingyi that fans or researchers would want to explore.
113
166
  ${topicInstruction}
167
+ ${langInstruction}
114
168
 
115
169
  Output ONLY a valid JSON array of objects with the exact schema:
116
170
  [
@@ -123,7 +177,7 @@ Output ONLY a valid JSON array of objects with the exact schema:
123
177
  Do not include markdown code fences or any text other than the JSON array.`;
124
178
 
125
179
  const controller = new AbortController();
126
- const timeoutId = setTimeout(() => controller.abort(), 6000);
180
+ const timeoutId = setTimeout(() => controller.abort(), 30000);
127
181
  if (options.signal) {
128
182
  options.signal.addEventListener('abort', () => controller.abort(), { once: true });
129
183
  }
@@ -153,6 +207,9 @@ Do not include markdown code fences or any text other than the JSON array.`;
153
207
  category: String(item.category || 'ANALYSIS').toUpperCase(),
154
208
  question: String(item.question || '').trim(),
155
209
  description: String(item.description || '').trim(),
210
+ tools: Array.isArray(item.tools) && item.tools.length > 0 ? item.tools : ['search_works'],
211
+ depth: item.depth || 'Deep Analysis',
212
+ estimatedSeconds: item.estimatedSeconds || 3,
156
213
  source: 'ai_model',
157
214
  })).filter(s => s.question.length > 5);
158
215
 
@@ -236,8 +293,17 @@ Do not include markdown code fences or any text other than the JSON array.`;
236
293
  })),
237
294
  });
238
295
 
296
+ const isLocalProvider = Boolean(
297
+ this.client?.provider === 'lmstudio' ||
298
+ this.client?.provider === 'ollama' ||
299
+ this.client?.isLocal ||
300
+ this.client?.getProviderInfo?.()?.isLocal
301
+ );
302
+ const defaultLimit = isLocalProvider ? 1 : this.maxWorkers;
303
+ const concurrency = Math.min(Math.max(options.concurrency || defaultLimit, 1), this.maxWorkers || 5);
304
+
239
305
  try {
240
- const settled = await Promise.allSettled(tasks.map(async (task, index) => {
306
+ const settled = await mapConcurrent(tasks, concurrency, async (task, index) => {
241
307
  const workerId = `worker_${index + 1}`;
242
308
  const workerLabel = `[Worker ${index + 1}] ${task.category || 'Research'}`;
243
309
 
@@ -298,7 +364,7 @@ Do not include markdown code fences or any text other than the JSON array.`;
298
364
  toolCalls: result.toolCalls,
299
365
  iterations: result.iterations,
300
366
  };
301
- }));
367
+ });
302
368
 
303
369
  const results = settled.map((entry, index) => {
304
370
  if (entry.status === 'fulfilled') return entry.value;
@@ -0,0 +1,119 @@
1
+ export const MODEL_PRICING = {
2
+ lmstudio: {
3
+ default: { in: 0.0, out: 0.0 },
4
+ 'google/gemma-4-e4b': { in: 0.0, out: 0.0 },
5
+ 'google/gemma-4-e4b-Q4_K_M': { in: 0.0, out: 0.0 },
6
+ 'google/gemma-4-e4b-Q6_K': { in: 0.0, out: 0.0 },
7
+ 'google/gemma-4-e4b-Q8_0': { in: 0.0, out: 0.0 }
8
+ },
9
+ groq: {
10
+ default: { in: 0.15, out: 0.60 },
11
+ 'openai/gpt-oss-120b': { in: 0.15, out: 0.60 },
12
+ 'openai/gpt-oss-20b': { in: 0.075, out: 0.30 }
13
+ },
14
+ deepseek: {
15
+ default: { in: 0.30, out: 1.20 },
16
+ 'deepseek-v4-flash': { in: 0.27, out: 1.10 },
17
+ 'deepseek-v4-pro': { in: 0.99, out: 2.19 }
18
+ },
19
+ openai: {
20
+ default: { in: 0.20, out: 1.20 },
21
+ 'gpt-5.6-luna': { in: 0.20, out: 1.20 },
22
+ 'gpt-5.6-terra': { in: 2.00, out: 6.00 },
23
+ 'gpt-5.6-sol': { in: 4.00, out: 12.00 }
24
+ },
25
+ gemini: {
26
+ default: { in: 0.75, out: 3.00 },
27
+ 'gemini-3.7-flash': { in: 0.75, out: 3.00 },
28
+ 'gemini-3.5-flash-lite': { in: 0.30, out: 1.20 },
29
+ 'gemini-3.6-flash': { in: 0.75, out: 3.00 }
30
+ },
31
+ openrouter: {
32
+ default: { in: 0.20, out: 0.80 },
33
+ 'openrouter/auto': { in: 0.20, out: 0.80 }
34
+ },
35
+ custom: {
36
+ default: { in: 0.0, out: 0.0 }
37
+ }
38
+ };
39
+
40
+
41
+ export function estimateTokens(text) {
42
+ if (!text || typeof text !== 'string') return 0;
43
+ const str = text.trim();
44
+ if (!str) return 0;
45
+
46
+
47
+ const cjkAndThai = str.match(/[\u4e00-\u9fa5\u0e00-\u0e7f\u3040-\u30ff\uac00-\ud7af]/g) || [];
48
+ const nonCjk = str.replace(/[\u4e00-\u9fa5\u0e00-\u0e7f\u3040-\u30ff\uac00-\ud7af]/g, ' ');
49
+ const words = nonCjk.split(/\s+/).filter(Boolean);
50
+
51
+
52
+ const cjkTokens = Math.ceil(cjkAndThai.length * 1.25);
53
+
54
+
55
+ const wordTokens = Math.ceil(words.length * 1.3);
56
+
57
+ return Math.max(1, cjkTokens + wordTokens);
58
+ }
59
+
60
+
61
+ export function calculateTokenCost(providerOrTokens, modelOrProvider, promptTokensOrModel = 0, completionTokens = 0) {
62
+ let pId, mId, prompt, completion;
63
+ if (typeof providerOrTokens === 'object' && providerOrTokens !== null) {
64
+ prompt = Number(providerOrTokens.prompt || 0);
65
+ completion = Number(providerOrTokens.completion || 0);
66
+ pId = String(modelOrProvider || 'lmstudio').toLowerCase().trim();
67
+ mId = String(promptTokensOrModel || 'default').trim();
68
+ } else {
69
+ pId = String(providerOrTokens || 'lmstudio').toLowerCase().trim();
70
+ mId = String(modelOrProvider || 'default').trim();
71
+ prompt = Number(promptTokensOrModel || 0);
72
+ completion = Number(completionTokens || 0);
73
+ }
74
+
75
+ const providerPricing = MODEL_PRICING[pId] || MODEL_PRICING.custom;
76
+ const rates = providerPricing[mId] || providerPricing.default || { in: 0, out: 0 };
77
+
78
+ const inputCost = (prompt * rates.in) / 1_000_000;
79
+ const outputCost = (completion * rates.out) / 1_000_000;
80
+
81
+ return Number((inputCost + outputCost).toFixed(6));
82
+ }
83
+
84
+ /**
85
+ * Human-readable formatted cost in USD
86
+ */
87
+ export function formatCostUsd(amount, isLocal = false) {
88
+ const num = Number(amount || 0);
89
+ if (isLocal || num === 0) {
90
+ return '$0.00 (Local)';
91
+ }
92
+ if (num < 0.0001) {
93
+ return '<$0.0001';
94
+ }
95
+ return `$${num.toFixed(4)}`;
96
+ }
97
+
98
+ /**
99
+ * Format token count with thousand separators
100
+ */
101
+ export function formatTokenCount(count) {
102
+ const num = Number(count || 0);
103
+ return num.toLocaleString('en-US');
104
+ }
105
+
106
+ /**
107
+ * Creates a normalized token usage object
108
+ */
109
+ export function createTokenUsage({ prompt = 0, completion = 0, reasoning = 0 } = {}) {
110
+ const p = Math.max(0, Math.round(Number(prompt) || 0));
111
+ const c = Math.max(0, Math.round(Number(completion) || 0));
112
+ const r = Math.max(0, Math.round(Number(reasoning) || 0));
113
+ return {
114
+ prompt: p,
115
+ completion: c,
116
+ reasoning: r,
117
+ total: p + c
118
+ };
119
+ }