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
@@ -1,20 +1,27 @@
1
- import { normalizeProviderModel } from './config.js';
2
-
3
- const NON_CHAT_MODEL = /(embed|embedding|rerank|whisper|tts|clip)/i;
1
+ import { normalizeProviderModel, matchModelIdentifier } from './config.js';
2
+
3
+ const NON_CHAT_MODEL = /(embed|embedding|rerank|whisper|tts|clip)/i;
4
4
 
5
5
  export class ModelManager {
6
- constructor(client, options = {}) {
6
+ constructor(client, options = {}) {
7
7
  if (!client) throw new Error('ModelManager requires an LLM client.');
8
8
  this.client = client;
9
- this.requestedModel = options.requestedModel || client.model;
10
- this.fallbackModel = options.fallbackModel || null;
11
- this.fallbackModels = Array.isArray(options.fallbackModels) ? options.fallbackModels : [];
9
+ this.requestedModel = options.requestedModel || client.model;
10
+ this.fallbackModel = options.fallbackModel || null;
11
+ this.fallbackModels = Array.isArray(options.fallbackModels) ? options.fallbackModels : [];
12
12
  this.lastResolution = null;
13
13
  }
14
14
 
15
15
  async inspect() {
16
16
  const health = await this.client.checkConnection();
17
17
  const models = health.models || [];
18
+ const isReady = health.connected && models.length > 0 && (
19
+ models.includes(this.client.model) ||
20
+ models.some(m => matchModelIdentifier(this.client.model, m)) ||
21
+ Boolean(health.activeLoadedModel && models.includes(health.activeLoadedModel)) ||
22
+ Boolean(health.activeModel && models.includes(health.activeModel))
23
+ );
24
+
18
25
  return {
19
26
  ...health,
20
27
  models,
@@ -23,28 +30,40 @@ export class ModelManager {
23
30
  ? 'server_offline'
24
31
  : models.length === 0
25
32
  ? 'server_online_no_models'
26
- : models.includes(this.client.model)
33
+ : isReady
27
34
  ? 'ready'
28
35
  : 'requested_model_missing',
29
36
  };
30
37
  }
31
38
 
32
- select(model, availableModels = []) {
33
- const requested = normalizeProviderModel(this.client.provider, String(model || '').trim());
34
- if (!requested) throw new Error('A model ID is required.');
35
- if (availableModels.length > 0 && !availableModels.includes(requested)) {
36
- throw new Error(`Model is not available: ${requested}`);
37
- }
39
+ select(model, availableModels = []) {
40
+ const rawInput = String(model || '').trim();
41
+ if (!rawInput) throw new Error('A model ID is required.');
42
+ const requested = normalizeProviderModel(this.client.provider, rawInput);
38
43
  if (NON_CHAT_MODEL.test(requested)) {
39
44
  throw new Error(`Model does not appear to support chat: ${requested}`);
40
45
  }
41
- if (typeof this.client.setModel === 'function') {
42
- this.client.setModel(requested);
43
- } else {
44
- this.client.model = requested;
45
- }
46
- this.requestedModel = requested;
47
- return requested;
46
+
47
+ let targetModel = requested;
48
+ if (availableModels.length > 0) {
49
+ const exact = availableModels.find(m => m === requested || m === rawInput);
50
+ const fuzzy = availableModels.find(m => matchModelIdentifier(requested, m) || matchModelIdentifier(rawInput, m));
51
+ if (exact) {
52
+ targetModel = exact;
53
+ } else if (fuzzy) {
54
+ targetModel = fuzzy;
55
+ } else {
56
+ throw new Error(`Model is not available: ${requested}`);
57
+ }
58
+ }
59
+
60
+ if (typeof this.client.setModel === 'function') {
61
+ this.client.setModel(targetModel);
62
+ } else {
63
+ this.client.model = targetModel;
64
+ }
65
+ this.requestedModel = targetModel;
66
+ return targetModel;
48
67
  }
49
68
 
50
69
  async resolve(options = {}) {
@@ -57,33 +76,54 @@ export class ModelManager {
57
76
  return this._record({ state: 'server_online_no_models', connected: true, models, error: `${this.client.providerName || 'LLM server'} is online but no model is loaded.` });
58
77
  }
59
78
 
60
- const requested = options.requestedModel || this.requestedModel;
61
- const exact = models.find(id => id === requested);
62
- const configuredFallback = models.find(id => id === (options.fallbackModel || this.fallbackModel));
63
- const providerRecommendations = this.client.getProviderInfo?.().recommendedModels || [];
64
- const fallbackCandidates = [
65
- ...(Array.isArray(options.fallbackModels) ? options.fallbackModels : this.fallbackModels),
66
- ...providerRecommendations.map(model => model.id),
67
- ];
68
- const recommendedFallback = fallbackCandidates
69
- .map(candidate => models.find(id => id === candidate))
70
- .find(Boolean);
71
- const gemma = this.client.provider === 'lmstudio'
72
- ? models.find(id => /gemma/i.test(id) && !NON_CHAT_MODEL.test(id))
73
- : null;
74
- const chatFallback = models.find(id => !NON_CHAT_MODEL.test(id));
75
- const selected = exact || configuredFallback || recommendedFallback || gemma || chatFallback;
79
+ const requestedRaw = options.requestedModel || this.requestedModel || this.client.model;
80
+ const requested = normalizeProviderModel(this.client.provider, requestedRaw);
81
+
82
+
83
+ const activeLoaded = health.activeLoadedModel;
84
+
85
+
86
+ const exact = models.find(id => id === requested || id === requestedRaw);
87
+
88
+
89
+ const fuzzyRequested = models.find(id => matchModelIdentifier(requested, id) || matchModelIdentifier(requestedRaw, id));
90
+
91
+
92
+ const configuredFallback = models.find(id => id === (options.fallbackModel || this.fallbackModel));
93
+
94
+
95
+ const providerRecommendations = this.client.getProviderInfo?.().recommendedModels || [];
96
+ const fallbackCandidates = [
97
+ ...(Array.isArray(options.fallbackModels) ? options.fallbackModels : this.fallbackModels),
98
+ ...providerRecommendations.map(model => model.id),
99
+ ];
100
+ const recommendedFallback = fallbackCandidates
101
+ .map(candidate => models.find(id => id === candidate || matchModelIdentifier(candidate, id)))
102
+ .find(Boolean);
103
+
104
+
105
+ const gemma = this.client.provider === 'lmstudio'
106
+ ? (activeLoaded && /gemma/i.test(activeLoaded) ? activeLoaded : models.find(id => /gemma/i.test(id) && !NON_CHAT_MODEL.test(id)))
107
+ : null;
108
+
109
+
110
+ const chatFallback = (activeLoaded && !NON_CHAT_MODEL.test(activeLoaded))
111
+ ? activeLoaded
112
+ : models.find(id => !NON_CHAT_MODEL.test(id));
113
+
114
+ const selected = exact || fuzzyRequested || (activeLoaded && matchModelIdentifier(requested, activeLoaded) ? activeLoaded : null) || configuredFallback || recommendedFallback || activeLoaded || gemma || chatFallback;
76
115
  if (!selected) {
77
116
  return this._record({ state: 'no_chat_model', connected: true, models, error: 'No chat-capable model was found.' });
78
117
  }
118
+
79
119
  this.client.model = selected;
80
120
  return this._record({
81
- state: exact ? 'ready' : 'fallback_selected',
121
+ state: (exact || fuzzyRequested) ? 'ready' : 'fallback_selected',
82
122
  connected: true,
83
123
  models,
84
124
  selectedModel: selected,
85
125
  requestedModel: requested,
86
- usedFallback: !exact,
126
+ usedFallback: !(exact || (fuzzyRequested && matchModelIdentifier(requested, selected))),
87
127
  });
88
128
  }
89
129
 
@@ -0,0 +1,178 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { PROFILES_FILE, KIKU_DATA_DIR } from './appPaths.js';
4
+
5
+ export const BUILTIN_PROFILES = {
6
+ default: {
7
+ name: 'default',
8
+ provider: 'lmstudio',
9
+ model: 'google/gemma-4-e4b',
10
+ thinking: 'show',
11
+ temperature: 0.3,
12
+ description: 'Local Gemma 4 e4b via LM Studio (free, private, offline)',
13
+ isBuiltIn: true
14
+ },
15
+ fast: {
16
+ name: 'fast',
17
+ provider: 'groq',
18
+ model: 'openai/gpt-oss-20b',
19
+ thinking: 'hide',
20
+ temperature: 0.2,
21
+ description: 'Ultra-fast inference on Groq LPU (low latency)',
22
+ isBuiltIn: true
23
+ },
24
+ 'deep-reasoning': {
25
+ name: 'deep-reasoning',
26
+ provider: 'deepseek',
27
+ model: 'deepseek-v4-flash',
28
+ thinking: 'show',
29
+ temperature: 0.3,
30
+ description: 'DeepSeek V4 with full reasoning traces and CoT',
31
+ isBuiltIn: true
32
+ },
33
+ cloud: {
34
+ name: 'cloud',
35
+ provider: 'openai',
36
+ model: 'gpt-5.6-luna',
37
+ thinking: 'show',
38
+ temperature: 0.3,
39
+ description: 'OpenAI Cloud flagship multilingual agent tier',
40
+ isBuiltIn: true
41
+ },
42
+ gemini: {
43
+ name: 'gemini',
44
+ provider: 'gemini',
45
+ model: 'gemini-3.7-flash',
46
+ thinking: 'show',
47
+ temperature: 0.3,
48
+ description: 'Google Gemini 3.7 Flash multilingual reasoning',
49
+ isBuiltIn: true
50
+ }
51
+ };
52
+
53
+ export function loadProfiles() {
54
+ try {
55
+ if (fs.existsSync(PROFILES_FILE)) {
56
+ const raw = fs.readFileSync(PROFILES_FILE, 'utf-8');
57
+ const parsed = JSON.parse(raw);
58
+ if (parsed && typeof parsed === 'object') {
59
+ const userProfiles = parsed.profiles || {};
60
+ return {
61
+ activeProfile: parsed.activeProfile || 'default',
62
+ profiles: {
63
+ ...BUILTIN_PROFILES,
64
+ ...userProfiles
65
+ }
66
+ };
67
+ }
68
+ }
69
+ } catch {}
70
+
71
+ return {
72
+ activeProfile: 'default',
73
+ profiles: { ...BUILTIN_PROFILES }
74
+ };
75
+ }
76
+
77
+ export function saveProfiles(data) {
78
+ try {
79
+ if (!fs.existsSync(KIKU_DATA_DIR)) {
80
+ fs.mkdirSync(KIKU_DATA_DIR, { recursive: true });
81
+ }
82
+ const customProfiles = {};
83
+ for (const [key, val] of Object.entries(data.profiles || {})) {
84
+ if (!BUILTIN_PROFILES[key] || val.customized) {
85
+ customProfiles[key] = val;
86
+ }
87
+ }
88
+ const toWrite = {
89
+ activeProfile: data.activeProfile || 'default',
90
+ profiles: customProfiles,
91
+ updatedAt: new Date().toISOString()
92
+ };
93
+ fs.writeFileSync(PROFILES_FILE, JSON.stringify(toWrite, null, 2), 'utf-8');
94
+ return true;
95
+ } catch (err) {
96
+ console.error('Failed to save profiles to disk:', err.message);
97
+ return false;
98
+ }
99
+ }
100
+
101
+ export function getProfile(name) {
102
+ if (!name || typeof name !== 'string') return null;
103
+ const normalized = name.toLowerCase().trim();
104
+ const data = loadProfiles();
105
+ return data.profiles[normalized] || null;
106
+ }
107
+
108
+ export function getActiveProfile() {
109
+ const data = loadProfiles();
110
+ const activeName = data.activeProfile || 'default';
111
+ const profile = data.profiles[activeName] || BUILTIN_PROFILES.default;
112
+ return {
113
+ ...profile,
114
+ name: activeName
115
+ };
116
+ }
117
+
118
+ export function setActiveProfile(name) {
119
+ if (!name || typeof name !== 'string') return false;
120
+ const normalized = name.toLowerCase().trim();
121
+ const data = loadProfiles();
122
+ if (!data.profiles[normalized]) {
123
+ throw new Error(`Profile "${name}" does not exist. Run /profile list to see available profiles.`);
124
+ }
125
+ data.activeProfile = normalized;
126
+ saveProfiles(data);
127
+ return data.profiles[normalized];
128
+ }
129
+
130
+ export function createOrUpdateProfile(name, config) {
131
+ if (!name || typeof name !== 'string') {
132
+ throw new Error('Profile name is required');
133
+ }
134
+ const normalized = name.toLowerCase().trim();
135
+ const data = loadProfiles();
136
+
137
+ const existing = data.profiles[normalized] || {};
138
+ data.profiles[normalized] = {
139
+ ...existing,
140
+ name: normalized,
141
+ provider: config.provider || existing.provider || 'lmstudio',
142
+ model: config.model || existing.model || 'google/gemma-4-e4b',
143
+ thinking: config.thinking || existing.thinking || 'show',
144
+ temperature: config.temperature !== undefined ? Number(config.temperature) : (existing.temperature ?? 0.3),
145
+ description: config.description || existing.description || `Custom profile ${normalized}`,
146
+ customized: true,
147
+ updatedAt: new Date().toISOString()
148
+ };
149
+
150
+ saveProfiles(data);
151
+ return data.profiles[normalized];
152
+ }
153
+
154
+ export function deleteProfile(name) {
155
+ if (!name || typeof name !== 'string') return false;
156
+ const normalized = name.toLowerCase().trim();
157
+ if (BUILTIN_PROFILES[normalized]) {
158
+ throw new Error(`Cannot delete built-in profile "${name}".`);
159
+ }
160
+ const data = loadProfiles();
161
+ if (!data.profiles[normalized]) return false;
162
+
163
+ delete data.profiles[normalized];
164
+ if (data.activeProfile === normalized) {
165
+ data.activeProfile = 'default';
166
+ }
167
+ saveProfiles(data);
168
+ return true;
169
+ }
170
+
171
+ export function listProfiles() {
172
+ const data = loadProfiles();
173
+ return Object.values(data.profiles).map(p => ({
174
+ id: p.name,
175
+ ...p,
176
+ isActive: p.name === data.activeProfile
177
+ }));
178
+ }
@@ -0,0 +1,22 @@
1
+
2
+ export const PUBLIC_REMOTE_TOOL_NAMES = Object.freeze(new Set([
3
+ 'global_search',
4
+ 'search_works',
5
+ 'get_work_detail',
6
+ 'get_character_roles',
7
+ 'search_discography',
8
+ 'get_profile_and_fandom',
9
+ 'get_awards',
10
+ 'get_brand_endorsements',
11
+ 'get_career_timeline',
12
+ 'get_social_feed',
13
+ 'get_gallery_photos',
14
+ 'get_quotes',
15
+ 'get_music_videos',
16
+ 'get_media_links',
17
+ 'search_youtube',
18
+ 'web_search',
19
+ 'web_extract',
20
+ 'fetch_webpage',
21
+ 'fetch_api',
22
+ ]));
@@ -5,13 +5,32 @@ const DETAIL_VIEWS = new Set([
5
5
  'markdown', 'json', 'text', 'raw', 'session', 'transcript', 'history'
6
6
  ]);
7
7
 
8
+ const DETAIL_VIEW_ALIASES = Object.freeze({
9
+ think: 'thinking',
10
+ thought: 'thinking',
11
+ thoughts: 'thinking',
12
+ reasoning: 'thinking',
13
+ ans: 'answer',
14
+ response: 'answer',
15
+ result: 'answer',
16
+ toolcalls: 'tools',
17
+ tool_calls: 'tools',
18
+ calls: 'tools',
19
+ tool_call: 'tool',
20
+ md: 'markdown',
21
+ txt: 'text',
22
+ sess: 'session',
23
+ });
24
+
8
25
  export function parseDetailsRequest(reference = '') {
9
26
  const tokens = String(reference || '').trim().split(/\s+/).filter(Boolean);
10
27
  let queryNumber = null;
11
28
  let isSession = false;
12
29
 
13
30
  const first = (tokens[0] || '').toLowerCase();
14
- if (first === 'session' || first === 'transcript' || first === 'history') {
31
+ const normalizedFirst = DETAIL_VIEW_ALIASES[first] || first;
32
+
33
+ if (normalizedFirst === 'session' || normalizedFirst === 'transcript' || normalizedFirst === 'history') {
15
34
  isSession = true;
16
35
  tokens.shift();
17
36
  } else if (/^q?\d+$/i.test(tokens[0])) {
@@ -20,10 +39,13 @@ export function parseDetailsRequest(reference = '') {
20
39
  tokens.shift();
21
40
  }
22
41
 
23
- let view = (tokens.shift() || (isSession ? 'markdown' : 'answer')).toLowerCase();
42
+ let rawView = (tokens.shift() || (isSession ? 'markdown' : 'answer')).toLowerCase();
43
+ let view = DETAIL_VIEW_ALIASES[rawView] || rawView;
44
+
24
45
  if (view === 'session' || view === 'transcript' || view === 'history') {
25
46
  isSession = true;
26
- view = (tokens.shift() || 'markdown').toLowerCase();
47
+ const nextRaw = (tokens.shift() || 'markdown').toLowerCase();
48
+ view = DETAIL_VIEW_ALIASES[nextRaw] || nextRaw;
27
49
  }
28
50
 
29
51
  if (!DETAIL_VIEWS.has(view)) {
@@ -95,14 +117,15 @@ export function copyToClipboard(text) {
95
117
 
96
118
  try {
97
119
  if (platform === 'win32') {
98
- const proc = spawnSync('clip', [], { input: text, encoding: 'utf-8', windowsHide: true });
99
- if (proc.status === 0) return true;
100
- const ps = spawnSync('powershell.exe', ['-NoProfile', '-Command', '$input | Set-Clipboard'], {
120
+ const ps = spawnSync('powershell.exe', ['-NoProfile', '-Command', '[Console]::InputEncoding = [System.Text.Encoding]::UTF8; [Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([System.Console]::In.ReadToEnd())'], {
101
121
  input: text,
102
122
  encoding: 'utf-8',
103
123
  windowsHide: true,
104
124
  });
105
- return ps.status === 0;
125
+ if (ps.status === 0) return true;
126
+
127
+ const proc = spawnSync('clip', [], { input: text, encoding: 'utf-8', windowsHide: true });
128
+ if (proc.status === 0) return true;
106
129
  }
107
130
  if (platform === 'darwin') {
108
131
  const proc = spawnSync('pbcopy', [], { input: text, encoding: 'utf-8' });
@@ -123,8 +146,38 @@ export function copyToClipboard(text) {
123
146
  return false;
124
147
  }
125
148
 
149
+ export function readFromClipboard() {
150
+ const platform = process.platform;
151
+ try {
152
+ if (platform === 'win32') {
153
+ const ps = spawnSync('powershell.exe', ['-NoProfile', '-Command', '[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Clipboard -Raw'], {
154
+ encoding: 'utf-8',
155
+ windowsHide: true,
156
+ timeout: 1500,
157
+ });
158
+ if (ps.status === 0 && typeof ps.stdout === 'string') return ps.stdout;
159
+ } else if (platform === 'darwin') {
160
+ const proc = spawnSync('pbpaste', [], { encoding: 'utf-8', timeout: 1500 });
161
+ if (proc.status === 0 && typeof proc.stdout === 'string') return proc.stdout;
162
+ } else {
163
+ const tools = [
164
+ { cmd: 'wl-paste', args: ['--no-newline'] },
165
+ { cmd: 'xclip', args: ['-selection', 'clipboard', '-o'] },
166
+ { cmd: 'xsel', args: ['--clipboard', '--output'] },
167
+ ];
168
+ for (const t of tools) {
169
+ try {
170
+ const proc = spawnSync(t.cmd, t.args, { encoding: 'utf-8', timeout: 1500 });
171
+ if (proc.status === 0 && typeof proc.stdout === 'string') return proc.stdout;
172
+ } catch {}
173
+ }
174
+ }
175
+ } catch {}
176
+ return '';
177
+ }
178
+
126
179
  export function getCopyableContent(details, request = {}, session = null) {
127
- const isSessionReq = request.isSession || (!details && session);
180
+ const isSessionReq = Boolean(request.isSession);
128
181
  const view = request.view || 'answer';
129
182
 
130
183
  if (isSessionReq && session) {
@@ -135,6 +188,13 @@ export function getCopyableContent(details, request = {}, session = null) {
135
188
  type: 'session_json',
136
189
  };
137
190
  }
191
+ if (view === 'html' || view === 'web') {
192
+ return {
193
+ title: `Full Session HTML • ${session.id}`,
194
+ text: typeof session.getTranscriptHtml === 'function' ? session.getTranscriptHtml() : '',
195
+ type: 'session_html',
196
+ };
197
+ }
138
198
  if (view === 'text' || view === 'raw') {
139
199
  return {
140
200
  title: `Full Session Text • ${session.id}`,