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
@@ -4,16 +4,21 @@ import { ensureYouTubeCookies, resetCookieCheck, getCookieStatus } from './youtu
4
4
  import { AgentMemory } from './memory.js';
5
5
  import {
6
6
  C, UI_ICONS, printSolidPanel, formatTerminalMarkdown, formatMarkdownLines, randomFanName, createProgressRunner, wrapText, getFullWidth, getWelcomeHeroLines, sanitizeTerminalText,
7
- formatRichCardLines, printRichCard, printRichStatusCard, formatStatusBadge, formatRichTable, getStringDisplayWidth
7
+ formatRichCardLines, printRichCard, printRichStatusCard, formatStatusBadge, formatRichTable, getStringDisplayWidth,
8
+ formatCareerTimelineGraphic, formatDebateMatrix, formatWorkerProgressMatrix, formatExecutiveFindings, getCategoryBadge
8
9
  } from './ui.js';
10
+ import { promptInteractiveSuggestion } from './suggestionTui.js';
11
+ import { DebateOrchestrator } from './debate.js';
12
+ import { timelineData } from '../../src/data/timelineData.js';
9
13
  import { COMMAND_NAMES, getCommandDetails, findCommandDefinition } from './commandRegistry.js';
10
14
  import { THEMES, THEME_NAMES, ansiBackground, ansiForeground, getThemeName, setTheme } from './theme.js';
11
15
  import { getGpuMetrics, formatProgressBar } from './gpuMetrics.js';
12
16
  import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
13
- import { UpdateError, UpdateManager, UPDATE_CHANNELS } from './updateManager.js';
14
17
  import { QUIZ_QUESTIONS, QUIZ_I18N, LETTER_LABELS, shuffleQuizQuestions } from './quizData.js';
15
18
  import crypto from 'node:crypto';
19
+ import { isAbortError } from './errors.js';
16
20
  import { featureCommandHandlers, aiNarration, languageHint } from './commandFeatures.js';
21
+ import { startWebChatServer, stopWebChatServer, getWebChatServerStatus, openBrowser } from '../webChatServer.js';
17
22
 
18
23
  function levenshteinDistance(a, b) {
19
24
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -138,8 +143,74 @@ export const handlers = {
138
143
  return 'HANDLED';
139
144
  },
140
145
 
146
+ '/web': async (ctx) => {
147
+ const rawAction = (ctx.sub || 'start').toLowerCase();
148
+ const extraPort = Number(ctx.extra || (ctx.sub && /^\d+$/.test(ctx.sub) ? ctx.sub : null)) || 3300;
149
+
150
+ if (rawAction === 'stop' || rawAction === 'close') {
151
+ const stopped = await stopWebChatServer();
152
+ const msg = stopped
153
+ ? ` ${C.emerald}[OK] Standalone web chatbot server stopped.${C.reset}`
154
+ : ` ${C.slate}Standalone web chatbot server was not running.${C.reset}`;
155
+ if (ctx.tuiPrompt?.addViewportLines) ctx.tuiPrompt.addViewportLines(msg);
156
+ else console.log(`\n${msg}\n`);
157
+ return 'HANDLED';
158
+ }
159
+
160
+ if (rawAction === 'status' || rawAction === 'info') {
161
+ const status = getWebChatServerStatus();
162
+ const lines = [
163
+ `• Status: ${status.running ? `${C.emerald}Running${C.reset}` : `${C.slate}Stopped${C.reset}`}`,
164
+ `• URL: ${status.running ? `${C.cyberCyan}${status.url}${C.reset}` : `${C.dim}${status.url}${C.reset}`}`,
165
+ `• Port: ${status.port}`,
166
+ `• Provider: ${status.provider || 'lmstudio'}`,
167
+ `• Model: ${status.model || 'google/gemma-4-e4b'}`,
168
+ ];
169
+ printRichCard({
170
+ title: 'Standalone Web Chatbot Status',
171
+ status: status.running ? 'ok' : 'info',
172
+ lines,
173
+ });
174
+ return 'HANDLED';
175
+ }
176
+
177
+ if (rawAction === 'restart') {
178
+ await stopWebChatServer();
179
+ }
180
+
181
+ const port = extraPort >= 1000 ? extraPort : 3300;
182
+ const res = await startWebChatServer({ port, agent: ctx.agent });
183
+ openBrowser(res.url);
184
+
185
+ const lines = [
186
+ `• Dedicated Web Chatbot started on ${C.bold}${C.cyberCyan}${res.url}${C.reset}`,
187
+ `• Running as an independent web application on Port ${res.port}.`,
188
+ `• Browser opened automatically.`,
189
+ `• To stop the web server, type ${C.cyberCyan}/web stop${C.reset}`,
190
+ ];
191
+
192
+ printRichCard({
193
+ title: 'Standalone Web Chatbot Launched',
194
+ status: 'ok',
195
+ lines,
196
+ });
197
+
198
+ return 'HANDLED';
199
+ },
200
+
201
+ '/web-chat': async (ctx) => handlers['/web'](ctx),
202
+
141
203
  '/details': async (ctx) => {
142
- ctx.printQueryDetails([ctx.sub, ctx.extra].filter(Boolean).join(' '));
204
+ if (typeof ctx.printQueryDetails === 'function') {
205
+ ctx.printQueryDetails([ctx.sub, ctx.extra].filter(Boolean).join(' '));
206
+ return 'HANDLED';
207
+ }
208
+ const { formatQueryDetails, parseDetailsRequest } = await import('./queryDetails.js');
209
+ const reference = [ctx.sub, ctx.extra].filter(Boolean).join(' ');
210
+ const request = parseDetailsRequest(reference);
211
+ const details = ctx.agent?.lastQueryDetails || ctx.agent?._lastQueryDetails || null;
212
+ const formatted = formatQueryDetails(details, request, ctx.agent?.session);
213
+ console.log(`\n${formatted}\n`);
143
214
  return 'HANDLED';
144
215
  },
145
216
 
@@ -219,6 +290,7 @@ export const handlers = {
219
290
  ctx.setIsProcessing?.(true, `Subagent: ${task.slice(0, 24)}`);
220
291
  try {
221
292
  const result = await ctx.agent.runSubagent(task, {
293
+ signal: ctx.signal,
222
294
  onProgress: event => {
223
295
  if (event.type === 'iteration') {
224
296
  console.log(` ${C.dim}[ITERATION ${event.iteration}/${event.maxIterations}]...${C.reset}`);
@@ -231,6 +303,23 @@ export const handlers = {
231
303
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
232
304
  ctx.agent._lastSubagentResult = result;
233
305
 
306
+ ctx.recordQueryDiagnostics?.({
307
+ prompt: task,
308
+ answer: result.answer,
309
+ thinking: result.thinking || '',
310
+ tools: result.toolCalls?.map((tc, idx) => ({
311
+ name: tc.name || 'tool',
312
+ args: tc.args || {},
313
+ result: tc.result || { status: 'ok' },
314
+ duration: tc.duration || 0,
315
+ })) || [],
316
+ status: 'complete',
317
+ iterations: result.iterations || (result.toolCalls?.length ? 1 : 0),
318
+ startedAt: startTime,
319
+ completedAt: Date.now(),
320
+ durationMs: Date.now() - startTime,
321
+ });
322
+
234
323
  const lines = [
235
324
  `${C.bold}${C.emerald}[DONE] Subagent completed in ${elapsed}s with ${result.toolCalls.length} tool calls.${C.reset}`,
236
325
  ''
@@ -257,7 +346,9 @@ export const handlers = {
257
346
  lines,
258
347
  });
259
348
  } catch (error) {
260
- console.log(`\n ${C.red}[SUBAGENT ERROR] ${error.message}${C.reset}\n`);
349
+ if (!isAbortError(error)) {
350
+ console.log(`\n ${C.red}[SUBAGENT ERROR] ${error.message}${C.reset}\n`);
351
+ }
261
352
  } finally {
262
353
  ctx.setIsProcessing?.(false);
263
354
  }
@@ -356,6 +447,7 @@ export const handlers = {
356
447
  ctx.setIsProcessing?.(true, `Research: ${question.slice(0, 24)}`);
357
448
  try {
358
449
  const result = await ctx.agent.runResearch(question, {
450
+ signal: ctx.signal,
359
451
  onProgress: event => {
360
452
  if (event.type === 'worker-start') console.log(` ${C.dim}▸ Worker started: ${event.label}${C.reset}`);
361
453
  if (event.type === 'worker-complete') console.log(` ${C.emerald}[OK] Worker finished: ${event.label}${C.reset}`);
@@ -363,6 +455,24 @@ export const handlers = {
363
455
  });
364
456
 
365
457
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
458
+ const allWorkerTools = (result.reports || []).flatMap(r => r.toolCalls || []).map((tc, idx) => ({
459
+ name: typeof tc === 'string' ? tc : (tc.name || 'tool'),
460
+ args: typeof tc === 'object' && tc.args ? tc.args : {},
461
+ result: typeof tc === 'object' && tc.result ? tc.result : { status: 'ok' },
462
+ duration: typeof tc === 'object' && tc.duration ? tc.duration : 0,
463
+ }));
464
+
465
+ ctx.recordQueryDiagnostics?.({
466
+ prompt: question,
467
+ answer: result.answer,
468
+ tools: allWorkerTools,
469
+ status: 'complete',
470
+ iterations: (result.reports || []).length || 1,
471
+ startedAt: startTime,
472
+ completedAt: Date.now(),
473
+ durationMs: Date.now() - startTime,
474
+ });
475
+
366
476
  const lines = [
367
477
  `${C.bold}${C.emerald}[DONE] Research synthesis completed in ${elapsed}s across 3 workers.${C.reset}`,
368
478
  '',
@@ -378,7 +488,9 @@ export const handlers = {
378
488
  lines
379
489
  });
380
490
  } catch (error) {
381
- console.log(`\n ${C.red}[RESEARCH ERROR] ${error.message}${C.reset}\n`);
491
+ if (!isAbortError(error)) {
492
+ console.log(`\n ${C.red}[RESEARCH ERROR] ${error.message}${C.reset}\n`);
493
+ }
382
494
  } finally {
383
495
  ctx.setIsProcessing?.(false);
384
496
  }
@@ -416,10 +528,28 @@ export const handlers = {
416
528
  const startTime = Date.now();
417
529
  ctx.setIsProcessing?.(true, `Verifying: ${claim.slice(0, 24)}`);
418
530
  try {
419
- const result = await ctx.agent.runResearch(claim, { mode: 'verify' });
531
+ const result = await ctx.agent.runResearch(claim, { mode: 'verify', signal: ctx.signal });
420
532
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
421
533
  const answerStr = String(result.answer || '');
422
534
 
535
+ const allWorkerTools = (result.reports || []).flatMap(r => r.toolCalls || []).map((tc, idx) => ({
536
+ name: typeof tc === 'string' ? tc : (tc.name || 'tool'),
537
+ args: typeof tc === 'object' && tc.args ? tc.args : {},
538
+ result: typeof tc === 'object' && tc.result ? tc.result : { status: 'ok' },
539
+ duration: typeof tc === 'object' && tc.duration ? tc.duration : 0,
540
+ }));
541
+
542
+ ctx.recordQueryDiagnostics?.({
543
+ prompt: `/verify ${claim}`,
544
+ answer: answerStr,
545
+ tools: allWorkerTools,
546
+ status: 'complete',
547
+ iterations: (result.reports || []).length || 1,
548
+ startedAt: startTime,
549
+ completedAt: Date.now(),
550
+ durationMs: Date.now() - startTime,
551
+ });
552
+
423
553
  let verdictBadge = `${C.amber}[UNVERIFIED / AMBIGUOUS]`;
424
554
  if (/VERIFIED|จริง|ถูกต้อง|ยืนยันแล้ว|CONFIRMED/i.test(answerStr) && !/UNVERIFIED|FALSE|เท็จ|ไม่จริง/i.test(answerStr.slice(0, 80))) {
425
555
  verdictBadge = `${C.emerald}${C.bold}[VERIFIED / FACTUAL]${C.reset}`;
@@ -444,7 +574,9 @@ export const handlers = {
444
574
  lines
445
575
  });
446
576
  } catch (error) {
447
- console.log(`\n ${C.red}[VERIFY ERROR] ${error.message}${C.reset}\n`);
577
+ if (!isAbortError(error)) {
578
+ console.log(`\n ${C.red}[VERIFY ERROR] ${error.message}${C.reset}\n`);
579
+ }
448
580
  } finally {
449
581
  ctx.setIsProcessing?.(false);
450
582
  }
@@ -452,11 +584,152 @@ export const handlers = {
452
584
  },
453
585
 
454
586
  '/sources': async (ctx) => {
455
- const latest = ctx.agent.getResearchStatus().lastResult;
456
- printSolidPanel({
457
- title: 'Latest Research Sources',
458
- lines: latest?.sources?.length ? latest.sources.map((url, index) => `${C.gold}[${index + 1}]${C.reset} ${C.cyberCyan}${url}${C.reset}`) : [`${C.dim}No research sources recorded.${C.reset}`],
587
+ if (ctx.agent?.lastCitations?.length > 0) {
588
+ const lines = ctx.agent.lastCitations.map(c => {
589
+ const meta = [c.category, c.year, c.tool].filter(Boolean).join(' • ');
590
+ const urlStr = c.url ? ` ${C.cyberCyan}${c.url}${C.reset}` : '';
591
+ return `${C.gold}[${c.id}]${C.reset} ${C.bold}${c.title}${C.reset} ${C.dim}(${meta})${C.reset}${urlStr}`;
592
+ });
593
+ printSolidPanel({
594
+ title: `Grounded Sources & Citations • ${ctx.agent.lastCitations.length} sources`,
595
+ lines
596
+ });
597
+ return 'HANDLED';
598
+ }
599
+ return handlers['/research']({ ...ctx, sub: 'sources' });
600
+ },
601
+
602
+ '/debate': async (ctx) => {
603
+ const rawAction = (ctx.sub || '').trim();
604
+ const extra = (ctx.extra || '').trim();
605
+ const fullQuery = `${rawAction} ${extra}`.trim();
606
+
607
+ if (!fullQuery || fullQuery === 'help') {
608
+ console.log(`\n ${C.bold}Usage:${C.reset} /debate <topic or claim> | /debate status | /debate last\n`);
609
+ return 'HANDLED';
610
+ }
611
+
612
+ if (rawAction.toLowerCase() === 'status') {
613
+ const status = ctx.agent?.debateOrchestrator?.getStatus?.() || { state: 'idle' };
614
+ printRichCard({
615
+ title: `Debate Status • ${status.state.toUpperCase()}`,
616
+ status: status.state === 'running' ? 'autostart' : 'info',
617
+ lines: [
618
+ status.state === 'running' ? `Active Topic: ${status.topic}` : 'No debate job is currently running.',
619
+ ],
620
+ });
621
+ return 'HANDLED';
622
+ }
623
+
624
+ if (rawAction.toLowerCase() === 'last') {
625
+ const last = ctx.agent?.debateOrchestrator?.getStatus?.()?.lastResult;
626
+ if (!last) {
627
+ console.log(`\n ${C.amber}No previous debate results recorded.${C.reset}\n`);
628
+ return 'HANDLED';
629
+ }
630
+ const lines = formatDebateMatrix(last, getFullWidth());
631
+ console.log(`\n${lines.join('\n')}\n`);
632
+ return 'HANDLED';
633
+ }
634
+
635
+ if (rawAction.toLowerCase() === 'cancel' || rawAction.toLowerCase() === 'abort') {
636
+ const cancelled = ctx.agent?.debateOrchestrator?.cancel?.();
637
+ console.log(`\n ${cancelled ? C.emerald + '[OK] Cancelled active debate job.' : C.amber + 'No active debate job to cancel.'}${C.reset}\n`);
638
+ return 'HANDLED';
639
+ }
640
+
641
+ const debateOrchestrator = ctx.agent?.debateOrchestrator || new DebateOrchestrator({
642
+ client: ctx.agent?.client || ctx.agent?.llmClient,
643
+ executeTool: (name, args) => ctx.agent?.executeTool?.(name, args) || executeTool(name, args),
459
644
  });
645
+ if (ctx.agent) ctx.agent.debateOrchestrator = debateOrchestrator;
646
+
647
+ printRichCard({
648
+ title: 'Initiating Adversarial Fact Debate',
649
+ status: 'autostart',
650
+ lines: [
651
+ `• Topic / Claim: "${fullQuery}"`,
652
+ '• Dispatching Advocate and Skeptic subagents with verified sources...',
653
+ ],
654
+ });
655
+
656
+ const startTime = Date.now();
657
+ ctx.setIsProcessing?.(true, `Debating: ${fullQuery.slice(0, 24)}`);
658
+
659
+ try {
660
+ const result = await debateOrchestrator.run(fullQuery, {
661
+ signal: ctx.signal,
662
+ onProgress: event => {
663
+ if (event.type === 'worker-start') {
664
+ ctx.setAgentState?.(`Debating • ${event.id === 'advocate' ? 'Advocate' : 'Critic'}`);
665
+ if (ctx.tuiPrompt?.addViewportLines) {
666
+ ctx.tuiPrompt.addViewportLines(` ${C.cyberCyan}▸ Debate Worker Started:${C.reset} ${C.bold}${event.label}${C.reset}`);
667
+ }
668
+ } else if (event.type === 'synthesizing') {
669
+ ctx.setAgentState?.('Synthesizing Verdict…');
670
+ if (ctx.tuiPrompt?.addViewportLines) {
671
+ ctx.tuiPrompt.addViewportLines(` ${C.gold}✦ Synthesizing Neutral Judge Verdict...${C.reset}`);
672
+ }
673
+ }
674
+ },
675
+ });
676
+
677
+ const lines = formatDebateMatrix(result, getFullWidth());
678
+ if (ctx.tuiPrompt?.addViewportLines) {
679
+ ctx.tuiPrompt.addViewportLines(lines);
680
+ } else {
681
+ console.log(`\n${lines.join('\n')}\n`);
682
+ }
683
+ } catch (error) {
684
+ if (!isAbortError(error)) {
685
+ if (ctx.tuiPrompt?.addViewportLines) {
686
+ ctx.tuiPrompt.addViewportLines(` ${C.red}[DEBATE ERROR] ${error.message}${C.reset}`);
687
+ } else {
688
+ console.log(`\n ${C.red}[DEBATE ERROR] ${error.message}${C.reset}\n`);
689
+ }
690
+ }
691
+ } finally {
692
+ ctx.setIsProcessing?.(false);
693
+ }
694
+ return 'HANDLED';
695
+ },
696
+
697
+ '/timeline': async (ctx) => {
698
+ const rawFilter = `${ctx.sub || ''} ${ctx.extra || ''}`.trim().toLowerCase();
699
+ let items = [...timelineData];
700
+
701
+ if (rawFilter) {
702
+ if (/^\d{4}$/.test(rawFilter)) {
703
+ items = items.filter(t => t.year === rawFilter);
704
+ } else if (/^\d{4}-\d{4}$/.test(rawFilter)) {
705
+ const [startY, endY] = rawFilter.split('-').map(Number);
706
+ items = items.filter(t => {
707
+ const y = Number(t.year);
708
+ return !isNaN(y) && y >= startY && y <= endY;
709
+ });
710
+ } else {
711
+ items = items.filter(t =>
712
+ (t.era && t.era.toLowerCase().includes(rawFilter)) ||
713
+ (t.category && t.category.toLowerCase().includes(rawFilter)) ||
714
+ (t.title && t.title.toLowerCase().includes(rawFilter)) ||
715
+ (t.summary && t.summary.toLowerCase().includes(rawFilter))
716
+ );
717
+ }
718
+ }
719
+
720
+ if (items.length === 0) {
721
+ const msg = ` ${C.amber}No career timeline milestones found matching "${rawFilter}".${C.reset}`;
722
+ if (ctx.tuiPrompt?.addViewportLines) ctx.tuiPrompt.addViewportLines(msg);
723
+ else console.log(`\n${msg}\n`);
724
+ return 'HANDLED';
725
+ }
726
+
727
+ const lines = formatCareerTimelineGraphic(items, getFullWidth());
728
+ if (ctx.tuiPrompt?.addViewportLines) {
729
+ ctx.tuiPrompt.addViewportLines(lines);
730
+ } else {
731
+ console.log(`\n${lines.join('\n')}\n`);
732
+ }
460
733
  return 'HANDLED';
461
734
  },
462
735
 
@@ -480,6 +753,7 @@ export const handlers = {
480
753
  ctx.setIsProcessing?.(true, `Suggestions: ${targets.slice(0, 20)}`);
481
754
  try {
482
755
  const batchResult = await ctx.agent.runSuggestionBatch(targets, {
756
+ signal: ctx.signal,
483
757
  onProgress: event => {
484
758
  if (event.type === 'worker-start') {
485
759
  console.log(` ${C.cyberCyan}▸ Worker ${event.index} Started:${C.reset} ${C.bold}${event.label}${C.reset} — ${C.dim}${event.question}${C.reset}`);
@@ -501,36 +775,41 @@ export const handlers = {
501
775
  ],
502
776
  });
503
777
 
504
- for (const res of batchResult.results) {
505
- if (res.answer) {
506
- const answerLines = formatMarkdownLines(res.answer);
507
- printSolidPanel({
508
- title: `[Worker ${res.index}] ${res.category} • ${res.question}`,
509
- lines: [
510
- `${C.bold}${C.cyberCyan}Question:${C.reset} ${res.question}`,
511
- `${C.dim}Tool calls: ${res.toolCalls?.length || 0} • Iterations: ${res.iterations || 1}${C.reset}`,
512
- '',
513
- `${C.bold}${C.roseGlow}Findings:${C.reset}`,
514
- ...answerLines,
515
- ],
516
- });
517
- } else {
518
- printRichCard({
519
- title: `Worker ${res.index} Failed: ${res.category}`,
520
- status: 'error',
521
- lines: [
522
- `Question: ${res.question}`,
523
- `Error: ${res.error || 'Failed to research'}`,
524
- ],
525
- });
526
- }
778
+ const allTools = (batchResult.results || []).flatMap(r => r.toolCalls || []).map(tc => ({
779
+ name: typeof tc === 'string' ? tc : (tc.name || 'tool'),
780
+ args: typeof tc === 'object' && tc.args ? tc.args : {},
781
+ result: typeof tc === 'object' && tc.result ? tc.result : { status: 'ok' },
782
+ duration: typeof tc === 'object' && tc.duration ? tc.duration : 0,
783
+ }));
784
+
785
+ const combinedAnswers = (batchResult.results || [])
786
+ .filter(r => Boolean(r.answer))
787
+ .map(r => `### [Worker ${r.index}] ${r.category} • ${r.question}\n${r.answer}`)
788
+ .join('\n\n') || '';
789
+
790
+ ctx.recordQueryDiagnostics?.({
791
+ prompt: `/suggestion run ${targets}`,
792
+ answer: combinedAnswers,
793
+ tools: allTools,
794
+ status: batchResult.successfulCount > 0 ? 'complete' : 'error',
795
+ iterations: batchResult.tasksCount || 1,
796
+ startedAt: startTime,
797
+ completedAt: Date.now(),
798
+ durationMs: Date.now() - startTime,
799
+ });
800
+
801
+ const findingLines = formatExecutiveFindings({ batchResult, termWidth: getFullWidth() });
802
+ for (const line of findingLines) {
803
+ console.log(line);
527
804
  }
528
805
  } catch (err) {
529
- printRichCard({
530
- title: 'Batch Subagent Error',
531
- status: 'error',
532
- lines: [err.message],
533
- });
806
+ if (!isAbortError(err)) {
807
+ printRichCard({
808
+ title: 'Batch Subagent Error',
809
+ status: 'error',
810
+ lines: [err.message],
811
+ });
812
+ }
534
813
  } finally {
535
814
  ctx.setIsProcessing?.(false);
536
815
  }
@@ -558,14 +837,42 @@ export const handlers = {
558
837
  topic: topicName,
559
838
  limit: 5,
560
839
  });
840
+ ctx.setIsProcessing?.(false);
561
841
 
842
+
843
+ const isInteractiveTty = Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.KIKU_PLAIN && ctx.readInput);
844
+
845
+ if (isInteractiveTty && !isTopic && (rawAction === 'list' || !rawAction)) {
846
+ const selected = await promptInteractiveSuggestion(suggestions, {
847
+ stdin: process.stdin,
848
+ stdout: process.stdout,
849
+ signal: ctx.signal,
850
+ });
851
+
852
+ if (selected.action === 'run' && selected.targets) {
853
+ return handlers['/suggestion']({ ...ctx, sub: 'run', extra: selected.targets });
854
+ }
855
+ if (selected.action === 'refresh') {
856
+ return handlers['/suggestion']({ ...ctx, sub: 'list', extra: '' });
857
+ }
858
+ if (selected.action === 'cancel') {
859
+ return 'HANDLED';
860
+ }
861
+ }
862
+
863
+
562
864
  const cardLines = [
563
865
  `${C.dim}Explore exciting Ju Jingyi topics with concurrent subagents:${C.reset}`,
564
866
  '',
565
- ...suggestions.flatMap((s) => [
566
- ` ${C.bold}${C.gold}[${s.id}]${C.reset} ${C.bold}${C.cyberCyan}[${s.category}]${C.reset} ${C.white}${s.question}${C.reset}`,
567
- ` ${C.dim}↳ ${s.description}${C.reset}`,
568
- ]),
867
+ ...suggestions.flatMap((s, idx) => {
868
+ const badge = getCategoryBadge(s.category);
869
+ return [
870
+ ` ${C.bold}${C.gold}[${s.id}]${C.reset} ${badge.color}[${badge.icon} ${badge.label}]${C.reset} ${C.bold}${C.white}${s.question}${C.reset}`,
871
+ ` ${C.dim}↳ ${s.description}${C.reset}`,
872
+ ` ${C.slate}Tools: ${(s.tools || []).join(', ')} • Est: ~${s.estimatedSeconds || 2}s • Depth: ${s.depth || 'Standard'}${C.reset}`,
873
+ ...(idx < suggestions.length - 1 ? [''] : []),
874
+ ];
875
+ }),
569
876
  '',
570
877
  `${C.bold}${C.roseGlow}Quick Execution Tips:${C.reset}`,
571
878
  ` • Run one task: ${C.white}/suggestion run 1${C.reset}`,
@@ -927,19 +1234,22 @@ export const handlers = {
927
1234
  let exported;
928
1235
  if (format === 'json') {
929
1236
  exported = await agent.session.exportToJson();
1237
+ } else if (format === 'html' || format === 'web') {
1238
+ exported = await agent.session.exportToHtml();
930
1239
  } else if (format === 'text' || format === 'txt') {
931
1240
  exported = await agent.session.exportToText();
932
1241
  } else {
933
1242
  exported = await agent.session.exportToMarkdown();
934
1243
  }
935
1244
 
1245
+ const fmt = (exported?.format || (format === 'json' ? 'JSON' : format === 'html' || format === 'web' ? 'HTML' : format === 'text' || format === 'txt' ? 'TEXT' : 'MARKDOWN')).toUpperCase();
936
1246
  printRichCard({
937
1247
  title: 'Session Exported Successfully',
938
1248
  status: 'ok',
939
1249
  lines: [
940
- `• Format: ${exported.format.toUpperCase()}`,
941
- `• File: ${exported.filePath}`,
942
- `• Size: ${(exported.size || 0).toLocaleString()} bytes`,
1250
+ `• Format: ${fmt}`,
1251
+ `• File: ${exported?.filePath || 'Export generated'}`,
1252
+ `• Size: ${(exported?.size || 0).toLocaleString()} bytes`,
943
1253
  ],
944
1254
  });
945
1255
  return 'HANDLED';
@@ -1048,15 +1358,7 @@ export const handlers = {
1048
1358
  ctx.tuiPrompt.render();
1049
1359
  }
1050
1360
 
1051
- printSolidPanel({
1052
- title: 'Theme Updated',
1053
- lines: [
1054
- `${C.emerald}[OK] Active theme: ${C.bold}${theme.label} (${requested})${C.reset}`,
1055
- `${C.dim}${theme.description}${C.reset}`,
1056
- `${C.sakura}Primary${C.reset} ${C.roseGlow}Accent${C.reset} ${C.cyberCyan}Info${C.reset} ${C.emerald}Success${C.reset} ${C.amber}Warning${C.reset} ${C.red}Error${C.reset}`,
1057
- `${C.dim}Saved for future Kiku Agent sessions.${C.reset}`,
1058
- ],
1059
- });
1361
+ console.log(`\n ${C.sakura}◆${C.reset} Active theme: ${C.bold}${theme.label}${C.reset} ${C.dim}(${requested})${C.reset}\n`);
1060
1362
  return 'HANDLED';
1061
1363
  },
1062
1364
 
@@ -1368,17 +1670,12 @@ export const handlers = {
1368
1670
  },
1369
1671
 
1370
1672
  '/save': async (ctx) => {
1371
- try {
1372
- const exported = await ctx.agent.session.exportToMarkdown();
1373
- console.log(`\n ${C.emerald}[OK] Exported.${C.reset} ${C.white}${exported.filePath}${C.reset}\n`);
1374
- } catch (err) {
1375
- console.log(`\n ${C.red}[SAVE ERROR] ${err.message || String(err)}${C.reset}\n`);
1376
- }
1377
- return 'HANDLED';
1673
+ const fmt = ctx.sub || ctx.extra || 'markdown';
1674
+ return handlers['/session']({ ...ctx, sub: 'export', extra: fmt });
1378
1675
  },
1379
1676
 
1380
1677
  '/provider': async (ctx) => {
1381
- const { CLOUD_PROVIDERS, getProvider, getRecommendedModels } = await import('./config.js');
1678
+ const { CLOUD_PROVIDERS, getProvider, getRecommendedModels, isProviderKeyPersistenceEnabled } = await import('./config.js');
1382
1679
  const action = (ctx.sub || 'status').toLowerCase();
1383
1680
  const parts = ctx.parts || [];
1384
1681
 
@@ -1574,7 +1871,10 @@ export const handlers = {
1574
1871
  const masked = newKey.length > 8
1575
1872
  ? `${newKey.slice(0, 4)}...${newKey.slice(-4)}`
1576
1873
  : '****';
1577
- console.log(`\n ${C.emerald}[OK] API key saved & persisted for ${targetProvider}${C.reset} (${masked})\n`);
1874
+ const persistenceLabel = isProviderKeyPersistenceEnabled()
1875
+ ? 'saved with encrypted persistence'
1876
+ : 'saved in memory only (set KIKU_PROVIDER_KEY_ENCRYPTION_KEY to persist)';
1877
+ console.log(`\n ${C.emerald}[OK] API key ${persistenceLabel} for ${targetProvider}${C.reset} (${masked})\n`);
1578
1878
  return 'HANDLED';
1579
1879
  }
1580
1880
 
@@ -1653,12 +1953,25 @@ export const handlers = {
1653
1953
  : await ctx.client.checkConnection();
1654
1954
 
1655
1955
  if (action === 'list') {
1656
- const serverModels = (liveCheck.models || []).slice(0, 40).map(id => `${id === ctx.client.model ? '[x]' : '[ ]'} ${id}`);
1956
+ const activeLoaded = liveCheck.activeLoadedModel;
1957
+ const serverModels = (liveCheck.models || []).slice(0, 40).map(id => {
1958
+ const isCurrent = id === ctx.client.model;
1959
+ const isLoaded = id === activeLoaded || (liveCheck.loadedModels && liveCheck.loadedModels.includes(id));
1960
+ const badge = isCurrent ? `${C.gold}[x]${C.reset}` : `${C.dim}[ ]${C.reset}`;
1961
+ const loadTag = isLoaded ? ` ${C.emerald}(Loaded in VRAM)${C.reset}` : '';
1962
+ return `${badge} ${C.white}${id}${C.reset}${loadTag}`;
1963
+ });
1657
1964
  const recs = getRecommendedModels(ctx.client.provider);
1658
1965
  const recLines = recs.length > 0
1659
1966
  ? [
1660
- `${C.bold}${C.lavender}Recommended Active Models:${C.reset}`,
1661
- ...recs.map(m => ` ${m.id === ctx.client.model ? C.gold + '[x]' : C.dim + '[ ]'} ${C.white}${m.id}${C.reset} ${C.dim}(${m.cost} • ${m.tier})${C.reset}`),
1967
+ `${C.bold}${C.lavender}Recommended Active Models / Quantizations:${C.reset}`,
1968
+ ...recs.map(m => {
1969
+ const isCurrent = m.id === ctx.client.model || (ctx.client.model && ctx.client.model.includes(m.id.replace('google/', '')));
1970
+ const isLoaded = activeLoaded && activeLoaded.includes(m.id.replace('google/', ''));
1971
+ const marker = isCurrent ? `${C.gold}[x]${C.reset}` : `${C.dim}[ ]${C.reset}`;
1972
+ const loadTag = isLoaded ? ` ${C.emerald}● VRAM Ready${C.reset}` : '';
1973
+ return ` ${marker} ${C.white}${m.id.padEnd(28)}${C.reset} ${C.dim}(${m.cost} • ${m.tier})${C.reset}${loadTag}`;
1974
+ }),
1662
1975
  ''
1663
1976
  ]
1664
1977
  : [];
@@ -1670,7 +1983,7 @@ export const handlers = {
1670
1983
  `${C.bold}${C.lavender}Reported by Server:${C.reset}`,
1671
1984
  ...(serverModels.length ? serverModels.map(s => ` ${s}`) : [` ${C.dim}No live model list reported by ${providerName}.${C.reset}`]),
1672
1985
  '',
1673
- `${C.dim}Select model: /model use <model_id> or /model <provider_or_model>${C.reset}`
1986
+ `${C.dim}Select model: /model use <model_id> or /model use <Q8_0|Q6_K|Q4_K_M>${C.reset}`
1674
1987
  ],
1675
1988
  });
1676
1989
  return 'HANDLED';
@@ -1683,14 +1996,16 @@ export const handlers = {
1683
1996
  try {
1684
1997
  const lowerTarget = targetModel.toLowerCase();
1685
1998
 
1686
-
1687
1999
  if (CLOUD_PROVIDERS[lowerTarget]) {
1688
2000
  return await handlers['/provider']({ ...ctx, sub: 'use', extra: lowerTarget, parts: ['/provider', 'use', lowerTarget] });
1689
2001
  }
1690
2002
 
1691
-
1692
2003
  let matchedProvider = null;
1693
- if (lowerTarget.includes('gemma') || lowerTarget === 'google/gemma-4-e4b') {
2004
+ if (
2005
+ lowerTarget.includes('gemma') ||
2006
+ lowerTarget === 'google/gemma-4-e4b' ||
2007
+ /^(q[0-9]|8bit|6bit|4bit|fp16)/i.test(lowerTarget)
2008
+ ) {
1694
2009
  matchedProvider = 'lmstudio';
1695
2010
  } else {
1696
2011
  for (const [pId, pDef] of Object.entries(CLOUD_PROVIDERS)) {
@@ -1708,9 +2023,19 @@ export const handlers = {
1708
2023
 
1709
2024
  if (ctx.modelManager) {
1710
2025
  const selected = ctx.modelManager.select(targetModel, liveCheck.models || []);
2026
+ if (ctx.tuiPrompt) {
2027
+ ctx.tuiPrompt.modelName = selected;
2028
+ ctx.tuiPrompt.isConnected = true;
2029
+ ctx.tuiPrompt.render();
2030
+ }
1711
2031
  console.log(`\n ${C.emerald}[OK] Active model changed to ${selected}.${C.reset}\n`);
1712
2032
  } else {
1713
2033
  ctx.client.setModel(targetModel);
2034
+ if (ctx.tuiPrompt) {
2035
+ ctx.tuiPrompt.modelName = targetModel;
2036
+ ctx.tuiPrompt.isConnected = true;
2037
+ ctx.tuiPrompt.render();
2038
+ }
1714
2039
  console.log(`\n ${C.emerald}[OK] Active model changed to ${targetModel}.${C.reset}\n`);
1715
2040
  }
1716
2041
  } catch (error) {
@@ -2001,17 +2326,122 @@ export const handlers = {
2001
2326
  return 'HANDLED';
2002
2327
  },
2003
2328
 
2329
+ '/profile': async (ctx) => {
2330
+ const { listProfiles, getProfile, setActiveProfile, createOrUpdateProfile, deleteProfile, getActiveProfile } = await import('./profiles.js');
2331
+ const action = (ctx.sub || 'list').toLowerCase();
2332
+ const parts = ctx.parts || [];
2333
+
2334
+ if (action === 'list' || (action === 'show' && !ctx.extra)) {
2335
+ const profiles = listProfiles();
2336
+ const lines = profiles.map(p => {
2337
+ const marker = p.isActive ? `${C.emerald}* [ACTIVE]${C.reset}` : ` [ ]`;
2338
+ const nameCol = `${C.bold}${C.cyberCyan}${p.name.padEnd(16)}${C.reset}`;
2339
+ const provCol = `${C.white}${(p.provider || 'lmstudio').padEnd(12)}${C.reset}`;
2340
+ const modelCol = `${C.lavender}${(p.model || '').padEnd(26)}${C.reset}`;
2341
+ const thinkCol = p.thinking === 'show' ? `${C.emerald}think:on${C.reset}` : `${C.dim}think:off${C.reset}`;
2342
+ return `${marker} ${nameCol} ${provCol} ${modelCol} ${thinkCol}`;
2343
+ });
2344
+
2345
+ printSolidPanel({
2346
+ title: `Configuration Profiles (${profiles.length})`,
2347
+ lines: [
2348
+ ...lines,
2349
+ '',
2350
+ `${C.dim}Usage: /profile use <name> | /profile show <name> | /profile save <name>${C.reset}`
2351
+ ],
2352
+ });
2353
+ return 'HANDLED';
2354
+ }
2355
+
2356
+ if (action === 'use' || action === 'switch') {
2357
+ const targetName = (parts[2] || ctx.extra || '').trim();
2358
+ if (!targetName) {
2359
+ console.log(`\n ${C.amber}Usage: /profile use <profile_name>${C.reset}\n`);
2360
+ return 'HANDLED';
2361
+ }
2362
+ try {
2363
+ const profile = setActiveProfile(targetName);
2364
+ if (ctx.client && profile.provider) {
2365
+ ctx.client.switchProvider(profile.provider, { model: profile.model });
2366
+ }
2367
+ if (ctx.agent) {
2368
+ if (profile.thinking) ctx.agent.session.thinkingMode = profile.thinking;
2369
+ }
2370
+ if (ctx.tuiPrompt) {
2371
+ ctx.tuiPrompt.modelName = profile.model;
2372
+ ctx.tuiPrompt.providerName = profile.provider;
2373
+ ctx.tuiPrompt.render();
2374
+ }
2375
+ console.log(`\n ${C.emerald}[OK] Switched to profile "${profile.name}" (${profile.provider} / ${profile.model})${C.reset}\n`);
2376
+ } catch (err) {
2377
+ console.log(`\n ${C.red}[PROFILE ERROR] ${err.message}${C.reset}\n`);
2378
+ }
2379
+ return 'HANDLED';
2380
+ }
2381
+
2382
+ if (action === 'save') {
2383
+ const targetName = (parts[2] || ctx.extra || '').trim();
2384
+ if (!targetName) {
2385
+ console.log(`\n ${C.amber}Usage: /profile save <profile_name>${C.reset}\n`);
2386
+ return 'HANDLED';
2387
+ }
2388
+ try {
2389
+ const currentProvider = ctx.client?.provider || 'lmstudio';
2390
+ const currentModel = ctx.client?.model || 'google/gemma-4-e4b';
2391
+ const currentThinking = ctx.agent?.session?.thinkingMode || 'show';
2392
+ const saved = createOrUpdateProfile(targetName, {
2393
+ provider: currentProvider,
2394
+ model: currentModel,
2395
+ thinking: currentThinking,
2396
+ description: `Custom profile created from active session`
2397
+ });
2398
+ console.log(`\n ${C.emerald}[OK] Saved profile "${saved.name}" (${saved.provider} / ${saved.model})${C.reset}\n`);
2399
+ } catch (err) {
2400
+ console.log(`\n ${C.red}[PROFILE ERROR] ${err.message}${C.reset}\n`);
2401
+ }
2402
+ return 'HANDLED';
2403
+ }
2404
+
2405
+ if (action === 'delete' || action === 'del') {
2406
+ const targetName = (parts[2] || ctx.extra || '').trim();
2407
+ if (!targetName) {
2408
+ console.log(`\n ${C.amber}Usage: /profile delete <profile_name>${C.reset}\n`);
2409
+ return 'HANDLED';
2410
+ }
2411
+ try {
2412
+ deleteProfile(targetName);
2413
+ console.log(`\n ${C.emerald}[OK] Deleted profile "${targetName}".${C.reset}\n`);
2414
+ } catch (err) {
2415
+ console.log(`\n ${C.red}[PROFILE ERROR] ${err.message}${C.reset}\n`);
2416
+ }
2417
+ return 'HANDLED';
2418
+ }
2419
+
2420
+ return 'HANDLED';
2421
+ },
2422
+
2004
2423
  '/config': async (ctx) => {
2005
- const { config } = await import('./config.js');
2424
+ const { getActiveProfile } = await import('./profiles.js');
2425
+ if (ctx.sub === 'list' || ctx.sub === 'profiles') {
2426
+ return handlers['/profile'](ctx);
2427
+ }
2428
+ if (ctx.sub === 'use') {
2429
+ return handlers['/profile'](ctx);
2430
+ }
2431
+ const activeProf = getActiveProfile();
2006
2432
  printSolidPanel({
2007
2433
  title: `Configuration`,
2008
2434
  lines: [
2009
- `${C.gold}URL:${C.reset} ${C.cyberCyan}${ctx.baseUrl}${C.reset}`,
2435
+ `${C.gold}Profile:${C.reset} ${C.bold}${C.emerald}${activeProf.name}${C.reset} ${C.dim}(${activeProf.description || ''})${C.reset}`,
2436
+ `${C.gold}Provider:${C.reset} ${C.white}${ctx.client.providerName || ctx.client.provider || 'LM Studio'}${C.reset}`,
2010
2437
  `${C.gold}Model:${C.reset} ${C.white}${ctx.client.model}${C.reset}`,
2438
+ `${C.gold}Thinking:${C.reset} ${C.white}${ctx.agent?.session?.thinkingMode || 'show'}${C.reset}`,
2011
2439
  `${C.gold}Theme:${C.reset} ${C.white}${getThemeName()}${C.reset}`,
2012
- `${C.gold}Max Iters:${C.reset} ${C.white}${ctx.agent.maxIterations}${C.reset}`,
2013
- `${C.gold}Window:${C.reset} ${C.white}${ctx.agent.slidingWindowSize}${C.reset}`,
2014
- `${C.gold}Tools:${C.reset} ${C.cyberCyan}${toolsSchema.length}${C.reset}`
2440
+ `${C.gold}Max Iters:${C.reset} ${C.white}${ctx.agent?.maxIterations ?? 5}${C.reset}`,
2441
+ `${C.gold}Window:${C.reset} ${C.white}${ctx.agent?.slidingWindowSize ?? 10}${C.reset}`,
2442
+ `${C.gold}Tools:${C.reset} ${C.cyberCyan}${toolsSchema.length}${C.reset}`,
2443
+ '',
2444
+ `${C.dim}Manage profiles: /profile list | /profile use <name> | /profile save <name>${C.reset}`
2015
2445
  ],
2016
2446
  headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
2017
2447
  });
@@ -2048,194 +2478,6 @@ export const handlers = {
2048
2478
  return 'HANDLED';
2049
2479
  },
2050
2480
 
2051
- '/update': async (ctx) => {
2052
- const manager = ctx.updateManager || new UpdateManager();
2053
- const action = (ctx.sub || 'check').toLowerCase();
2054
- const parts = ctx.parts || [];
2055
- const allowMajor = parts.includes('--allow-major');
2056
- const allowDeprecated = parts.includes('--allow-deprecated');
2057
- const positional = parts.slice(2).filter(part => !part.startsWith('--'));
2058
- const flags = parts.slice(2).filter(part => part.startsWith('--'));
2059
-
2060
- const printUpdateHelp = () => printSolidPanel({
2061
- title: 'Kiku Agent Update',
2062
- lines: [
2063
- `${C.bold}${C.white}/update${C.reset} ${C.dim}or /update check — check the configured channel${C.reset}`,
2064
- `${C.bold}${C.white}/update install [latest|next|canary|x.y.z]${C.reset}`,
2065
- `${C.bold}${C.white}/update channel [latest|next|canary]${C.reset}`,
2066
- `${C.bold}${C.white}/update rollback${C.reset} ${C.dim}— reinstall the previously recorded version${C.reset}`,
2067
- '',
2068
- `${C.dim}Major releases require --allow-major. Deprecated releases require --allow-deprecated.${C.reset}`,
2069
- ],
2070
- });
2071
-
2072
- const statusLines = status => {
2073
- const changeLabel = status.change === 'upgrade'
2074
- ? `${C.emerald}UPDATE AVAILABLE${C.reset}`
2075
- : status.change === 'downgrade'
2076
- ? `${C.amber}OLDER RELEASE${C.reset}`
2077
- : `${C.emerald}UP TO DATE${C.reset}`;
2078
- const lines = [
2079
- `${C.gold}Current:${C.reset} ${C.white}${status.currentVersion}${C.reset}`,
2080
- `${C.gold}Target:${C.reset} ${C.white}${status.targetVersion}${C.reset} ${C.dim}(${status.target})${C.reset}`,
2081
- `${C.gold}Status:${C.reset} ${changeLabel}`,
2082
- `${C.gold}Installation:${C.reset} ${C.white}${status.installMode.label}${C.reset}`,
2083
- `${C.gold}Node:${C.reset} ${status.nodeCompatible ? C.emerald + 'Compatible' : C.red + 'Incompatible'}${C.reset}${status.nodeRange ? ` ${C.dim}(requires ${status.nodeRange})${C.reset}` : ''}`,
2084
- `${C.gold}Integrity:${C.reset} ${status.integrity ? C.emerald + 'Published with npm integrity metadata' : C.amber + 'Not reported'}${C.reset}`,
2085
- ];
2086
- if (status.majorChange) lines.push(`${C.amber}[MAJOR] This release crosses a major-version boundary.${C.reset}`);
2087
- if (status.deprecated) lines.push(`${C.red}[DEPRECATED] ${status.deprecated}${C.reset}`);
2088
- lines.push('', `${C.bold}${C.lavender}What's new:${C.reset}`);
2089
- if (status.releaseNotes.length > 0) {
2090
- for (const note of status.releaseNotes) lines.push(` ${C.sakura}•${C.reset} ${C.white}${note}${C.reset}`);
2091
- } else {
2092
- lines.push(` ${C.dim}No release summary was published for this version.${C.reset}`);
2093
- }
2094
- if (status.releaseUrl) lines.push('', `${C.dim}Release: ${status.releaseUrl}${C.reset}`);
2095
- return lines;
2096
- };
2097
-
2098
- try {
2099
- const allowedFlags = action === 'install'
2100
- ? ['--allow-major', '--allow-deprecated']
2101
- : action === 'rollback'
2102
- ? ['--allow-deprecated']
2103
- : [];
2104
- if (flags.some(flag => !allowedFlags.includes(flag))) {
2105
- throw new UpdateError('Unknown option. Run /update help for supported arguments.', 'INVALID_OPTION');
2106
- }
2107
- if (positional.length > 1 || ((action === 'rollback' || action === 'help') && positional.length > 0)) {
2108
- throw new UpdateError('Too many update arguments. Run /update help for usage.', 'INVALID_ARGUMENTS');
2109
- }
2110
-
2111
- if (action === 'help') {
2112
- printUpdateHelp();
2113
- return 'HANDLED';
2114
- }
2115
-
2116
- if (action === 'channel') {
2117
- const requested = positional[0];
2118
- if (!requested) {
2119
- const channel = await manager.getChannel();
2120
- printSolidPanel({
2121
- title: 'Update Channel',
2122
- lines: [
2123
- `${C.gold}Current:${C.reset} ${C.white}${channel}${C.reset}`,
2124
- `${C.dim}Available: ${UPDATE_CHANNELS.join(', ')}${C.reset}`,
2125
- ],
2126
- });
2127
- return 'HANDLED';
2128
- }
2129
- const channel = await manager.setChannel(requested);
2130
- const lines = [
2131
- `${C.emerald}[OK] Future /update checks will use ${C.bold}${channel}${C.reset}${C.emerald}.${C.reset}`,
2132
- ];
2133
- if (channel !== 'latest') {
2134
- lines.push(`${C.amber}[NOTE] The "${channel}" channel may not have published builds on npm yet.${C.reset}`);
2135
- }
2136
- lines.push(`${C.dim}This changes only the release channel; it does not install anything.${C.reset}`);
2137
- printSolidPanel({
2138
- title: 'Update Channel Changed',
2139
- lines,
2140
- });
2141
- return 'HANDLED';
2142
- }
2143
-
2144
- if (action === 'install') {
2145
- const target = positional[0];
2146
- console.log(`\n ${C.cyberCyan}[UPDATE] Resolving and validating the requested npm release...${C.reset}\n`);
2147
- const result = await manager.install(target, {
2148
- allowMajor,
2149
- allowDeprecated,
2150
- onProgress: message => { if (message) console.log(` ${C.dim}${message}${C.reset}`); },
2151
- });
2152
- if (result.outcome === 'manual') {
2153
- printSolidPanel({
2154
- title: 'Manual Update Required',
2155
- lines: [
2156
- ...statusLines(result),
2157
- '',
2158
- `${C.amber}This installation type is not modified automatically.${C.reset}`,
2159
- `${C.gold}Run:${C.reset} ${C.white}${result.command}${C.reset}`,
2160
- ],
2161
- });
2162
- return 'HANDLED';
2163
- }
2164
- if (result.outcome === 'current') {
2165
- printSolidPanel({ title: 'Kiku Agent Is Up to Date', lines: statusLines(result) });
2166
- return 'HANDLED';
2167
- }
2168
- printSolidPanel({
2169
- title: 'Update Installed Successfully',
2170
- lines: [
2171
- `${C.emerald}[OK] ${PACKAGE_NAME} ${result.installedVersion} was installed and verified.${C.reset}`,
2172
- ...(!result.historyRecorded ? [`${C.amber}[WARNING] Rollback history could not be saved.${C.reset}`] : []),
2173
- `${C.dim}This session will close. Start kiku-agent again to load the new code.${C.reset}`,
2174
- `${C.dim}Restart MCP hosts to load the matching kiku-mcp server version.${C.reset}`,
2175
- ],
2176
- });
2177
- try { await ctx.agent?.endSession?.(); } catch {}
2178
- return 'EXIT';
2179
- }
2180
-
2181
- if (action === 'rollback') {
2182
- console.log(`\n ${C.cyberCyan}[UPDATE] Resolving the previously installed version...${C.reset}\n`);
2183
- const result = await manager.rollback({ allowDeprecated });
2184
- if (result.outcome === 'manual') {
2185
- printSolidPanel({
2186
- title: 'Manual Rollback Required',
2187
- lines: [
2188
- `${C.amber}Run the following command outside Kiku Agent:${C.reset}`,
2189
- `${C.white}${result.command}${C.reset}`,
2190
- ],
2191
- });
2192
- return 'HANDLED';
2193
- }
2194
- if (result.outcome === 'current') {
2195
- printSolidPanel({ title: 'Rollback Not Needed', lines: statusLines(result) });
2196
- return 'HANDLED';
2197
- }
2198
- printSolidPanel({
2199
- title: 'Rollback Installed Successfully',
2200
- lines: [
2201
- `${C.emerald}[OK] Restored ${PACKAGE_NAME} ${result.installedVersion}.${C.reset}`,
2202
- ...(!result.historyRecorded ? [`${C.amber}[WARNING] Rollback history could not be saved.${C.reset}`] : []),
2203
- `${C.dim}This session will close. Restart Kiku Agent and any MCP host.${C.reset}`,
2204
- ],
2205
- });
2206
- try { await ctx.agent?.endSession?.(); } catch {}
2207
- return 'EXIT';
2208
- }
2209
-
2210
- if (action === 'check' || action === 'status') {
2211
- const status = await manager.check(positional[0]);
2212
- const lines = statusLines(status);
2213
- if (status.updateAvailable) {
2214
- lines.push('', `${C.cyberCyan}Run /update install${status.target === 'latest' ? '' : ` ${status.target}`} to update.${C.reset}`);
2215
- }
2216
- printSolidPanel({ title: 'Kiku Agent Update', lines });
2217
- return 'HANDLED';
2218
- }
2219
-
2220
- printUpdateHelp();
2221
- return 'HANDLED';
2222
- } catch (error) {
2223
- const safeMessage = error instanceof UpdateError ? error.message : 'The update operation failed unexpectedly.';
2224
- const lines = [
2225
- `${C.red}[${error.code || 'UPDATE_ERROR'}] ${safeMessage}${C.reset}`,
2226
- ];
2227
- if (error.code === 'CHANNEL_NOT_FOUND') {
2228
- lines.push(`${C.gold}Tip:${C.reset} ${C.white}Run ${C.bold}/update channel latest${C.reset}${C.white} to return to the active release channel.${C.reset}`);
2229
- }
2230
- lines.push(`${C.dim}No session, memory, or environment files were changed.${C.reset}`);
2231
- printSolidPanel({
2232
- title: 'Update Failed',
2233
- lines,
2234
- });
2235
- return 'HANDLED';
2236
- }
2237
- },
2238
-
2239
2481
  '/system': async () => {
2240
2482
  const os = await import('node:os');
2241
2483
  const mem = process.memoryUsage();
@@ -2451,13 +2693,13 @@ export const handlers = {
2451
2693
  const { getCopyableContent, copyToClipboard, parseDetailsRequest } = await import('./queryDetails.js');
2452
2694
  const reference = [ctx.sub, ctx.extra].filter(Boolean).join(' ');
2453
2695
  const request = parseDetailsRequest(reference);
2454
- const details = ctx.agent?.lastQueryDetails || null;
2696
+ const details = ctx.agent?.lastQueryDetails || ctx.agent?._lastQueryDetails || null;
2455
2697
  const copyable = getCopyableContent(details, request, ctx.agent?.session);
2456
- if (!copyable?.text) {
2698
+ if (!copyable?.text || !copyable.text.trim()) {
2457
2699
  printRichCard({
2458
2700
  title: 'Nothing to Copy',
2459
2701
  status: 'warning',
2460
- lines: ['The requested query or session section is empty.'],
2702
+ lines: ['The requested query or session section is empty or no query has been recorded.'],
2461
2703
  });
2462
2704
  return 'HANDLED';
2463
2705
  }
@@ -2480,14 +2722,42 @@ export const handlers = {
2480
2722
  },
2481
2723
 
2482
2724
  '/gallery': async (ctx) => {
2483
- const count = Number(ctx.sub) || 2;
2484
- const displayed = typeof ctx.showTerminalImage === 'function' && ctx.showTerminalImage(count);
2725
+ const rawSub = String(ctx.sub || '').trim().toLowerCase();
2726
+ let renderOptions = { count: 2 };
2727
+
2728
+ if (rawSub === 'hd' || rawSub === 'single' || rawSub === 'card') {
2729
+ renderOptions = { count: 1, hd: true };
2730
+ } else if (!isNaN(Number(rawSub)) && Number(rawSub) > 0) {
2731
+ renderOptions = { index: Number(rawSub) - 1, hd: true };
2732
+ } else if (rawSub === 'all' || rawSub === 'pair') {
2733
+ renderOptions = { count: 2 };
2734
+ } else if (rawSub === 'list') {
2735
+ const { discoverImages, findGalleryMetadata } = await import('./terminalImage.js');
2736
+ const imgs = discoverImages();
2737
+ const lines = imgs.map((img, i) => {
2738
+ const meta = findGalleryMetadata(img);
2739
+ const title = meta?.titleCn || meta?.titleTh || path.basename(img);
2740
+ return ` ${C.cyan}${String(i + 1).padStart(2, ' ')}.${C.reset} ${C.bold}${title}${C.reset} ${meta?.date ? `${C.dim}(${meta.date})${C.reset}` : ''}`;
2741
+ });
2742
+ printSolidPanel({
2743
+ title: '🌸 Ju Jingyi Visual Photo Gallery Catalog',
2744
+ lines: [
2745
+ `${C.dim}Use /gallery <number> or /gallery hd to render in crystal-clear TrueColor:${C.reset}`,
2746
+ '',
2747
+ ...lines,
2748
+ ],
2749
+ bg: '\x1b[48;2;45;35;55m'
2750
+ });
2751
+ return 'HANDLED';
2752
+ }
2753
+
2754
+ const displayed = typeof ctx.showTerminalImage === 'function' && ctx.showTerminalImage(renderOptions);
2485
2755
  if (!displayed) {
2486
2756
  printRichCard({
2487
2757
  title: 'Terminal Gallery Unavailable',
2488
2758
  status: 'warning',
2489
2759
  lines: [
2490
- 'Images require an interactive color terminal and packaged files in src/assets/images.',
2760
+ 'Images require an interactive color terminal and packaged files in public/images or src/assets/images.',
2491
2761
  'Set KIKU_TUI_IMAGES=on to override terminal detection, or off to disable images.',
2492
2762
  ],
2493
2763
  });
@@ -2608,7 +2878,7 @@ export const handlers = {
2608
2878
  const selectedText = opts[idx];
2609
2879
  if (idx === q.answer) {
2610
2880
  answers.push({
2611
- qIndex: i,
2881
+ qIndex: q.originalIndex ?? i,
2612
2882
  answerIndex: q.optionOrder?.[idx] ?? idx,
2613
2883
  });
2614
2884
  score++;
@@ -2774,12 +3044,16 @@ const ALIASES = {
2774
3044
  '/sponsor': '/ads', '/ad': '/ads',
2775
3045
  '/image': '/gallery', '/photo': '/gallery',
2776
3046
  '/cls': '/clear', '/reset': '/clear',
2777
- '/export': '/save',
3047
+ '/export': '/save', '/markdown': '/save',
3048
+ '/src': '/sources', '/srcs': '/sources',
2778
3049
  '/r': '/retry',
2779
3050
  '/kvip': '/key',
2780
3051
  '/switch': '/provider',
2781
3052
  '/providers': '/provider',
2782
3053
  '/fallback': '/provider',
3054
+ '/profiles': '/profile',
3055
+ '/preset': '/profile',
3056
+ '/presets': '/profile',
2783
3057
  };
2784
3058
 
2785
3059
  export async function routeCommand(rawInput, ctx) {