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,23 +1,27 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { EXPORT_DIR, KIKU_DATA_DIR, SESSION_DIR } from './appPaths.js';
3
+ import { EXPORT_DIR, KIKU_DATA_DIR, SESSION_DIR } from './appPaths.js';
4
+ import { redactSensitiveValue } from './session.js';
4
5
 
5
6
  const MEMORY_DIR = KIKU_DATA_DIR;
6
7
  const MEMORY_FILE = path.join(MEMORY_DIR, 'long_term_memory.json');
7
8
  const SESSIONS_DIR = SESSION_DIR;
8
9
 
9
10
 
10
- export class AgentMemory {
11
- constructor() {
11
+ export class AgentMemory {
12
+ constructor(options = {}) {
13
+ this.persistenceEnabled = options.persistence !== false;
14
+ this.storageFile = options.storageFile ? path.resolve(String(options.storageFile)) : MEMORY_FILE;
12
15
  this.facts = [];
13
16
  this.preferences = {
14
17
  language: null,
15
18
  topicsOfInterest: [],
16
- responseStyle: 'default',
17
- userSkills: [],
18
- skillsEnabled: true,
19
- favorites: [],
20
- };
19
+ responseStyle: 'default',
20
+ userSkills: [],
21
+ skillsEnabled: true,
22
+ favorites: [],
23
+ learnedStyles: [],
24
+ };
21
25
  this.sessions = [];
22
26
  this.summaries = [];
23
27
  this.stats = {
@@ -31,215 +35,229 @@ export class AgentMemory {
31
35
  this._storageWarningShown = false;
32
36
  this._storageError = null;
33
37
  }
34
-
35
-
36
- async init() {
37
- try {
38
- await fs.mkdir(MEMORY_DIR, { recursive: true, mode: 0o700 });
39
- await fs.mkdir(SESSIONS_DIR, { recursive: true, mode: 0o700 });
40
- this._storageError = null;
41
- return true;
42
- } catch (error) {
43
- this._storageError = error;
44
- this._warnStorage(error);
45
- return false;
46
- }
47
- }
48
-
49
- _warnStorage(error) {
50
- if (this._storageWarningShown) return;
51
- this._storageWarningShown = true;
52
- console.error(`Kiku memory is running without persistence (${error?.code || 'unknown'}) in ${MEMORY_DIR}. Set KIKU_DATA_DIR to a writable user folder; Administrator privileges are not required.`);
53
- }
54
-
55
-
56
- async load() {
57
- try {
58
- await this.init();
59
- const data = await fs.readFile(MEMORY_FILE, 'utf-8');
60
- const parsed = JSON.parse(data);
61
-
62
-
63
- this.facts = (parsed.facts || []).map(f => ({
64
- ...f,
65
- createdAt: f.createdAt ? new Date(f.createdAt) : new Date(),
66
- lastAccessedAt: f.lastAccessedAt ? new Date(f.lastAccessedAt) : new Date()
67
- }));
68
-
38
+
39
+
40
+ async init() {
41
+ if (!this.persistenceEnabled) {
42
+ this._loaded = true;
43
+ return true;
44
+ }
45
+ try {
46
+ await fs.mkdir(MEMORY_DIR, { recursive: true, mode: 0o700 });
47
+ await fs.mkdir(SESSIONS_DIR, { recursive: true, mode: 0o700 });
48
+ this._storageError = null;
49
+ return true;
50
+ } catch (error) {
51
+ this._storageError = error;
52
+ this._warnStorage(error);
53
+ return false;
54
+ }
55
+ }
56
+
57
+ _warnStorage(error) {
58
+ if (this._storageWarningShown) return;
59
+ this._storageWarningShown = true;
60
+ console.error(`Kiku memory is running without persistence (${error?.code || 'unknown'}) in ${MEMORY_DIR}. Set KIKU_DATA_DIR to a writable user folder; Administrator privileges are not required.`);
61
+ }
62
+
63
+ async load() {
64
+ if (!this.persistenceEnabled) {
65
+ this._loaded = true;
66
+ return false;
67
+ }
68
+ try {
69
+ await this.init();
70
+ const data = await fs.readFile(this.storageFile, 'utf-8');
71
+ const parsed = JSON.parse(data);
72
+
73
+
74
+ this.facts = (parsed.facts || []).map(f => ({
75
+ ...f,
76
+ createdAt: f.createdAt ? new Date(f.createdAt) : new Date(),
77
+ lastAccessedAt: f.lastAccessedAt ? new Date(f.lastAccessedAt) : new Date()
78
+ }));
79
+
69
80
  const loadedPrefs = parsed.preferences || {};
70
81
  this.preferences = {
71
82
  ...this.preferences,
72
- ...loadedPrefs,
73
- userSkills: Array.isArray(loadedPrefs.userSkills) ? loadedPrefs.userSkills : [],
83
+ ...loadedPrefs,
84
+ userSkills: Array.isArray(loadedPrefs.userSkills) ? loadedPrefs.userSkills : [],
85
+ learnedStyles: Array.isArray(loadedPrefs.learnedStyles) ? loadedPrefs.learnedStyles : [],
74
86
  favorites: Array.isArray(loadedPrefs.favorites) ? loadedPrefs.favorites : [],
75
87
  topicsOfInterest: (loadedPrefs.topicsOfInterest || []).map(t => ({
76
- ...t,
77
- lastSeen: t.lastSeen ? new Date(t.lastSeen) : new Date()
78
- }))
79
- };
80
-
81
- this.sessions = parsed.sessions || [];
82
- this.summaries = parsed.summaries || [];
83
- this.stats = { ...this.stats, ...(parsed.stats || {}) };
84
- this._loaded = true;
85
- return true;
86
- } catch {
87
- this._loaded = true;
88
- return false;
89
- }
90
- }
91
-
92
-
93
- async save() {
94
- try {
95
- if (!await this.init()) return false;
96
-
97
- const serializablePrefs = {
98
- ...this.preferences,
99
- topicsOfInterest: (this.preferences.topicsOfInterest || []).map(t => ({
100
- ...t,
101
- lastSeen: t.lastSeen ? t.lastSeen.toISOString() : null
102
- }))
103
- };
104
- const data = {
105
- facts: this.facts.map(f => ({
106
- ...f,
107
- createdAt: f.createdAt ? f.createdAt.toISOString() : f.createdAt,
108
- lastAccessedAt: f.lastAccessedAt ? f.lastAccessedAt.toISOString() : f.lastAccessedAt
109
- })),
110
- preferences: serializablePrefs,
111
- sessions: this.sessions,
112
- summaries: this.summaries,
113
- stats: this.stats,
114
- lastSavedAt: new Date().toISOString()
115
- };
116
- await fs.writeFile(MEMORY_FILE, JSON.stringify(data, null, 2), 'utf-8');
117
- return true;
118
- } catch (error) {
119
- this._warnStorage(error);
120
- return false;
121
- }
122
- }
123
-
124
- addFact(fact, meta = {}) {
125
- const entry = {
126
- id: `fact_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
127
- content: fact,
128
- category: meta.category || 'general',
129
- source: meta.source || 'conversation',
130
- confidence: meta.confidence || 'high',
131
- createdAt: new Date(),
132
- lastAccessedAt: new Date(),
133
- accessCount: 0,
134
- tags: meta.tags || []
135
- };
136
- this.facts.push(entry);
137
- this.stats.totalFacts = this.facts.length;
138
- this.save().catch(() => {});
139
- return entry;
140
- }
141
-
142
-
143
- scoreFactRelevance(fact, queryTerms = [], now = Date.now()) {
144
- if (!fact) return 0;
145
- const content = (fact.content || '').toLowerCase();
146
- const tags = (fact.tags || []).map(t => String(t).toLowerCase());
147
-
148
- let matchScore = 0;
149
- if (queryTerms.length > 0) {
150
- for (const term of queryTerms) {
151
- if (!term || term.length < 2) continue;
152
- if (tags.some(t => t.includes(term))) matchScore += 3.0;
153
- if (content.includes(term)) matchScore += 2.0;
154
- }
155
- } else {
156
- matchScore = 1.0;
157
- }
158
-
159
- const createdTime = fact.createdAt ? new Date(fact.createdAt).getTime() : now;
160
- const ageInDays = Math.max(0, (now - createdTime) / (1000 * 60 * 60 * 24));
161
- const recencyDecay = Math.exp(-ageInDays / 45);
162
-
163
- const accessBoost = Math.log10(1 + (fact.accessCount || 0) * 2);
164
-
165
- return (matchScore * (0.6 + 0.4 * recencyDecay)) + accessBoost;
166
- }
167
-
168
- getRelevantFacts(query = '', limit = 8) {
169
- if (!this.facts || this.facts.length === 0) return [];
170
- const q = String(query || '').toLowerCase().trim();
171
- const terms = q ? q.split(/\s+/).filter(t => t.length >= 2) : [];
172
- const now = Date.now();
173
-
174
- const scored = this.facts.map(fact => ({
175
- fact,
176
- score: this.scoreFactRelevance(fact, terms, now)
177
- }));
178
-
179
- scored.sort((a, b) => b.score - a.score);
180
- const top = scored.slice(0, limit).map(s => s.fact);
181
-
182
- for (const f of top) {
183
- f.lastAccessedAt = new Date();
184
- f.accessCount = (f.accessCount || 0) + 1;
185
- }
186
- return top;
187
- }
188
-
189
- searchFacts(query, options = {}) {
190
- const { category, limit = 10 } = options;
191
- const q = (query || '').toLowerCase().trim();
192
-
193
- let results = this.facts;
194
-
195
- if (category) {
196
- results = results.filter(f => f.category === category);
197
- }
198
-
199
- if (q) {
200
- results = results.filter(f =>
201
- f.content.toLowerCase().includes(q) ||
202
- f.tags.some(t => t.toLowerCase().includes(q))
203
- );
204
- }
205
-
206
- const terms = q ? q.split(/\s+/).filter(t => t.length >= 2) : [];
207
- const now = Date.now();
208
-
209
- results.sort((a, b) => {
210
- const scoreA = this.scoreFactRelevance(a, terms, now);
211
- const scoreB = this.scoreFactRelevance(b, terms, now);
212
- return scoreB - scoreA;
213
- });
214
-
215
- for (const fact of results.slice(0, limit)) {
216
- fact.lastAccessedAt = new Date();
217
- fact.accessCount = (fact.accessCount || 0) + 1;
218
- }
219
-
220
- return results.slice(0, limit);
221
- }
222
-
223
- removeFact(factId) {
224
- const idx = this.facts.findIndex(f => f.id === factId);
225
- if (idx === -1) return false;
226
- this.facts.splice(idx, 1);
227
- this.stats.totalFacts = this.facts.length;
228
- this.save().catch(() => {});
229
- return true;
230
- }
231
-
232
-
233
- getFactsByCategory() {
234
- const grouped = {};
235
- for (const fact of this.facts) {
236
- const cat = fact.category || 'general';
237
- if (!grouped[cat]) grouped[cat] = [];
238
- grouped[cat].push(fact);
239
- }
240
- return grouped;
241
- }
242
-
88
+ ...t,
89
+ lastSeen: t.lastSeen ? new Date(t.lastSeen) : new Date()
90
+ }))
91
+ };
92
+
93
+ this.sessions = parsed.sessions || [];
94
+ this.summaries = parsed.summaries || [];
95
+ this.stats = { ...this.stats, ...(parsed.stats || {}) };
96
+ this._loaded = true;
97
+ return true;
98
+ } catch {
99
+ this._loaded = true;
100
+ return false;
101
+ }
102
+ }
103
+
104
+ async save() {
105
+ if (!this.persistenceEnabled) return false;
106
+ try {
107
+ if (!await this.init()) return false;
108
+
109
+ const serializablePrefs = {
110
+ ...this.preferences,
111
+ topicsOfInterest: (this.preferences.topicsOfInterest || []).map(t => ({
112
+ ...t,
113
+ lastSeen: t.lastSeen ? t.lastSeen.toISOString() : null
114
+ }))
115
+ };
116
+ const data = {
117
+ facts: this.facts.map(f => ({
118
+ ...f,
119
+ createdAt: f.createdAt ? f.createdAt.toISOString() : f.createdAt,
120
+ lastAccessedAt: f.lastAccessedAt ? f.lastAccessedAt.toISOString() : f.lastAccessedAt
121
+ })),
122
+ preferences: serializablePrefs,
123
+ sessions: this.sessions,
124
+ summaries: this.summaries,
125
+ stats: this.stats,
126
+ lastSavedAt: new Date().toISOString()
127
+ };
128
+ await fs.writeFile(this.storageFile, JSON.stringify(data, null, 2), { encoding: 'utf-8', mode: 0o600 });
129
+ return true;
130
+ } catch (error) {
131
+ this._warnStorage(error);
132
+ return false;
133
+ }
134
+ }
135
+
136
+ addFact(fact, meta = {}) {
137
+ const entry = {
138
+ id: `fact_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
139
+ content: fact,
140
+ category: meta.category || 'general',
141
+ source: meta.source || 'conversation',
142
+ confidence: meta.confidence || 'high',
143
+ createdAt: new Date(),
144
+ lastAccessedAt: new Date(),
145
+ accessCount: 0,
146
+ tags: meta.tags || []
147
+ };
148
+ this.facts.push(entry);
149
+ this.stats.totalFacts = this.facts.length;
150
+ this.save().catch(() => {});
151
+ return entry;
152
+ }
153
+
154
+ scoreFactRelevance(fact, queryTerms = [], now = Date.now()) {
155
+ if (!fact) return 0;
156
+ const content = (fact.content || '').toLowerCase();
157
+ const tags = (fact.tags || []).map(t => String(t).toLowerCase());
158
+ const category = (fact.category || '').toLowerCase();
159
+
160
+ let matchScore = 0;
161
+ if (queryTerms.length > 0) {
162
+ for (const term of queryTerms) {
163
+ if (!term || term.length < 2) continue;
164
+ const termLenBonus = Math.min(2.0, term.length / 4);
165
+
166
+ if (tags.includes(term)) {
167
+ matchScore += 5.0 * termLenBonus;
168
+ } else if (tags.some(t => t.includes(term))) {
169
+ matchScore += 3.0 * termLenBonus;
170
+ }
171
+
172
+ if (category === term || category.includes(term)) {
173
+ matchScore += 2.5;
174
+ }
175
+
176
+ const wordRegex = new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
177
+ if (wordRegex.test(content)) {
178
+ matchScore += 3.5 * termLenBonus;
179
+ } else if (content.includes(term)) {
180
+ matchScore += 1.8 * termLenBonus;
181
+ }
182
+ }
183
+ } else {
184
+ matchScore = 1.0;
185
+ }
186
+
187
+ const createdTime = fact.createdAt ? new Date(fact.createdAt).getTime() : now;
188
+ const ageInDays = Math.max(0, (now - createdTime) / (1000 * 60 * 60 * 24));
189
+ const recencyDecay = Math.exp(-ageInDays / 45);
190
+
191
+ const accessBoost = Math.log10(1 + (fact.accessCount || 0) * 2);
192
+ const confidenceWeight = typeof fact.confidence === 'number' ? Math.max(0.5, Math.min(1.5, fact.confidence)) : 1.0;
193
+
194
+ return ((matchScore * confidenceWeight) * (0.6 + 0.4 * recencyDecay)) + accessBoost;
195
+ }
196
+
197
+ getRelevantFacts(query = '', limit = 8) {
198
+ if (!this.facts || this.facts.length === 0) return [];
199
+ const q = String(query || '').toLowerCase().trim();
200
+ const terms = q ? q.split(/\s+/).filter(t => t.length >= 2) : [];
201
+ const now = Date.now();
202
+
203
+ const scored = this.facts.map(fact => ({
204
+ fact,
205
+ score: this.scoreFactRelevance(fact, terms, now)
206
+ }));
207
+
208
+ scored.sort((a, b) => b.score - a.score);
209
+ const top = scored.slice(0, limit).map(s => s.fact);
210
+
211
+ for (const f of top) {
212
+ f.lastAccessedAt = new Date();
213
+ f.accessCount = (f.accessCount || 0) + 1;
214
+ }
215
+ return top;
216
+ }
217
+
218
+ searchFacts(query, options = {}) {
219
+ const { category, limit = 10 } = options;
220
+ const q = (query || '').toLowerCase().trim();
221
+
222
+ let results = this.facts;
223
+
224
+ if (category) {
225
+ results = results.filter(f => f.category === category);
226
+ }
227
+
228
+ if (q) {
229
+ results = results.filter(f =>
230
+ f.content.toLowerCase().includes(q) ||
231
+ f.tags.some(t => t.toLowerCase().includes(q))
232
+ );
233
+ }
234
+
235
+ const terms = q ? q.split(/\s+/).filter(t => t.length >= 2) : [];
236
+ const now = Date.now();
237
+
238
+ results.sort((a, b) => {
239
+ const scoreA = this.scoreFactRelevance(a, terms, now);
240
+ const scoreB = this.scoreFactRelevance(b, terms, now);
241
+ return scoreB - scoreA;
242
+ });
243
+
244
+ for (const fact of results.slice(0, limit)) {
245
+ fact.lastAccessedAt = new Date();
246
+ fact.accessCount = (fact.accessCount || 0) + 1;
247
+ }
248
+
249
+ return results.slice(0, limit);
250
+ }
251
+
252
+ removeFact(factId) {
253
+ const idx = this.facts.findIndex(f => f.id === factId);
254
+ if (idx === -1) return false;
255
+ this.facts.splice(idx, 1);
256
+ this.stats.totalFacts = this.facts.length;
257
+ this.save().catch(() => {});
258
+ return true;
259
+ }
260
+
243
261
  setPreference(key, value) {
244
262
  this.preferences[key] = value;
245
263
  this.save().catch(() => {});
@@ -344,6 +362,28 @@ export class AgentMemory {
344
362
  return this.preferences.userSkills.map((skill, index) => `${index + 1}. ${skill.instruction}`).join('\n');
345
363
  }
346
364
 
365
+ applyFeedbackDirectives(directives = []) {
366
+ const clean = (Array.isArray(directives) ? directives : [])
367
+ .filter(d => d && typeof d.text === 'string' && d.text.trim())
368
+ .slice(0, 6)
369
+ .map(d => ({
370
+ id: String(d.id || '').slice(0, 60),
371
+ kind: d.kind === 'avoid' ? 'avoid' : 'prefer',
372
+ text: String(d.text).replace(/\s+/g, ' ').trim().slice(0, 200),
373
+ support: Number.isFinite(Number(d.support)) ? Number(d.support) : 0,
374
+ updatedAt: d.updatedAt || new Date().toISOString(),
375
+ }));
376
+ this.preferences.learnedStyles = clean;
377
+ this.save().catch(() => {});
378
+ return clean;
379
+ }
380
+
381
+ buildFeedbackStyleContext() {
382
+ const styles = Array.isArray(this.preferences.learnedStyles) ? this.preferences.learnedStyles : [];
383
+ if (styles.length === 0) return '';
384
+ return styles.map(s => `- ${s.text}`).join('\n');
385
+ }
386
+
347
387
 
348
388
  async savePreferences() {
349
389
  await this.save();
@@ -376,7 +416,8 @@ export class AgentMemory {
376
416
  }
377
417
 
378
418
 
379
- async archiveSession(sessionData) {
419
+ async archiveSession(sessionData) {
420
+ if (!this.persistenceEnabled) return null;
380
421
  const archive = {
381
422
  id: sessionData.id,
382
423
  createdAt: sessionData.createdAt,
@@ -391,8 +432,10 @@ export class AgentMemory {
391
432
  let archivedToDisk = false;
392
433
  try {
393
434
  if (!await this.init()) return null;
394
- const sessionFile = path.join(SESSIONS_DIR, `${sessionData.id}.json`);
395
- await fs.writeFile(sessionFile, JSON.stringify(sessionData, null, 2), 'utf-8');
435
+ const safeId = String(sessionData.id || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 100);
436
+ if (!safeId) return null;
437
+ const sessionFile = path.join(SESSIONS_DIR, `${safeId}.json`);
438
+ await fs.writeFile(sessionFile, JSON.stringify(redactSensitiveValue(sessionData), null, 2), { encoding: 'utf-8', mode: 0o600 });
396
439
  archivedToDisk = true;
397
440
  } catch (err) {
398
441
  this._warnStorage(err);
@@ -412,9 +455,12 @@ export class AgentMemory {
412
455
  .slice(0, limit);
413
456
  }
414
457
 
415
- async loadArchivedSession(sessionId) {
416
- try {
417
- const sessionFile = path.join(SESSIONS_DIR, `${sessionId}.json`);
458
+ async loadArchivedSession(sessionId) {
459
+ if (!this.persistenceEnabled) return null;
460
+ try {
461
+ const safeId = String(sessionId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 100);
462
+ if (!safeId) return null;
463
+ const sessionFile = path.join(SESSIONS_DIR, `${safeId}.json`);
418
464
  const data = await fs.readFile(sessionFile, 'utf-8');
419
465
  return JSON.parse(data);
420
466
  } catch {
@@ -507,7 +553,7 @@ export class AgentMemory {
507
553
  }
508
554
 
509
555
 
510
- buildMemoryContext(currentQuery = '') {
556
+ buildMemoryContext(currentQuery = '') {
511
557
  const parts = [];
512
558
 
513
559
  if (this.preferences.language) {
@@ -519,6 +565,11 @@ export class AgentMemory {
519
565
  parts.push(`User-taught response skills (follow these preferences unless they conflict with safety or verified facts):\n${userSkillContext}`);
520
566
  }
521
567
 
568
+ const feedbackStyleContext = this.buildFeedbackStyleContext();
569
+ if (feedbackStyleContext) {
570
+ parts.push(`Learned answer-style preferences (distilled from this user's like/dislike feedback — answer this way unless it conflicts with safety or verified facts):\n${feedbackStyleContext}`);
571
+ }
572
+
522
573
  const topTopics = this.getTopTopics(5);
523
574
  if (topTopics.length > 0) {
524
575
  const topicList = topTopics.map(t => `${t.topic} (${t.count}x)`).join(', ');
@@ -545,10 +596,18 @@ export class AgentMemory {
545
596
 
546
597
  if (parts.length === 0) return '';
547
598
 
548
- return `\n\n=== LONG-TERM MEMORY CONTEXT ===\n${parts.join('\n\n')}\n=== END MEMORY CONTEXT ===`;
549
- }
550
-
551
- getStatus() {
599
+ return `\n\n=== LONG-TERM MEMORY CONTEXT ===\n${parts.join('\n\n')}\n=== END MEMORY CONTEXT ===`;
600
+ }
601
+
602
+ getFactsByCategory() {
603
+ return this.facts.reduce((counts, fact) => {
604
+ const category = String(fact?.category || 'general');
605
+ counts[category] = (counts[category] || 0) + 1;
606
+ return counts;
607
+ }, {});
608
+ }
609
+
610
+ getStatus() {
552
611
  return {
553
612
  factsCount: this.facts.length,
554
613
  factsByCategory: this.getFactsByCategory(),
@@ -726,7 +785,7 @@ export class AgentMemory {
726
785
 
727
786
  async getStorageSize() {
728
787
  try {
729
- const stat = await fs.stat(MEMORY_FILE);
788
+ const stat = await fs.stat(this.storageFile);
730
789
  return stat.size;
731
790
  } catch {
732
791
  return 0;