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,18 +4,61 @@ import { EXPORT_DIR, SESSION_DIR as DATA_SESSION_DIR } from './appPaths.js';
4
4
 
5
5
  const SESSION_DIR = DATA_SESSION_DIR;
6
6
  const SESSION_FILE = path.join(SESSION_DIR, 'current_session.json');
7
+ const SENSITIVE_FIELD = /(?:authorization|api[-_]?key|access[-_]?token|refresh[-_]?token|secret|password|cookie|private[-_]?key)/i;
8
+
9
+ export function redactSensitiveValue(value, depth = 0) {
10
+ if (depth > 12) return '[REDACTED:DEPTH_LIMIT]';
11
+ if (typeof value === 'string') {
12
+ return value
13
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, 'Bearer [REDACTED]')
14
+ .replace(/\b(?:sk-(?:or-v1-)?|gsk_|AIza)[A-Za-z0-9._-]{12,}/g, '[REDACTED_API_KEY]')
15
+ .replace(/\b(kiku_(?:master|vip|dev)_\S{8,})/gi, '[REDACTED_KIKU_KEY]');
16
+ }
17
+ if (Array.isArray(value)) return value.map(item => redactSensitiveValue(item, depth + 1));
18
+ if (!value || typeof value !== 'object') return value;
19
+
20
+ const out = {};
21
+ for (const [key, item] of Object.entries(value)) {
22
+ out[key] = SENSITIVE_FIELD.test(key) ? '[REDACTED]' : redactSensitiveValue(item, depth + 1);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ function normalizeStorageKey(value) {
28
+ return String(value || '')
29
+ .trim()
30
+ .replace(/[^a-zA-Z0-9_-]+/g, '_')
31
+ .slice(0, 120);
32
+ }
7
33
 
8
34
  export class AgentSession {
9
35
  constructor(options = {}) {
10
- this.id = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
36
+ this.persistenceEnabled = options.persistence !== false;
37
+ this.id = options.sessionId || `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
11
38
  this.createdAt = new Date();
12
39
  this.thinkingMode = options.thinkingMode || 'hide';
40
+ const storageKey = normalizeStorageKey(options.storageKey);
41
+ this.storageFile = options.storageFile
42
+ ? path.resolve(String(options.storageFile))
43
+ : (storageKey ? path.join(SESSION_DIR, `${storageKey}.json`) : SESSION_FILE);
13
44
  this.messages = [];
14
45
  this.stats = {
15
46
  totalQueries: 0,
16
47
  totalToolCalls: 0,
17
48
  totalDurationMs: 0,
49
+ totalPromptTokens: 0,
50
+ totalCompletionTokens: 0,
51
+ totalReasoningTokens: 0,
52
+ totalTokens: 0,
53
+ totalCostUsd: 0,
54
+ };
55
+ this.lastQueryTokens = {
56
+ prompt: 0,
57
+ completion: 0,
58
+ reasoning: 0,
59
+ total: 0
18
60
  };
61
+ this.lastQueryCostUsd = 0;
19
62
  this._storageWarningShown = false;
20
63
  }
21
64
 
@@ -57,12 +100,49 @@ export class AgentSession {
57
100
  model: meta.model || null,
58
101
  createdAt: new Date(),
59
102
  durationMs: 0,
103
+ tokens: {
104
+ prompt: 0,
105
+ completion: 0,
106
+ reasoning: 0,
107
+ total: 0
108
+ },
109
+ costUsd: 0,
60
110
  parts: []
61
111
  };
62
112
  this.messages.push(msg);
63
113
  return msg;
64
114
  }
65
115
 
116
+ recordQueryTokens(tokens = {}, costUsd = 0) {
117
+ const prompt = Number(tokens.prompt || tokens.prompt_tokens || 0);
118
+ const completion = Number(tokens.completion || tokens.completion_tokens || 0);
119
+ const reasoning = Number(tokens.reasoning || tokens.reasoning_tokens || 0);
120
+ const total = Number(tokens.total || tokens.total_tokens || (prompt + completion));
121
+ const cost = Number(costUsd || 0);
122
+
123
+ this.stats.totalPromptTokens += prompt;
124
+ this.stats.totalCompletionTokens += completion;
125
+ this.stats.totalReasoningTokens += reasoning;
126
+ this.stats.totalTokens += total;
127
+ this.stats.totalCostUsd = Number((this.stats.totalCostUsd + cost).toFixed(6));
128
+
129
+ this.lastQueryTokens = {
130
+ prompt,
131
+ completion,
132
+ reasoning,
133
+ total
134
+ };
135
+ this.lastQueryCostUsd = cost;
136
+
137
+ for (let i = this.messages.length - 1; i >= 0; i--) {
138
+ if (this.messages[i].role === 'assistant') {
139
+ this.messages[i].tokens = { ...this.lastQueryTokens };
140
+ this.messages[i].costUsd = cost;
141
+ break;
142
+ }
143
+ }
144
+ }
145
+
66
146
  addReasoningPart(message, reasoningText) {
67
147
  if (!message || !Array.isArray(message.parts)) return null;
68
148
  let part = message.parts.find(p => p.type === 'reasoning');
@@ -174,16 +254,17 @@ export class AgentSession {
174
254
  }
175
255
 
176
256
  async saveToDisk() {
257
+ if (!this.persistenceEnabled) return false;
177
258
  try {
178
- await fs.mkdir(SESSION_DIR, { recursive: true });
259
+ await fs.mkdir(SESSION_DIR, { recursive: true, mode: 0o700 });
179
260
  const sessionData = {
180
261
  id: this.id,
181
262
  createdAt: this.createdAt,
182
263
  thinkingMode: this.thinkingMode,
183
- messages: this.messages,
264
+ messages: redactSensitiveValue(this.messages),
184
265
  stats: this.stats,
185
266
  };
186
- await fs.writeFile(SESSION_FILE, JSON.stringify(sessionData, null, 2), 'utf-8');
267
+ await fs.writeFile(this.storageFile, JSON.stringify(sessionData, null, 2), { encoding: 'utf-8', mode: 0o600 });
187
268
  return true;
188
269
  } catch (error) {
189
270
  if (!this._storageWarningShown) {
@@ -217,10 +298,16 @@ export class AgentSession {
217
298
  this.createdAt = sessionData.createdAt ? new Date(sessionData.createdAt) : new Date();
218
299
  this.thinkingMode = sessionData.thinkingMode || 'hide';
219
300
  this.messages = Array.isArray(sessionData.messages) ? sessionData.messages : [];
220
- this.stats = sessionData.stats || {
301
+ this.stats = {
221
302
  totalQueries: 0,
222
303
  totalToolCalls: 0,
223
- totalDurationMs: 0
304
+ totalDurationMs: 0,
305
+ totalPromptTokens: 0,
306
+ totalCompletionTokens: 0,
307
+ totalReasoningTokens: 0,
308
+ totalTokens: 0,
309
+ totalCostUsd: 0,
310
+ ...(sessionData.stats || {})
224
311
  };
225
312
  return true;
226
313
  }
@@ -232,23 +319,37 @@ export class AgentSession {
232
319
  this.stats = {
233
320
  totalQueries: 0,
234
321
  totalToolCalls: 0,
235
- totalDurationMs: 0
322
+ totalDurationMs: 0,
323
+ totalPromptTokens: 0,
324
+ totalCompletionTokens: 0,
325
+ totalReasoningTokens: 0,
326
+ totalTokens: 0,
327
+ totalCostUsd: 0
236
328
  };
329
+ this.lastQueryTokens = { prompt: 0, completion: 0, reasoning: 0, total: 0 };
330
+ this.lastQueryCostUsd = 0;
237
331
  }
238
332
 
239
333
  async loadFromDisk() {
334
+ if (!this.persistenceEnabled) return false;
240
335
  try {
241
- const data = await fs.readFile(SESSION_FILE, 'utf-8');
336
+ const data = await fs.readFile(this.storageFile, 'utf-8');
242
337
  const sessionData = JSON.parse(data);
243
338
 
244
339
  this.id = sessionData.id;
245
340
  this.createdAt = new Date(sessionData.createdAt);
246
341
  this.thinkingMode = sessionData.thinkingMode || 'hide';
247
342
  this.messages = sessionData.messages || [];
248
- this.stats = sessionData.stats || {
343
+ this.stats = {
249
344
  totalQueries: 0,
250
345
  totalToolCalls: 0,
251
- totalDurationMs: 0
346
+ totalDurationMs: 0,
347
+ totalPromptTokens: 0,
348
+ totalCompletionTokens: 0,
349
+ totalReasoningTokens: 0,
350
+ totalTokens: 0,
351
+ totalCostUsd: 0,
352
+ ...(sessionData.stats || {})
252
353
  };
253
354
  return true;
254
355
  } catch {
@@ -257,8 +358,9 @@ export class AgentSession {
257
358
  }
258
359
 
259
360
  async clearDisk() {
361
+ if (!this.persistenceEnabled) return false;
260
362
  try {
261
- await fs.unlink(SESSION_FILE);
363
+ await fs.unlink(this.storageFile);
262
364
  return true;
263
365
  } catch {
264
366
  return false;
@@ -366,6 +468,198 @@ export class AgentSession {
366
468
  return out;
367
469
  }
368
470
 
471
+ getTranscriptHtml() {
472
+ const now = new Date();
473
+ const createdStr = this.createdAt ? new Date(this.createdAt).toISOString().replace('T', ' ').slice(0, 19) : 'unknown';
474
+ const totalDurationSec = ((this.stats.totalDurationMs || 0) / 1000).toFixed(1);
475
+ const toolStats = this.getToolStats();
476
+ const toolEntries = Object.entries(toolStats);
477
+
478
+ const escapeHtml = str => String(str || '')
479
+ .replace(/&/g, '&')
480
+ .replace(/</g, '&lt;')
481
+ .replace(/>/g, '&gt;')
482
+ .replace(/"/g, '&quot;');
483
+
484
+ let toolTableHtml = '';
485
+ if (toolEntries.length > 0) {
486
+ toolTableHtml = `
487
+ <div class="card" style="margin-bottom: 24px;">
488
+ <h3>[Tools] Tool Invocations Summary</h3>
489
+ <table class="data-table">
490
+ <thead>
491
+ <tr><th>Tool Name</th><th>Invocations</th><th>OK</th><th>Not Found</th><th>Errors</th><th>Total Time</th></tr>
492
+ </thead>
493
+ <tbody>
494
+ ${toolEntries.map(([name, s]) => `
495
+ <tr>
496
+ <td><code>${escapeHtml(name)}</code></td>
497
+ <td>${s.count}</td>
498
+ <td><span class="badge ok">${s.successCount}</span></td>
499
+ <td><span class="badge warn">${s.notFoundCount}</span></td>
500
+ <td><span class="badge err">${s.errorCount}</span></td>
501
+ <td>${(s.totalDurationMs / 1000).toFixed(2)}s</td>
502
+ </tr>
503
+ `).join('')}
504
+ </tbody>
505
+ </table>
506
+ </div>`;
507
+ }
508
+
509
+ let messagesHtml = '';
510
+ let qIdx = 1;
511
+ for (const msg of this.messages) {
512
+ if (msg.role === 'user') {
513
+ const text = msg.parts?.find(p => p.type === 'text')?.content || '';
514
+ const qNum = msg.queryNumber || qIdx++;
515
+ messagesHtml += `
516
+ <div class="message user-message">
517
+ <div class="message-header">
518
+ <span class="user-badge">Question ${qNum}</span>
519
+ <span class="timestamp">${msg.createdAt ? new Date(msg.createdAt).toLocaleTimeString() : ''}</span>
520
+ </div>
521
+ <div class="message-content user-text">${escapeHtml(text)}</div>
522
+ </div>`;
523
+ } else if (msg.role === 'assistant') {
524
+ let partsContent = '';
525
+ for (const part of msg.parts || []) {
526
+ if (part.type === 'reasoning' && part.content) {
527
+ const dur = part.durationMs ? `${(part.durationMs / 1000).toFixed(1)}s` : 'Completed';
528
+ partsContent += `
529
+ <details class="reasoning-box">
530
+ <summary>[Thinking] Reasoning Trace (${dur})</summary>
531
+ <pre><code>${escapeHtml(part.content.trim())}</code></pre>
532
+ </details>`;
533
+ } else if (part.type === 'tool') {
534
+ const dur = part.durationMs !== undefined ? `${part.durationMs}ms` : '';
535
+ const statusClass = part.status === 'ok' ? 'ok' : part.status === 'not_found' ? 'warn' : 'err';
536
+ partsContent += `
537
+ <details class="tool-box">
538
+ <summary>[Tool] <code>${escapeHtml(part.name)}</code> <span class="badge ${statusClass}">${(part.status || 'ok').toUpperCase()}</span> <span class="dim">(${dur})</span></summary>
539
+ <div class="tool-detail">
540
+ <strong>Arguments:</strong>
541
+ <pre><code>${escapeHtml(JSON.stringify(part.args, null, 2))}</code></pre>
542
+ ${part.result ? `<strong>Result:</strong><pre><code>${escapeHtml(JSON.stringify(part.result, null, 2))}</code></pre>` : ''}
543
+ </div>
544
+ </details>`;
545
+ } else if (part.type === 'text' && part.content) {
546
+ const formatted = escapeHtml(part.content)
547
+ .replace(/\n\n/g, '<br><br>')
548
+ .replace(/\n/g, '<br>');
549
+ partsContent += `<div class="assistant-text">${formatted}</div>`;
550
+ }
551
+ }
552
+
553
+ messagesHtml += `
554
+ <div class="message assistant-message">
555
+ <div class="message-header">
556
+ <span class="kiku-badge">Kiku AI</span>
557
+ ${msg.model ? `<span class="model-tag">${escapeHtml(msg.model)}</span>` : ''}
558
+ </div>
559
+ <div class="message-content">${partsContent}</div>
560
+ </div>`;
561
+ }
562
+ }
563
+
564
+ return `<!DOCTYPE html>
565
+ <html lang="en">
566
+ <head>
567
+ <meta charset="UTF-8">
568
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
569
+ <title>Kiku AI Agent — Session Transcript (${escapeHtml(this.id)})</title>
570
+ <style>
571
+ :root {
572
+ --bg: #090d16;
573
+ --card-bg: rgba(15, 23, 42, 0.85);
574
+ --card-border: rgba(255, 183, 197, 0.2);
575
+ --primary: #ff7eb3;
576
+ --secondary: #ff758c;
577
+ --accent: #38bdf8;
578
+ --text: #f8fafc;
579
+ --text-dim: #94a3b8;
580
+ --ok: #10b981;
581
+ --warn: #f59e0b;
582
+ --err: #ef4444;
583
+ }
584
+ * { box-sizing: border-box; margin: 0; padding: 0; }
585
+ body {
586
+ background: var(--bg);
587
+ color: var(--text);
588
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
589
+ line-height: 1.6;
590
+ padding: 32px 16px;
591
+ }
592
+ .container { max-width: 900px; margin: 0 auto; }
593
+ .header {
594
+ background: linear-gradient(135deg, rgba(255, 126, 179, 0.15), rgba(56, 189, 248, 0.1));
595
+ border: 1px solid var(--card-border);
596
+ border-radius: 16px;
597
+ padding: 28px;
598
+ margin-bottom: 24px;
599
+ backdrop-filter: blur(12px);
600
+ }
601
+ .title { font-size: 24px; font-weight: 700; color: var(--primary); margin-bottom: 12px; }
602
+ .meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
603
+ .meta-item { background: rgba(0, 0, 0, 0.3); padding: 10px 14px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.05); }
604
+ .meta-label { font-size: 11px; text-transform: uppercase; color: var(--text-dim); }
605
+ .meta-value { font-size: 14px; font-weight: 600; color: var(--text); }
606
+ .card {
607
+ background: var(--card-bg);
608
+ border: 1px solid var(--card-border);
609
+ border-radius: 12px;
610
+ padding: 20px;
611
+ margin-bottom: 20px;
612
+ }
613
+ .card h3 { color: var(--accent); font-size: 16px; margin-bottom: 12px; }
614
+ .data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
615
+ .data-table th, .data-table td { padding: 8px 12px; text-align: left; border-bottom: 1px solid rgba(255,255,255,0.06); }
616
+ .data-table th { color: var(--text-dim); font-weight: 600; }
617
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; }
618
+ .badge.ok { background: rgba(16, 185, 129, 0.2); color: var(--ok); }
619
+ .badge.warn { background: rgba(245, 158, 11, 0.2); color: var(--warn); }
620
+ .badge.err { background: rgba(239, 68, 68, 0.2); color: var(--err); }
621
+ .message { margin-bottom: 24px; border-radius: 12px; border: 1px solid var(--card-border); overflow: hidden; }
622
+ .user-message { background: rgba(30, 41, 59, 0.6); }
623
+ .assistant-message { background: var(--card-bg); }
624
+ .message-header {
625
+ display: flex; justify-content: space-between; align-items: center;
626
+ padding: 12px 16px; background: rgba(0, 0, 0, 0.2);
627
+ border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 13px;
628
+ }
629
+ .user-badge { color: var(--accent); font-weight: 700; }
630
+ .kiku-badge { color: var(--primary); font-weight: 700; }
631
+ .model-tag { color: var(--text-dim); font-size: 11px; background: rgba(255,255,255,0.05); padding: 2px 6px; border-radius: 4px; }
632
+ .timestamp { color: var(--text-dim); font-size: 11px; }
633
+ .message-content { padding: 16px; font-size: 14px; }
634
+ .user-text { font-size: 15px; font-weight: 500; }
635
+ .assistant-text { margin-top: 8px; }
636
+ details { margin: 10px 0; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; overflow: hidden; }
637
+ summary { padding: 8px 12px; background: rgba(0,0,0,0.3); cursor: pointer; font-size: 12px; font-weight: 600; }
638
+ pre { padding: 12px; background: rgba(0,0,0,0.4); font-size: 12px; overflow-x: auto; color: #e2e8f0; }
639
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
640
+ .tool-detail { padding: 12px; font-size: 12px; }
641
+ .dim { color: var(--text-dim); }
642
+ </style>
643
+ </head>
644
+ <body>
645
+ <div class="container">
646
+ <div class="header">
647
+ <div class="title">Kiku AI Agent — Session Transcript</div>
648
+ <div class="meta-grid">
649
+ <div class="meta-item"><div class="meta-label">Session ID</div><div class="meta-value">${escapeHtml(this.id)}</div></div>
650
+ <div class="meta-item"><div class="meta-label">Total Queries</div><div class="meta-value">${this.stats.totalQueries}</div></div>
651
+ <div class="meta-item"><div class="meta-label">Tool Calls</div><div class="meta-value">${this.stats.totalToolCalls}</div></div>
652
+ <div class="meta-item"><div class="meta-label">Total Duration</div><div class="meta-value">${totalDurationSec}s</div></div>
653
+ <div class="meta-item"><div class="meta-label">Exported At</div><div class="meta-value">${now.toISOString().slice(0, 19)} UTC</div></div>
654
+ </div>
655
+ </div>
656
+ ${toolTableHtml}
657
+ ${messagesHtml}
658
+ </div>
659
+ </body>
660
+ </html>`;
661
+ }
662
+
369
663
  async exportToMarkdown(options = {}) {
370
664
  const exportDir = EXPORT_DIR;
371
665
  await fs.mkdir(exportDir, { recursive: true }).catch(() => {});
@@ -380,6 +674,20 @@ export class AgentSession {
380
674
  return { filePath, filename, format: 'markdown', size: md.length };
381
675
  }
382
676
 
677
+ async exportToHtml() {
678
+ const exportDir = EXPORT_DIR;
679
+ await fs.mkdir(exportDir, { recursive: true }).catch(() => {});
680
+
681
+ const now = new Date();
682
+ const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
683
+ const filename = `kiku-session-${timestamp}.html`;
684
+ const filePath = path.join(exportDir, filename);
685
+
686
+ const html = this.getTranscriptHtml();
687
+ await fs.writeFile(filePath, html, 'utf-8');
688
+ return { filePath, filename, format: 'html', size: html.length };
689
+ }
690
+
383
691
  async exportToJson() {
384
692
  const exportDir = EXPORT_DIR;
385
693
  await fs.mkdir(exportDir, { recursive: true }).catch(() => {});
@@ -68,7 +68,8 @@ ${options.skillContext ? `\nUser response skill:\n${options.skillContext}` : ''}
68
68
 
69
69
  const calls = (response.tool_calls || []).map(normalizeToolCall);
70
70
  if (calls.length === 0) {
71
- return { task: objective, answer: response.content || '', toolCalls, iterations: iteration + 1 };
71
+ const text = response.content || response.reasoning_content || '';
72
+ return { task: objective, answer: text, toolCalls, iterations: iteration + 1 };
72
73
  }
73
74
 
74
75
  for (const call of calls) {
@@ -104,7 +105,7 @@ ${options.skillContext ? `\nUser response skill:\n${options.skillContext}` : ''}
104
105
 
105
106
  messages.push({
106
107
  role: 'user',
107
- content: 'Stop researching and return the concise evidence report now. Mention any missing or unverified information.',
108
+ content: 'Synthesize your research findings and return the concise evidence report now in clear markdown. State key facts directly.',
108
109
  });
109
110
  const final = await this.client.chat({
110
111
  messages,
@@ -112,6 +113,10 @@ ${options.skillContext ? `\nUser response skill:\n${options.skillContext}` : ''}
112
113
  stream: false,
113
114
  signal: options.signal,
114
115
  });
115
- return { task: objective, answer: final?.content || '', toolCalls, iterations: this.maxIterations };
116
+ let finalAnswer = final?.content || final?.reasoning_content || '';
117
+ if (!finalAnswer.trim() && toolCalls.length > 0) {
118
+ finalAnswer = `Verified primary evidence from ${toolCalls.length} catalog/web tools: ` + toolCalls.map(t => t.name).join(', ');
119
+ }
120
+ return { task: objective, answer: finalAnswer, toolCalls, iterations: this.maxIterations };
116
121
  }
117
122
  }