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,6 +1,7 @@
1
1
  import { ResearchSubagent } from './subagent.js';
2
2
  import { executeTool } from './tools.js';
3
3
  import { eventBus, Events } from './eventBus.js';
4
+ import { mapConcurrent } from './suggestions.js';
4
5
 
5
6
  const CATALOG_TOOLS = Object.freeze([
6
7
  'global_search', 'get_profile_and_fandom', 'search_works', 'get_work_detail',
@@ -78,13 +79,30 @@ export class ResearchOrchestrator {
78
79
  cancel() {
79
80
  if (!this.active) return false;
80
81
  this.active.controller.abort();
82
+ this.active = null;
81
83
  return true;
82
84
  }
83
85
 
84
86
  async run(question, options = {}) {
85
87
  const query = String(question || '').trim();
86
88
  if (!query) throw new Error('A research question is required.');
87
- if (this.active) throw new Error('A research job is already running.');
89
+
90
+
91
+ if (this.active && this.active.question === query && this.active.promise) {
92
+ try {
93
+ return await this.active.promise;
94
+ } catch (err) {
95
+
96
+ }
97
+ }
98
+
99
+
100
+ if (this.active) {
101
+ try {
102
+ this.active.controller.abort();
103
+ } catch {}
104
+ this.active = null;
105
+ }
88
106
 
89
107
  const controller = new AbortController();
90
108
  const abortParent = () => controller.abort(options.signal?.reason);
@@ -96,6 +114,7 @@ export class ResearchOrchestrator {
96
114
  startedAt: new Date().toISOString(),
97
115
  controller,
98
116
  workers: new Map(plan.map(task => [task.id, { id: task.id, label: task.label, state: 'pending', toolCalls: 0 }])),
117
+ promise: null,
99
118
  };
100
119
  const sharedToolCache = new Map();
101
120
  const cachedExecute = async (name, args) => {
@@ -113,90 +132,113 @@ export class ResearchOrchestrator {
113
132
  steps: plan.map(p => ({ id: p.id, label: p.label, state: 'pending', objective: p.objective }))
114
133
  });
115
134
 
116
- try {
117
- const settled = await Promise.allSettled(plan.map(async task => {
118
- const status = job.workers.get(task.id);
119
- status.state = 'running';
120
- options.onProgress?.({ type: 'worker-start', id: task.id, label: task.label });
121
- eventBus.emit(Events.AGENT_PLAN_STEP_UPDATE, {
122
- planId: job.id,
123
- stepId: task.id,
124
- state: 'running',
125
- label: task.label,
135
+ const isLocalProvider = Boolean(
136
+ this.client?.provider === 'lmstudio' ||
137
+ this.client?.provider === 'ollama' ||
138
+ this.client?.isLocal ||
139
+ this.client?.getProviderInfo?.()?.isLocal
140
+ );
141
+ const concurrency = isLocalProvider ? 1 : Math.min(this.maxWorkers, plan.length);
142
+
143
+ const executeJob = async () => {
144
+ try {
145
+ const settled = await mapConcurrent(plan, concurrency, async task => {
146
+ const status = job.workers.get(task.id);
147
+ status.state = 'running';
148
+ options.onProgress?.({ type: 'worker-start', id: task.id, label: task.label });
149
+ eventBus.emit(Events.AGENT_PLAN_STEP_UPDATE, {
150
+ planId: job.id,
151
+ stepId: task.id,
152
+ state: 'running',
153
+ label: task.label,
154
+ });
155
+
156
+ const worker = new ResearchSubagent({
157
+ client: this.client,
158
+ executeTool: cachedExecute,
159
+ allowedTools: task.tools,
160
+ maxIterations: this.maxIterations,
161
+ });
162
+ const result = await worker.run(task.objective, {
163
+ signal: controller.signal,
164
+ skillContext: options.skillContext,
165
+ onProgress: event => {
166
+ if (event.type === 'tool') status.toolCalls += 1;
167
+ options.onProgress?.({ ...event, workerId: task.id, label: task.label });
168
+ },
169
+ });
170
+ status.state = 'complete';
171
+ options.onProgress?.({ type: 'worker-complete', id: task.id, label: task.label });
172
+ eventBus.emit(Events.AGENT_PLAN_STEP_UPDATE, {
173
+ planId: job.id,
174
+ stepId: task.id,
175
+ state: 'complete',
176
+ label: task.label,
177
+ });
178
+ return { id: task.id, label: task.label, ...result };
126
179
  });
127
180
 
128
- const worker = new ResearchSubagent({
129
- client: this.client,
130
- executeTool: cachedExecute,
131
- allowedTools: task.tools,
132
- maxIterations: this.maxIterations,
181
+ const reports = settled.map((entry, index) => {
182
+ if (entry.status === 'fulfilled') return entry.value;
183
+ const task = plan[index];
184
+ const status = job.workers.get(task.id);
185
+ status.state = controller.signal.aborted ? 'cancelled' : 'failed';
186
+ status.error = entry.reason?.message || 'Unknown worker error';
187
+ return { id: task.id, label: task.label, answer: '', error: status.error, toolCalls: [] };
133
188
  });
134
- const result = await worker.run(task.objective, {
189
+ if (controller.signal.aborted) throw controller.signal.reason || new DOMException('Aborted', 'AbortError');
190
+
191
+ const usable = reports.filter(report => report.answer);
192
+ if (usable.length === 0) {
193
+ const { webExtract } = await import('./webSearch.js');
194
+ const fallbackRes = await webExtract(query);
195
+ if (fallbackRes?.text) {
196
+ usable.push({ label: 'Live Web Intelligence', answer: fallbackRes.text, sources: fallbackRes.sources || [] });
197
+ } else {
198
+ throw new Error('All research workers failed and no web intelligence was returned.');
199
+ }
200
+ }
201
+ const synthesisPrompt = options.mode === 'verify'
202
+ ? 'Return a clear verdict: VERIFIED, UNVERIFIED, CONFLICTING, or FALSE. Explain briefly and cite only URLs present in the reports.'
203
+ : 'Synthesize a comprehensive, detailed, and polished answer with verified facts. Cite only URLs present in the reports.';
204
+ const synthesis = await this.client.chat({
205
+ messages: [
206
+ { role: 'system', content: `You are the Kiku research editor. ${synthesisPrompt} Never invent a source, date, title, or URL. Match the language of the question. Do not reveal hidden reasoning.` },
207
+ { role: 'user', content: JSON.stringify({ question: query, reports: usable.map(({ label, answer }) => ({ label, answer })) }) },
208
+ ],
209
+ temperature: 0.1,
210
+ stream: false,
135
211
  signal: controller.signal,
136
- skillContext: options.skillContext,
137
- onProgress: event => {
138
- if (event.type === 'tool') status.toolCalls += 1;
139
- options.onProgress?.({ ...event, workerId: task.id, label: task.label });
140
- },
141
212
  });
142
- status.state = 'complete';
143
- options.onProgress?.({ type: 'worker-complete', id: task.id, label: task.label });
144
- eventBus.emit(Events.AGENT_PLAN_STEP_UPDATE, {
213
+ const answer = synthesis?.content || usable.map(report => `### ${report.label}\n${report.answer}`).join('\n\n');
214
+ const sources = uniqueUrls(usable.map(report => report.answer).join('\n'));
215
+ const result = {
216
+ id: job.id,
217
+ mode: options.mode || 'research',
218
+ question: query,
219
+ answer,
220
+ sources,
221
+ reports,
222
+ startedAt: job.startedAt,
223
+ completedAt: new Date().toISOString(),
224
+ };
225
+ this.lastResult = result;
226
+ options.onProgress?.({ type: 'complete', id: job.id, sources: sources.length });
227
+ eventBus.emit(Events.AGENT_PLAN_COMPLETE, {
145
228
  planId: job.id,
146
- stepId: task.id,
147
- state: 'complete',
148
- label: task.label,
229
+ sourcesCount: sources.length,
230
+ reportsCount: reports.length,
149
231
  });
150
- return { id: task.id, label: task.label, ...result };
151
- }));
152
-
153
- const reports = settled.map((entry, index) => {
154
- if (entry.status === 'fulfilled') return entry.value;
155
- const task = plan[index];
156
- const status = job.workers.get(task.id);
157
- status.state = controller.signal.aborted ? 'cancelled' : 'failed';
158
- status.error = entry.reason?.message || 'Unknown worker error';
159
- return { id: task.id, label: task.label, answer: '', error: status.error, toolCalls: [] };
160
- });
161
- if (controller.signal.aborted) throw controller.signal.reason || new DOMException('Aborted', 'AbortError');
162
-
163
- const usable = reports.filter(report => report.answer);
164
- if (usable.length === 0) throw new Error('All research workers failed.');
165
- const synthesisPrompt = options.mode === 'verify'
166
- ? 'Return a clear verdict: VERIFIED, UNVERIFIED, CONFLICTING, or FALSE. Explain briefly and cite only URLs present in the reports.'
167
- : 'Synthesize a direct, polished answer. Separate verified facts, conflicts, and missing evidence. Cite only URLs present in the reports.';
168
- const synthesis = await this.client.chat({
169
- messages: [
170
- { role: 'system', content: `You are the Kiku research editor. ${synthesisPrompt} Never invent a source, date, title, or URL. Match the language of the question. Do not reveal hidden reasoning.` },
171
- { role: 'user', content: JSON.stringify({ question: query, reports: usable.map(({ label, answer }) => ({ label, answer })) }) },
172
- ],
173
- temperature: 0.1,
174
- stream: false,
175
- signal: controller.signal,
176
- });
177
- const answer = synthesis?.content || usable.map(report => `### ${report.label}\n${report.answer}`).join('\n\n');
178
- const sources = uniqueUrls(usable.map(report => report.answer).join('\n'));
179
- const result = {
180
- id: job.id,
181
- mode: options.mode || 'research',
182
- question: query,
183
- answer,
184
- sources,
185
- reports,
186
- startedAt: job.startedAt,
187
- completedAt: new Date().toISOString(),
188
- };
189
- this.lastResult = result;
190
- options.onProgress?.({ type: 'complete', id: job.id, sources: sources.length });
191
- eventBus.emit(Events.AGENT_PLAN_COMPLETE, {
192
- planId: job.id,
193
- sourcesCount: sources.length,
194
- reportsCount: reports.length,
195
- });
196
- return result;
197
- } finally {
198
- options.signal?.removeEventListener('abort', abortParent);
199
- this.active = null;
200
- }
232
+ return result;
233
+ } finally {
234
+ options.signal?.removeEventListener('abort', abortParent);
235
+ if (this.active?.id === job.id) {
236
+ this.active = null;
237
+ }
238
+ }
239
+ };
240
+
241
+ job.promise = executeJob();
242
+ return job.promise;
201
243
  }
202
244
  }
@@ -1,6 +1,5 @@
1
1
  import path from 'node:path';
2
-
3
-
2
+ import { lookup as dnsLookup } from 'node:dns/promises';
4
3
 
5
4
  const MAX_QUERY_LENGTH = 32768;
6
5
  const MAX_PATH_LENGTH = 2048;
@@ -66,10 +65,24 @@ const BLOCKED_HOST_PATTERNS = [
66
65
  const DANGEROUS_EXTENSIONS = new Set([
67
66
  '.sh', '.bash', '.zsh', '.ps1', '.cmd', '.bat', '.exe', '.msi',
68
67
  '.com', '.scr', '.pif', '.vbs', '.vbe', '.js', '.jse', '.wsf',
69
- '.wsh', '.reg',
68
+ '.wsh', '.reg', '.pem', '.key', '.p12', '.pfx', '.der',
70
69
  ]);
71
70
 
72
71
 
72
+ const SENSITIVE_FILENAMES = [
73
+ /(^|[\\/])\.env(\.|$)/i,
74
+ /(^|[\\/])cookies\.txt$/i,
75
+ /(^|[\\/])\.(git|npmrc|netrc|ssh)([\\/]|$)/i,
76
+ /(^|[\\/])\.kiku_sessions([\\/]|$)/i,
77
+ /\.pem$/i,
78
+ /\.key$/i,
79
+ /\.p12$/i,
80
+ /\.pfx$/i,
81
+ /secret/i,
82
+ /credential/i,
83
+ ];
84
+
85
+
73
86
  export function stripAnsi(str) {
74
87
  if (typeof str !== 'string') return '';
75
88
  return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
@@ -138,6 +151,12 @@ export function sanitizeFilePath(filePath, projectRoot = process.cwd(), { blockD
138
151
  return { safe: false, resolvedPath: resolved, reason: 'Path escapes project root (traversal detected)' };
139
152
  }
140
153
 
154
+ for (const pattern of SENSITIVE_FILENAMES) {
155
+ if (pattern.test(resolved)) {
156
+ return { safe: false, resolvedPath: resolved, reason: 'Sensitive file path blocked (secrets/dotfiles)' };
157
+ }
158
+ }
159
+
141
160
  if (blockDangerousExtensions) {
142
161
  const normalizedPath = resolved.replace(/[ .]+$/g, '');
143
162
  const extension = path.extname(normalizedPath).toLowerCase();
@@ -150,6 +169,40 @@ export function sanitizeFilePath(filePath, projectRoot = process.cwd(), { blockD
150
169
  }
151
170
 
152
171
 
172
+ function isPrivateIPv4(ip) {
173
+ const parts = ip.split('.').map(Number);
174
+ if (parts.length !== 4) return false;
175
+ const [a, b] = parts;
176
+ if (a === 10) return true;
177
+ if (a === 127) return true;
178
+ if (a === 0) return true;
179
+ if (a === 169 && b === 254) return true;
180
+ if (a === 172 && b >= 16 && b <= 31) return true;
181
+ if (a === 192 && b === 168) return true;
182
+ if (a === 100 && b >= 64 && b <= 127) return true;
183
+ if (a === 198 && (b === 18 || b === 19)) return true;
184
+ if (a >= 224) return true;
185
+ return false;
186
+ }
187
+
188
+ function isPrivateIPv6(ip) {
189
+ const normalized = ip.toLowerCase();
190
+ if (normalized === '::' || normalized === '::1') return true;
191
+ if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true;
192
+ if (normalized.startsWith('fe80') || normalized.startsWith('fe8') || normalized.startsWith('fe9') || normalized.startsWith('fea') || normalized.startsWith('feb')) return true;
193
+ if (normalized.startsWith('::ffff:')) {
194
+ const v4 = normalized.slice(7);
195
+ return isPrivateIPv4(v4);
196
+ }
197
+ if (normalized.startsWith('2001:db8:')) return true;
198
+ if (normalized.startsWith('::')) return true;
199
+ return false;
200
+ }
201
+
202
+ function isIpAddress(hostname) {
203
+ return /^\d+\.\d+\.\d+\.\d+$/.test(hostname) || /^[0-9a-f:]+$/i.test(hostname);
204
+ }
205
+
153
206
  export function sanitizeUrl(urlStr) {
154
207
  if (typeof urlStr !== 'string' || !urlStr.trim()) {
155
208
  return { safe: false, reason: 'URL is empty or not a string' };
@@ -173,6 +226,10 @@ export function sanitizeUrl(urlStr) {
173
226
  return { safe: false, reason: `Unsafe protocol: ${url.protocol} (only http/https allowed)` };
174
227
  }
175
228
 
229
+ if (url.username || url.password) {
230
+ return { safe: false, reason: 'Credentials embedded in URLs are not allowed' };
231
+ }
232
+
176
233
 
177
234
  const hostname = url.hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '').toLowerCase();
178
235
  for (const pattern of BLOCKED_HOST_PATTERNS) {
@@ -181,7 +238,36 @@ export function sanitizeUrl(urlStr) {
181
238
  }
182
239
  }
183
240
 
184
- return { safe: true, url };
241
+ if (!isIpAddress(hostname)) {
242
+ url.__pendingDnsCheck = true;
243
+ }
244
+
245
+ return { safe: true, url, hostname };
246
+ }
247
+
248
+
249
+ export async function assertSafeUrlHostname(hostname) {
250
+ if (!hostname || typeof hostname !== 'string') return { safe: false, reason: 'Missing hostname' };
251
+ const clean = hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '').toLowerCase();
252
+
253
+ if (isIpAddress(clean)) {
254
+ if (isPrivateIPv4(clean) || isPrivateIPv6(clean)) {
255
+ return { safe: false, reason: `Resolved host is private/internal: ${clean}` };
256
+ }
257
+ return { safe: true };
258
+ }
259
+
260
+ try {
261
+ const addresses = await dnsLookup(clean, { all: true });
262
+ for (const { address } of addresses) {
263
+ if (isPrivateIPv4(address) || isPrivateIPv6(address)) {
264
+ return { safe: false, reason: `Host resolved to private/internal IP: ${address}` };
265
+ }
266
+ }
267
+ return { safe: true };
268
+ } catch {
269
+ return { safe: false, reason: 'Unable to resolve hostname for SSRF check' };
270
+ }
185
271
  }
186
272
 
187
273
 
@@ -244,7 +330,7 @@ export function sanitizeToolArgs(toolName, args) {
244
330
  }
245
331
 
246
332
  case 'fetch_api': {
247
-
333
+
248
334
  const endpoint = sanitized.endpoint;
249
335
  if (endpoint && typeof endpoint === 'string') {
250
336
  const clean = stripAnsi(endpoint).trim();
@@ -253,6 +339,14 @@ export function sanitizeToolArgs(toolName, args) {
253
339
  }
254
340
  sanitized.endpoint = clean;
255
341
  }
342
+
343
+ const baseUrl = sanitized.baseUrl;
344
+ if (baseUrl) {
345
+ const check = sanitizeUrl(baseUrl);
346
+ if (!check.safe) {
347
+ return { safe: false, sanitized: {}, warnings: [`API base URL rejected: ${check.reason}`] };
348
+ }
349
+ }
256
350
  break;
257
351
  }
258
352
 
@@ -290,18 +384,15 @@ export function sanitizeResponse(response) {
290
384
 
291
385
 
292
386
  const leakPatterns = [
293
- /system\s*prompt\s*[:=]/i,
294
- /you\s*are\s*(actually|really)\s+(a|an)\s+/i,
295
- /my\s*instructions\s*(are|say|tell)/i,
296
- /i\s*was\s*(told|instructed|programmed)\s+to/i,
387
+ /system\s*prompt\s*[:=]/gi,
388
+ /you\s*are\s*(actually|really)\s+(a|an)\s+/gi,
389
+ /my\s*instructions\s*(are|say|tell)/gi,
390
+ /i\s*was\s*(told|instructed|programmed)\s+to/gi,
297
391
  ];
298
392
 
299
393
  let sanitized = response;
300
394
  for (const pattern of leakPatterns) {
301
- if (pattern.test(sanitized)) {
302
-
303
- break;
304
- }
395
+ sanitized = sanitized.replace(pattern, '[system content]');
305
396
  }
306
397
 
307
398
  return sanitized;