mulmoclaude 0.1.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 (408) hide show
  1. package/README.md +44 -0
  2. package/bin/mulmoclaude.js +202 -0
  3. package/bin/prepare-dist.js +93 -0
  4. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
  5. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
  6. package/client/assets/index-D8rhwXLq.js +4906 -0
  7. package/client/assets/index-KNLBjwuh.css +1 -0
  8. package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
  9. package/client/assets/material-icons-Dr0goTwe.woff +0 -0
  10. package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
  11. package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
  12. package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
  13. package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
  14. package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
  15. package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
  16. package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
  17. package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
  18. package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
  19. package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
  20. package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
  21. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
  22. package/client/index.html +28 -0
  23. package/package.json +66 -0
  24. package/server/agent/attachmentConverter.ts +270 -0
  25. package/server/agent/config.ts +414 -0
  26. package/server/agent/index.ts +260 -0
  27. package/server/agent/mcp-server.ts +412 -0
  28. package/server/agent/mcp-tools/index.ts +63 -0
  29. package/server/agent/mcp-tools/x.ts +188 -0
  30. package/server/agent/plugin-names.ts +75 -0
  31. package/server/agent/prompt.ts +349 -0
  32. package/server/agent/resumeFailover.ts +129 -0
  33. package/server/agent/sandboxMounts.ts +329 -0
  34. package/server/agent/stream.ts +194 -0
  35. package/server/api/auth/bearerAuth.ts +61 -0
  36. package/server/api/auth/token.ts +98 -0
  37. package/server/api/csrfGuard.ts +85 -0
  38. package/server/api/routes/agent.ts +478 -0
  39. package/server/api/routes/chart.ts +98 -0
  40. package/server/api/routes/chat-index.ts +46 -0
  41. package/server/api/routes/config.ts +258 -0
  42. package/server/api/routes/dispatchResponse.ts +79 -0
  43. package/server/api/routes/files.ts +812 -0
  44. package/server/api/routes/html.ts +101 -0
  45. package/server/api/routes/image.ts +169 -0
  46. package/server/api/routes/mulmo-script.ts +712 -0
  47. package/server/api/routes/mulmoScriptValidate.ts +101 -0
  48. package/server/api/routes/notifications.ts +69 -0
  49. package/server/api/routes/pdf.ts +163 -0
  50. package/server/api/routes/plugins.ts +276 -0
  51. package/server/api/routes/presentHtml.ts +48 -0
  52. package/server/api/routes/roles.ts +125 -0
  53. package/server/api/routes/scheduler.ts +153 -0
  54. package/server/api/routes/schedulerHandlers.ts +151 -0
  55. package/server/api/routes/schedulerTasks.ts +163 -0
  56. package/server/api/routes/sessions.ts +294 -0
  57. package/server/api/routes/sessionsCursor.ts +59 -0
  58. package/server/api/routes/skills.ts +195 -0
  59. package/server/api/routes/sources.ts +540 -0
  60. package/server/api/routes/todos.ts +263 -0
  61. package/server/api/routes/todosColumnsHandlers.ts +347 -0
  62. package/server/api/routes/todosHandlers.ts +274 -0
  63. package/server/api/routes/todosItemsHandlers.ts +386 -0
  64. package/server/api/routes/wiki/pageIndex.ts +53 -0
  65. package/server/api/routes/wiki.ts +363 -0
  66. package/server/api/sandboxStatus.ts +64 -0
  67. package/server/events/notifications.ts +160 -0
  68. package/server/events/pub-sub/index.ts +45 -0
  69. package/server/events/relay-client.ts +288 -0
  70. package/server/events/scheduler-adapter.ts +302 -0
  71. package/server/events/session-store/index.ts +492 -0
  72. package/server/events/task-manager/index.ts +181 -0
  73. package/server/index.ts +572 -0
  74. package/server/system/config.ts +243 -0
  75. package/server/system/credentials.ts +220 -0
  76. package/server/system/docker.ts +97 -0
  77. package/server/system/env.ts +109 -0
  78. package/server/system/logger/config.ts +112 -0
  79. package/server/system/logger/formatters.ts +40 -0
  80. package/server/system/logger/index.ts +53 -0
  81. package/server/system/logger/rotation.ts +37 -0
  82. package/server/system/logger/sinks.ts +101 -0
  83. package/server/system/logger/types.ts +29 -0
  84. package/server/utils/date.ts +57 -0
  85. package/server/utils/errors.ts +7 -0
  86. package/server/utils/fetch.ts +27 -0
  87. package/server/utils/files/atomic.ts +125 -0
  88. package/server/utils/files/html-io.ts +20 -0
  89. package/server/utils/files/image-store.ts +66 -0
  90. package/server/utils/files/index.ts +45 -0
  91. package/server/utils/files/journal-io.ts +213 -0
  92. package/server/utils/files/json.ts +69 -0
  93. package/server/utils/files/markdown-store.ts +33 -0
  94. package/server/utils/files/naming.ts +50 -0
  95. package/server/utils/files/reference-dirs-io.ts +45 -0
  96. package/server/utils/files/roles-io.ts +45 -0
  97. package/server/utils/files/safe.ts +106 -0
  98. package/server/utils/files/scheduler-io.ts +20 -0
  99. package/server/utils/files/scheduler-overrides-io.ts +64 -0
  100. package/server/utils/files/session-io.ts +136 -0
  101. package/server/utils/files/spreadsheet-store.ts +63 -0
  102. package/server/utils/files/todos-io.ts +29 -0
  103. package/server/utils/files/user-tasks-io.ts +25 -0
  104. package/server/utils/files/workspace-io.ts +221 -0
  105. package/server/utils/gemini.ts +59 -0
  106. package/server/utils/gitignore.ts +69 -0
  107. package/server/utils/http.ts +15 -0
  108. package/server/utils/httpError.ts +61 -0
  109. package/server/utils/id.ts +16 -0
  110. package/server/utils/json.ts +83 -0
  111. package/server/utils/logBackgroundError.ts +22 -0
  112. package/server/utils/markdown.ts +82 -0
  113. package/server/utils/request.ts +29 -0
  114. package/server/utils/slug.ts +50 -0
  115. package/server/utils/spawn.ts +62 -0
  116. package/server/utils/time.ts +34 -0
  117. package/server/utils/types.ts +47 -0
  118. package/server/workspace/chat-index/index.ts +153 -0
  119. package/server/workspace/chat-index/indexer.ts +209 -0
  120. package/server/workspace/chat-index/paths.ts +34 -0
  121. package/server/workspace/chat-index/summarizer.ts +247 -0
  122. package/server/workspace/chat-index/types.ts +38 -0
  123. package/server/workspace/custom-dirs.ts +220 -0
  124. package/server/workspace/helps/business.md +104 -0
  125. package/server/workspace/helps/github.md +23 -0
  126. package/server/workspace/helps/index.md +60 -0
  127. package/server/workspace/helps/mulmoscript.md +249 -0
  128. package/server/workspace/helps/sandbox.md +90 -0
  129. package/server/workspace/helps/spreadsheet.md +43 -0
  130. package/server/workspace/helps/telegram.md +135 -0
  131. package/server/workspace/helps/wiki.md +131 -0
  132. package/server/workspace/journal/archivist.ts +386 -0
  133. package/server/workspace/journal/dailyPass.ts +743 -0
  134. package/server/workspace/journal/diff.ts +71 -0
  135. package/server/workspace/journal/index.ts +185 -0
  136. package/server/workspace/journal/indexFile.ts +136 -0
  137. package/server/workspace/journal/linkRewrite.ts +4 -0
  138. package/server/workspace/journal/memoryExtractor.ts +130 -0
  139. package/server/workspace/journal/optimizationPass.ts +160 -0
  140. package/server/workspace/journal/paths.ts +76 -0
  141. package/server/workspace/journal/state.ts +125 -0
  142. package/server/workspace/paths.ts +158 -0
  143. package/server/workspace/reference-dirs.ts +252 -0
  144. package/server/workspace/roles.ts +37 -0
  145. package/server/workspace/skills/discovery.ts +125 -0
  146. package/server/workspace/skills/index.ts +10 -0
  147. package/server/workspace/skills/parser.ts +144 -0
  148. package/server/workspace/skills/paths.ts +41 -0
  149. package/server/workspace/skills/scheduler.ts +149 -0
  150. package/server/workspace/skills/types.ts +30 -0
  151. package/server/workspace/skills/user-tasks.ts +257 -0
  152. package/server/workspace/skills/writer.ts +189 -0
  153. package/server/workspace/sources/arxivDiscovery.ts +182 -0
  154. package/server/workspace/sources/classifier.ts +268 -0
  155. package/server/workspace/sources/fetchers/arxiv.ts +170 -0
  156. package/server/workspace/sources/fetchers/github.ts +106 -0
  157. package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
  158. package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
  159. package/server/workspace/sources/fetchers/index.ts +71 -0
  160. package/server/workspace/sources/fetchers/registerAll.ts +15 -0
  161. package/server/workspace/sources/fetchers/rss.ts +141 -0
  162. package/server/workspace/sources/fetchers/rssParser.ts +295 -0
  163. package/server/workspace/sources/httpFetcher.ts +230 -0
  164. package/server/workspace/sources/interests.ts +120 -0
  165. package/server/workspace/sources/paths.ts +110 -0
  166. package/server/workspace/sources/pipeline/dedup.ts +60 -0
  167. package/server/workspace/sources/pipeline/fetch.ts +136 -0
  168. package/server/workspace/sources/pipeline/index.ts +249 -0
  169. package/server/workspace/sources/pipeline/notify.ts +72 -0
  170. package/server/workspace/sources/pipeline/plan.ts +66 -0
  171. package/server/workspace/sources/pipeline/summarize.ts +189 -0
  172. package/server/workspace/sources/pipeline/write.ts +185 -0
  173. package/server/workspace/sources/rateLimiter.ts +148 -0
  174. package/server/workspace/sources/registry.ts +326 -0
  175. package/server/workspace/sources/robots.ts +271 -0
  176. package/server/workspace/sources/sourceState.ts +135 -0
  177. package/server/workspace/sources/taxonomy.ts +74 -0
  178. package/server/workspace/sources/types.ts +144 -0
  179. package/server/workspace/sources/urls.ts +112 -0
  180. package/server/workspace/tool-trace/classify.ts +114 -0
  181. package/server/workspace/tool-trace/index.ts +250 -0
  182. package/server/workspace/tool-trace/writeSearch.ts +98 -0
  183. package/server/workspace/wiki-backlinks/index.ts +107 -0
  184. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
  185. package/server/workspace/workspace.ts +66 -0
  186. package/src/App.vue +720 -0
  187. package/src/assets/mulmo_bw.png +0 -0
  188. package/src/components/CanvasViewToggle.vue +27 -0
  189. package/src/components/ChatAttachmentPreview.vue +45 -0
  190. package/src/components/ChatImagePreview.vue +17 -0
  191. package/src/components/ChatInput.vue +208 -0
  192. package/src/components/FileContentHeader.vue +49 -0
  193. package/src/components/FileContentRenderer.vue +162 -0
  194. package/src/components/FileTree.vue +115 -0
  195. package/src/components/FileTreePane.vue +85 -0
  196. package/src/components/FilesView.vue +206 -0
  197. package/src/components/LockStatusPopup.vue +111 -0
  198. package/src/components/NotificationBell.vue +131 -0
  199. package/src/components/NotificationToast.vue +72 -0
  200. package/src/components/PluginLauncher.vue +138 -0
  201. package/src/components/RightSidebar.vue +113 -0
  202. package/src/components/RoleSelector.vue +64 -0
  203. package/src/components/SessionHistoryPanel.vue +176 -0
  204. package/src/components/SessionTabBar.vue +81 -0
  205. package/src/components/SettingsMcpTab.vue +350 -0
  206. package/src/components/SettingsModal.vue +275 -0
  207. package/src/components/SettingsReferenceDirsTab.vue +173 -0
  208. package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
  209. package/src/components/SidebarHeader.vue +69 -0
  210. package/src/components/StackView.vue +360 -0
  211. package/src/components/SuggestionsPanel.vue +65 -0
  212. package/src/components/TodoExplorer.vue +358 -0
  213. package/src/components/ToolResultsPanel.vue +77 -0
  214. package/src/components/todo/TodoAddDialog.vue +131 -0
  215. package/src/components/todo/TodoEditDialog.vue +47 -0
  216. package/src/components/todo/TodoEditPanel.vue +113 -0
  217. package/src/components/todo/TodoKanbanView.vue +249 -0
  218. package/src/components/todo/TodoListView.vue +79 -0
  219. package/src/components/todo/TodoTableView.vue +177 -0
  220. package/src/composables/useActiveSession.ts +40 -0
  221. package/src/composables/useAppApi.ts +45 -0
  222. package/src/composables/useCanvasViewMode.ts +121 -0
  223. package/src/composables/useChatScroll.ts +47 -0
  224. package/src/composables/useClickOutside.ts +26 -0
  225. package/src/composables/useClipboardCopy.ts +44 -0
  226. package/src/composables/useContentDisplay.ts +52 -0
  227. package/src/composables/useDebugBeat.ts +23 -0
  228. package/src/composables/useDynamicFavicon.ts +115 -0
  229. package/src/composables/useEventListeners.ts +42 -0
  230. package/src/composables/useExpandedDirs.ts +64 -0
  231. package/src/composables/useFaviconState.ts +30 -0
  232. package/src/composables/useFileSelection.ts +115 -0
  233. package/src/composables/useFileSortMode.ts +24 -0
  234. package/src/composables/useFileTree.ts +85 -0
  235. package/src/composables/useFreshPluginData.ts +89 -0
  236. package/src/composables/useHealth.ts +38 -0
  237. package/src/composables/useImeAwareEnter.ts +57 -0
  238. package/src/composables/useKeyNavigation.ts +60 -0
  239. package/src/composables/useMarkdownLinkHandler.ts +46 -0
  240. package/src/composables/useMarkdownMode.ts +17 -0
  241. package/src/composables/useMcpTools.ts +71 -0
  242. package/src/composables/useMergedSessions.ts +27 -0
  243. package/src/composables/useNotifications.ts +90 -0
  244. package/src/composables/usePdfDownload.ts +60 -0
  245. package/src/composables/usePendingCalls.ts +77 -0
  246. package/src/composables/usePubSub.ts +85 -0
  247. package/src/composables/useRightSidebar.ts +23 -0
  248. package/src/composables/useRoles.ts +34 -0
  249. package/src/composables/useSandboxStatus.ts +67 -0
  250. package/src/composables/useSelectedResult.ts +49 -0
  251. package/src/composables/useSessionDerived.ts +51 -0
  252. package/src/composables/useSessionHistory.ts +81 -0
  253. package/src/composables/useSessionSync.ts +57 -0
  254. package/src/composables/useViewLayout.ts +55 -0
  255. package/src/config/apiRoutes.ts +173 -0
  256. package/src/config/pubsubChannels.ts +45 -0
  257. package/src/config/roles.ts +335 -0
  258. package/src/config/schedulerActions.ts +25 -0
  259. package/src/config/toolNames.ts +71 -0
  260. package/src/config/workspacePaths.ts +24 -0
  261. package/src/index.css +107 -0
  262. package/src/main.ts +25 -0
  263. package/src/plugins/canvas/Preview.vue +13 -0
  264. package/src/plugins/canvas/View.vue +333 -0
  265. package/src/plugins/canvas/definition.ts +38 -0
  266. package/src/plugins/canvas/index.ts +36 -0
  267. package/src/plugins/chart/Preview.vue +49 -0
  268. package/src/plugins/chart/View.vue +143 -0
  269. package/src/plugins/chart/definition.ts +58 -0
  270. package/src/plugins/chart/index.ts +52 -0
  271. package/src/plugins/editImage/Preview.vue +13 -0
  272. package/src/plugins/editImage/View.vue +13 -0
  273. package/src/plugins/editImage/definition.ts +27 -0
  274. package/src/plugins/editImage/index.ts +36 -0
  275. package/src/plugins/generateImage/Preview.vue +13 -0
  276. package/src/plugins/generateImage/View.vue +33 -0
  277. package/src/plugins/generateImage/definition.ts +32 -0
  278. package/src/plugins/generateImage/index.ts +56 -0
  279. package/src/plugins/manageRoles/Preview.vue +49 -0
  280. package/src/plugins/manageRoles/View.vue +525 -0
  281. package/src/plugins/manageRoles/definition.ts +43 -0
  282. package/src/plugins/manageRoles/index.ts +47 -0
  283. package/src/plugins/manageSkills/Preview.vue +21 -0
  284. package/src/plugins/manageSkills/View.vue +321 -0
  285. package/src/plugins/manageSkills/definition.ts +49 -0
  286. package/src/plugins/manageSkills/index.ts +49 -0
  287. package/src/plugins/manageSource/Preview.vue +33 -0
  288. package/src/plugins/manageSource/View.vue +697 -0
  289. package/src/plugins/manageSource/definition.ts +63 -0
  290. package/src/plugins/manageSource/index.ts +66 -0
  291. package/src/plugins/markdown/Preview.vue +77 -0
  292. package/src/plugins/markdown/View.vue +476 -0
  293. package/src/plugins/markdown/definition.ts +50 -0
  294. package/src/plugins/markdown/index.ts +36 -0
  295. package/src/plugins/presentHtml/Preview.vue +25 -0
  296. package/src/plugins/presentHtml/View.vue +52 -0
  297. package/src/plugins/presentHtml/definition.ts +27 -0
  298. package/src/plugins/presentHtml/helpers.ts +72 -0
  299. package/src/plugins/presentHtml/index.ts +41 -0
  300. package/src/plugins/presentMulmoScript/Preview.vue +23 -0
  301. package/src/plugins/presentMulmoScript/View.vue +1166 -0
  302. package/src/plugins/presentMulmoScript/definition.ts +95 -0
  303. package/src/plugins/presentMulmoScript/helpers.ts +162 -0
  304. package/src/plugins/presentMulmoScript/index.ts +40 -0
  305. package/src/plugins/scheduler/Preview.vue +67 -0
  306. package/src/plugins/scheduler/TasksTab.vue +205 -0
  307. package/src/plugins/scheduler/View.vue +565 -0
  308. package/src/plugins/scheduler/definition.ts +57 -0
  309. package/src/plugins/scheduler/index.ts +45 -0
  310. package/src/plugins/scheduler/viewModes.ts +26 -0
  311. package/src/plugins/spreadsheet/Preview.vue +29 -0
  312. package/src/plugins/spreadsheet/View.vue +997 -0
  313. package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
  314. package/src/plugins/spreadsheet/definition.ts +121 -0
  315. package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
  316. package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
  317. package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
  318. package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
  319. package/src/plugins/spreadsheet/engine/engine.ts +176 -0
  320. package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
  321. package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
  322. package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
  323. package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
  324. package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
  325. package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
  326. package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
  327. package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
  328. package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
  329. package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
  330. package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
  331. package/src/plugins/spreadsheet/engine/index.ts +27 -0
  332. package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
  333. package/src/plugins/spreadsheet/engine/parser.ts +143 -0
  334. package/src/plugins/spreadsheet/engine/registry.ts +150 -0
  335. package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
  336. package/src/plugins/spreadsheet/engine/types.ts +64 -0
  337. package/src/plugins/spreadsheet/index.ts +36 -0
  338. package/src/plugins/textResponse/Preview.vue +94 -0
  339. package/src/plugins/textResponse/View.vue +503 -0
  340. package/src/plugins/textResponse/definition.ts +34 -0
  341. package/src/plugins/textResponse/index.ts +27 -0
  342. package/src/plugins/textResponse/plugin.ts +29 -0
  343. package/src/plugins/textResponse/samples.ts +97 -0
  344. package/src/plugins/textResponse/types.ts +11 -0
  345. package/src/plugins/todo/Preview.vue +63 -0
  346. package/src/plugins/todo/View.vue +364 -0
  347. package/src/plugins/todo/composables/useTodos.ts +177 -0
  348. package/src/plugins/todo/definition.ts +45 -0
  349. package/src/plugins/todo/index.ts +61 -0
  350. package/src/plugins/todo/labels.ts +163 -0
  351. package/src/plugins/todo/priority.ts +98 -0
  352. package/src/plugins/todo/viewModes.ts +19 -0
  353. package/src/plugins/ui-image/ImagePreview.vue +23 -0
  354. package/src/plugins/ui-image/ImageView.vue +34 -0
  355. package/src/plugins/ui-image/index.ts +3 -0
  356. package/src/plugins/ui-image/types.ts +4 -0
  357. package/src/plugins/wiki/Preview.vue +65 -0
  358. package/src/plugins/wiki/View.vue +342 -0
  359. package/src/plugins/wiki/definition.ts +25 -0
  360. package/src/plugins/wiki/helpers.ts +59 -0
  361. package/src/plugins/wiki/index.ts +52 -0
  362. package/src/router/guards.ts +61 -0
  363. package/src/router/index.ts +50 -0
  364. package/src/tools/index.ts +52 -0
  365. package/src/tools/types.ts +27 -0
  366. package/src/types/events.ts +16 -0
  367. package/src/types/fileTree.ts +13 -0
  368. package/src/types/notification.ts +67 -0
  369. package/src/types/session.ts +116 -0
  370. package/src/types/sse.ts +90 -0
  371. package/src/types/toolCallHistory.ts +13 -0
  372. package/src/utils/agent/eventDispatch.ts +74 -0
  373. package/src/utils/agent/request.ts +55 -0
  374. package/src/utils/agent/toolCalls.ts +62 -0
  375. package/src/utils/api.ts +218 -0
  376. package/src/utils/canvas/viewMode.ts +46 -0
  377. package/src/utils/dom/authTokenMeta.ts +20 -0
  378. package/src/utils/dom/clickOutside.ts +11 -0
  379. package/src/utils/dom/externalLink.ts +57 -0
  380. package/src/utils/dom/scrollable.ts +24 -0
  381. package/src/utils/errors.ts +11 -0
  382. package/src/utils/files/expandedDirs.ts +25 -0
  383. package/src/utils/files/filename.ts +12 -0
  384. package/src/utils/files/sortChildren.ts +20 -0
  385. package/src/utils/filesPreview/schedulerPreview.ts +38 -0
  386. package/src/utils/filesPreview/todoPreview.ts +40 -0
  387. package/src/utils/format/date.ts +85 -0
  388. package/src/utils/format/frontmatter.ts +80 -0
  389. package/src/utils/format/jsonSyntax.ts +109 -0
  390. package/src/utils/html/previewCsp.ts +65 -0
  391. package/src/utils/image/resolve.ts +8 -0
  392. package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
  393. package/src/utils/markdown/extractFirstH1.ts +39 -0
  394. package/src/utils/notification/dispatch.ts +22 -0
  395. package/src/utils/path/relativeLink.ts +130 -0
  396. package/src/utils/role/icon.ts +20 -0
  397. package/src/utils/role/merge.ts +10 -0
  398. package/src/utils/role/plugins.ts +12 -0
  399. package/src/utils/session/mergeSessions.ts +103 -0
  400. package/src/utils/session/seedRoleDefault.ts +35 -0
  401. package/src/utils/session/sessionEntries.ts +121 -0
  402. package/src/utils/session/sessionFactory.ts +22 -0
  403. package/src/utils/session/sessionHelpers.ts +99 -0
  404. package/src/utils/tools/dedup.ts +17 -0
  405. package/src/utils/tools/mcp.ts +33 -0
  406. package/src/utils/tools/pendingCalls.ts +16 -0
  407. package/src/utils/tools/result.ts +40 -0
  408. package/src/utils/types.ts +44 -0
@@ -0,0 +1,135 @@
1
+ // Per-source runtime state I/O.
2
+ //
3
+ // State lives at `workspace/sources/_state/<slug>.json`, kept
4
+ // separate from the source config (`<slug>.md`) so the config
5
+ // is the git-tracked source of truth while state can grow /
6
+ // reset / get cleared without touching committed history.
7
+ //
8
+ // All functions take an explicit `workspaceRoot` so tests use
9
+ // mkdtempSync without touching real workspace state.
10
+
11
+ import fsp from "node:fs/promises";
12
+ import { defaultSourceState, type SourceState } from "./types.js";
13
+ import { sourceStatePath } from "./paths.js";
14
+ import { isValidSlug } from "../../utils/slug.js";
15
+ import { errorMessage } from "../../utils/errors.js";
16
+ import { writeJsonAtomic } from "../../utils/files/index.js";
17
+ import { isRecord } from "../../utils/types.js";
18
+
19
+ // Shallow-parse + type-guard one state record. Returns a
20
+ // default state (zeroed counters, empty cursor) when the file
21
+ // is missing, malformed, or any required field fails. Never
22
+ // throws.
23
+ export async function readSourceState(workspaceRoot: string, slug: string): Promise<SourceState> {
24
+ if (!isValidSlug(slug)) return defaultSourceState(slug);
25
+ let raw: string;
26
+ try {
27
+ raw = await fsp.readFile(sourceStatePath(workspaceRoot, slug), "utf-8");
28
+ } catch {
29
+ return defaultSourceState(slug);
30
+ }
31
+ let parsed: unknown;
32
+ try {
33
+ parsed = JSON.parse(raw);
34
+ } catch {
35
+ return defaultSourceState(slug);
36
+ }
37
+ return validateSourceState(parsed, slug);
38
+ }
39
+
40
+ // Runtime-validate an arbitrary parse result into a
41
+ // SourceState. Unknown fields are dropped; missing fields get
42
+ // default values; wrong-typed fields collapse to the default.
43
+ // Defensive: a hand-edited / corrupted state file should NOT
44
+ // crash the pipeline, it should quietly get rebuilt on the
45
+ // next successful run.
46
+ export function validateSourceState(raw: unknown, slug: string): SourceState {
47
+ if (!isRecord(raw)) {
48
+ return defaultSourceState(slug);
49
+ }
50
+ const o = raw as Record<string, unknown>;
51
+ const lastFetchedAt = typeof o.lastFetchedAt === "string" ? o.lastFetchedAt : null;
52
+ const nextAttemptAt = typeof o.nextAttemptAt === "string" ? o.nextAttemptAt : null;
53
+ const consecutiveFailures =
54
+ typeof o.consecutiveFailures === "number" && Number.isFinite(o.consecutiveFailures) && o.consecutiveFailures >= 0 ? Math.floor(o.consecutiveFailures) : 0;
55
+ const cursor = validateCursor(o.cursor);
56
+ return {
57
+ slug,
58
+ lastFetchedAt,
59
+ cursor,
60
+ consecutiveFailures,
61
+ nextAttemptAt,
62
+ };
63
+ }
64
+
65
+ function validateCursor(raw: unknown): Record<string, string> {
66
+ if (!isRecord(raw)) {
67
+ return {};
68
+ }
69
+ const out: Record<string, string> = {};
70
+ for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
71
+ if (typeof value === "string") out[key] = value;
72
+ }
73
+ return out;
74
+ }
75
+
76
+ // Atomic write: stage to `.tmp` then rename. Parent directory
77
+ // created as needed.
78
+ export async function writeSourceState(workspaceRoot: string, state: SourceState): Promise<void> {
79
+ if (!isValidSlug(state.slug)) {
80
+ throw new Error(`[sources/state] invalid slug: ${state.slug}`);
81
+ }
82
+ await writeJsonAtomic(sourceStatePath(workspaceRoot, state.slug), state);
83
+ }
84
+
85
+ // Convenience: read every state file listed for the given
86
+ // slugs. Used by the pipeline to gather per-source state before
87
+ // the fetch phase.
88
+ export async function readManyStates(workspaceRoot: string, slugs: readonly string[]): Promise<Map<string, SourceState>> {
89
+ const out = new Map<string, SourceState>();
90
+ const reads = await Promise.all(slugs.map((slug) => readSourceState(workspaceRoot, slug)));
91
+ for (const state of reads) out.set(state.slug, state);
92
+ return out;
93
+ }
94
+
95
+ // Convenience: write every state back in parallel. Failure on
96
+ // one state write is logged and absorbed — the daily run's
97
+ // summary has already landed on disk, a lost state update just
98
+ // means the next run re-fetches slightly more than needed.
99
+ export async function writeManyStates(workspaceRoot: string, states: readonly SourceState[]): Promise<{ written: number; errors: string[] }> {
100
+ const errors: string[] = [];
101
+ let written = 0;
102
+ for (const state of states) {
103
+ try {
104
+ await writeSourceState(workspaceRoot, state);
105
+ written++;
106
+ } catch (err) {
107
+ errors.push(`[sources/state] ${state.slug}: ${errorMessage(err)}`);
108
+ }
109
+ }
110
+ return { written, errors };
111
+ }
112
+
113
+ // Delete the state file for a slug. Used by `manageSource
114
+ // delete` so a removed source doesn't leave orphan state.
115
+ // Missing file is fine — returns false rather than throwing.
116
+ export async function deleteSourceState(workspaceRoot: string, slug: string): Promise<boolean> {
117
+ if (!isValidSlug(slug)) return false;
118
+ try {
119
+ await fsp.unlink(sourceStatePath(workspaceRoot, slug));
120
+ return true;
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ // Utility: sort outcomes by source slug so results are
127
+ // deterministic regardless of which fetcher finished first.
128
+ // Used by reporting / logging code.
129
+ export function sortBySlug<T extends { sourceSlug?: string; slug?: string }>(items: readonly T[]): T[] {
130
+ return [...items].sort((a, b) => {
131
+ const ak = a.sourceSlug ?? a.slug ?? "";
132
+ const bk = b.sourceSlug ?? b.slug ?? "";
133
+ return ak < bk ? -1 : ak > bk ? 1 : 0;
134
+ });
135
+ }
@@ -0,0 +1,74 @@
1
+ // Fixed taxonomy of source categories. Keeping this a closed enum
2
+ // (rather than accepting free-form LLM-generated tags) prevents
3
+ // synonym sprawl — `ai` vs `artificial-intelligence` vs `AI` would
4
+ // otherwise all coexist and make filtering useless.
5
+ //
6
+ // The auto-categorizer (see plans/feat-source-registry.md §"Auto-
7
+ // categorization") classifies each new source into 1-5 of these
8
+ // slugs and writes them into the source file's frontmatter. Users
9
+ // can override by editing the file; the next daily run picks up
10
+ // the edits.
11
+ //
12
+ // Pin-tested so a silent enum mutation doesn't sneak past review.
13
+
14
+ export const CATEGORY_SLUGS = [
15
+ "tech-news",
16
+ "business-news",
17
+ "ai",
18
+ "security",
19
+ "devops",
20
+ "frontend",
21
+ "backend",
22
+ "ml-research",
23
+ "dependencies",
24
+ "product-updates",
25
+ "japanese",
26
+ "english",
27
+ "papers",
28
+ "general",
29
+ "startup",
30
+ "personal",
31
+ // --- Phase-1 expansion (resolved from #188 open-question Q1) ---
32
+ // Added to cover common genres the original 16 couldn't capture
33
+ // (which were tech-centric and collapsed everything non-tech
34
+ // into `general`). See plans/feat-source-registry.md §Resolved
35
+ // decisions for rationale per slug.
36
+ "finance",
37
+ "design",
38
+ "productivity",
39
+ "science",
40
+ "health",
41
+ "gaming",
42
+ "climate",
43
+ "culture",
44
+ "policy",
45
+ ] as const;
46
+
47
+ export type CategorySlug = (typeof CATEGORY_SLUGS)[number];
48
+
49
+ const CATEGORY_SET: ReadonlySet<string> = new Set(CATEGORY_SLUGS);
50
+
51
+ // Runtime type-guard. Used when reading a source file from disk to
52
+ // drop any legacy / typo categories so downstream code only ever
53
+ // deals in the current enum.
54
+ export function isCategorySlug(value: unknown): value is CategorySlug {
55
+ return typeof value === "string" && CATEGORY_SET.has(value);
56
+ }
57
+
58
+ // Normalize an unknown list of category candidates into a clean,
59
+ // deduplicated array of valid slugs. Used when reading from
60
+ // frontmatter (where the user may have typo'd) and when receiving
61
+ // classifier output (where the LLM may have hallucinated a slug
62
+ // outside the taxonomy).
63
+ export function normalizeCategories(raw: unknown): CategorySlug[] {
64
+ if (!Array.isArray(raw)) return [];
65
+ const seen = new Set<CategorySlug>();
66
+ const out: CategorySlug[] = [];
67
+ for (const item of raw) {
68
+ if (isCategorySlug(item) && !seen.has(item)) {
69
+ seen.add(item);
70
+ out.push(item);
71
+ }
72
+ }
73
+ return out;
74
+ }
@@ -0,0 +1,144 @@
1
+ // Data model for the information-source registry. Every source
2
+ // lives as one markdown file under `workspace/sources/<slug>.md`,
3
+ // with these fields in the YAML frontmatter plus optional free-form
4
+ // markdown notes in the body.
5
+ //
6
+ // Design invariants the consumer code relies on:
7
+ //
8
+ // - `slug` is the primary key and matches the filename exactly.
9
+ // Enforced by `registry.ts` on both read and write.
10
+ // - `url` is always the normalized form (see urls.ts) so dedup
11
+ // across sources works by string equality.
12
+ // - `fetcherKind` is one of a closed set so the fetcher dispatcher
13
+ // can look up the right handler without `any`.
14
+ // - `schedule` drives the daily / weekly aggregation pipeline.
15
+ // - `categories` contains only valid CategorySlug values
16
+ // (runtime-validated on read).
17
+ //
18
+ // Secrets (API tokens, bearer auth) are NEVER stored here —
19
+ // phase-1 scope is public sources only. Phase-3 authed fetchers
20
+ // will read credentials from `.env` at runtime by name; the name
21
+ // reference lives in `fetcherParams` as an `envVar` field.
22
+
23
+ import type { CategorySlug } from "./taxonomy.js";
24
+
25
+ // Closed set of fetcher kinds we can dispatch on. Adding a new
26
+ // fetcher means: add the string literal here, implement a matching
27
+ // module under `server/sources/fetchers/<kind>.ts`, and register
28
+ // it in the fetcher index. Nothing else in the framework needs to
29
+ // change.
30
+ //
31
+ // Phase-1 surface:
32
+ // "rss" — public RSS / Atom feeds (server-side fetch)
33
+ // "github-releases" — GitHub /releases endpoint, unauthenticated
34
+ // "github-issues" — GitHub /issues + /pulls, unauthenticated
35
+ // "arxiv" — arXiv query API
36
+ // "web-fetch" — one-shot page fetch via Claude's web_fetch
37
+ // "web-search" — ad-hoc query via Claude's web_search
38
+ export const FETCHER_KINDS = ["rss", "github-releases", "github-issues", "arxiv", "web-fetch", "web-search"] as const;
39
+
40
+ export type FetcherKind = (typeof FETCHER_KINDS)[number];
41
+
42
+ const FETCHER_KIND_SET: ReadonlySet<string> = new Set(FETCHER_KINDS);
43
+
44
+ export function isFetcherKind(value: unknown): value is FetcherKind {
45
+ return typeof value === "string" && FETCHER_KIND_SET.has(value);
46
+ }
47
+
48
+ // How often the daily pipeline is expected to refresh this source.
49
+ // `on-demand` sources are never auto-fetched; they only respond to
50
+ // the `manageSource fetch` action or the on-demand research
51
+ // workflow.
52
+ export const SOURCE_SCHEDULES = ["hourly", "daily", "weekly", "on-demand"] as const;
53
+
54
+ export type SourceSchedule = (typeof SOURCE_SCHEDULES)[number];
55
+
56
+ const SOURCE_SCHEDULE_SET: ReadonlySet<string> = new Set(SOURCE_SCHEDULES);
57
+
58
+ export function isSourceSchedule(value: unknown): value is SourceSchedule {
59
+ return typeof value === "string" && SOURCE_SCHEDULE_SET.has(value);
60
+ }
61
+
62
+ // Per-fetcher extra parameters carried on the Source file. Flat
63
+ // string map on disk so the minimal frontmatter parser can handle
64
+ // it without nested YAML. Fetchers interpret the keys they care
65
+ // about and ignore the rest — keeps cross-fetcher rewiring cheap.
66
+ export type FetcherParams = Record<string, string>;
67
+
68
+ // The on-disk configuration for one source. This is the exact
69
+ // shape serialized into the YAML frontmatter of
70
+ // `workspace/sources/<slug>.md` — state (cursors, etags, failure
71
+ // counts) lives separately under `_state/<slug>.json`.
72
+ export interface Source {
73
+ slug: string;
74
+ title: string;
75
+ url: string;
76
+ fetcherKind: FetcherKind;
77
+ fetcherParams: FetcherParams;
78
+ schedule: SourceSchedule;
79
+ categories: CategorySlug[];
80
+ maxItemsPerFetch: number;
81
+ addedAt: string; // ISO timestamp
82
+ notes: string; // markdown body of the file
83
+ }
84
+
85
+ // One normalized item after a fetch. All fetchers produce this
86
+ // shape regardless of source type so the pipeline / dedup / summary
87
+ // layers don't care where items came from.
88
+ export interface SourceItem {
89
+ // Stable unique id for dedup. Hash of the normalized URL, or
90
+ // the fetcher's native id (e.g. GitHub release id) when one is
91
+ // available.
92
+ id: string;
93
+ title: string;
94
+ url: string;
95
+ publishedAt: string; // ISO timestamp
96
+ // Short one-line summary if the fetcher can produce one without
97
+ // LLM help. The pipeline's summarize step may replace this with
98
+ // a richer LLM-generated version.
99
+ summary?: string;
100
+ // Full body content if available (RSS description, GitHub release
101
+ // body, etc.). The summarize step reads this when the short
102
+ // summary is insufficient.
103
+ content?: string;
104
+ // Categories inherited from the source this item came from.
105
+ // Duplicated on the item so per-category daily rollups don't
106
+ // need a re-join.
107
+ categories: CategorySlug[];
108
+ // Slug of the parent Source so the dashboard / notification
109
+ // layer can link back.
110
+ sourceSlug: string;
111
+ // Optional severity hint set by the classifier or the fetcher
112
+ // itself (security advisories set `critical`). Daily pipeline
113
+ // uses this to decide whether to notify.
114
+ severity?: "info" | "warn" | "critical";
115
+ }
116
+
117
+ // Per-source runtime state, NOT committed to git. Mirrors the
118
+ // Source-vs-_state split described in plans/feat-source-registry.md.
119
+ export interface SourceState {
120
+ slug: string;
121
+ // Last successful fetch.
122
+ lastFetchedAt: string | null;
123
+ // Fetcher-specific cursor — ISO timestamp, etag, GitHub release
124
+ // id, arXiv last-seen, whatever the fetcher persists to
125
+ // de-duplicate across runs. Free-form string map so the fetcher
126
+ // interface doesn't need to know the shape upfront.
127
+ cursor: Record<string, string>;
128
+ // Consecutive failure count. Incremented per failed fetch,
129
+ // reset to 0 on success. Drives exponential backoff.
130
+ consecutiveFailures: number;
131
+ // Timestamp after which the next attempt is allowed, so backoff
132
+ // survives server restarts.
133
+ nextAttemptAt: string | null;
134
+ }
135
+
136
+ export function defaultSourceState(slug: string): SourceState {
137
+ return {
138
+ slug,
139
+ lastFetchedAt: null,
140
+ cursor: {},
141
+ consecutiveFailures: 0,
142
+ nextAttemptAt: null,
143
+ };
144
+ }
@@ -0,0 +1,112 @@
1
+ // URL normalization utilities for dedup + cache keys.
2
+ //
3
+ // The same article commonly arrives from multiple sources with
4
+ // different query strings (utm_source, fbclid, gclid, mc_cid, ...)
5
+ // or trailing slashes. We normalize before dedup so "same article
6
+ // different feed" collapses into one item.
7
+ //
8
+ // Pure — no I/O, fully testable.
9
+
10
+ import { createHash } from "node:crypto";
11
+
12
+ // Tracking parameters we always strip. Case-insensitive on the
13
+ // parameter NAME only.
14
+ const TRACKING_PARAM_PREFIXES: readonly string[] = ["utm_", "mc_", "pk_", "hsa_"];
15
+
16
+ const TRACKING_PARAMS: ReadonlySet<string> = new Set([
17
+ "fbclid",
18
+ "gclid",
19
+ "msclkid",
20
+ "dclid",
21
+ "gbraid",
22
+ "wbraid",
23
+ "yclid",
24
+ "ref",
25
+ "ref_src",
26
+ "ref_url",
27
+ "share",
28
+ "share_source",
29
+ "trk",
30
+ "igshid",
31
+ "cmp",
32
+ ]);
33
+
34
+ function isTrackingParam(name: string): boolean {
35
+ const lower = name.toLowerCase();
36
+ if (TRACKING_PARAMS.has(lower)) return true;
37
+ for (const prefix of TRACKING_PARAM_PREFIXES) {
38
+ if (lower.startsWith(prefix)) return true;
39
+ }
40
+ return false;
41
+ }
42
+
43
+ // Normalize a URL for use as a dedup key:
44
+ //
45
+ // 1. Parse it. Invalid → return null (caller decides fallback).
46
+ // 2. Lowercase the protocol + host.
47
+ // 3. Drop the fragment.
48
+ // 4. Drop tracking query params.
49
+ // 5. Sort remaining query params so different orderings hash the
50
+ // same.
51
+ // 6. Collapse trailing slash on the pathname (except root "/").
52
+ // 7. Drop default ports (80 for http, 443 for https).
53
+ //
54
+ // Returns the normalized href on success, null on unparseable
55
+ // input.
56
+ export function normalizeUrl(raw: string): string | null {
57
+ if (typeof raw !== "string" || raw.trim() === "") return null;
58
+ let url: URL;
59
+ try {
60
+ url = new URL(raw.trim());
61
+ } catch {
62
+ return null;
63
+ }
64
+
65
+ // protocol + host lowercase (URL already normalizes these but
66
+ // doing it explicitly guards against future WHATWG tweaks).
67
+ url.protocol = url.protocol.toLowerCase();
68
+ url.hostname = url.hostname.toLowerCase();
69
+
70
+ // Drop fragment.
71
+ url.hash = "";
72
+
73
+ // Drop tracking params. Iterate on a snapshot because delete
74
+ // mutates the underlying list.
75
+ const paramNames = Array.from(url.searchParams.keys());
76
+ for (const name of paramNames) {
77
+ if (isTrackingParam(name)) url.searchParams.delete(name);
78
+ }
79
+
80
+ // Sort remaining params for deterministic ordering. Preserve
81
+ // multi-value params by iterating all entries.
82
+ const entries = Array.from(url.searchParams.entries());
83
+ entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
84
+ // Clear and reinsert.
85
+ for (const name of Array.from(url.searchParams.keys())) {
86
+ url.searchParams.delete(name);
87
+ }
88
+ for (const [name, value] of entries) {
89
+ url.searchParams.append(name, value);
90
+ }
91
+
92
+ // Collapse trailing slash on non-root paths.
93
+ if (url.pathname.length > 1 && url.pathname.endsWith("/")) {
94
+ url.pathname = url.pathname.slice(0, -1);
95
+ }
96
+
97
+ // Drop default ports.
98
+ if ((url.protocol === "http:" && url.port === "80") || (url.protocol === "https:" && url.port === "443")) {
99
+ url.port = "";
100
+ }
101
+
102
+ return url.toString();
103
+ }
104
+
105
+ // Stable content-addressed id for an item. Used for persistent
106
+ // cross-run dedup of news items. Truncated SHA-256 gives ~64 bits
107
+ // of collision resistance — safe into the billions of items, vs.
108
+ // FNV-1a 32-bit which starts colliding in the tens of thousands
109
+ // (birthday-paradox territory at our projected volume).
110
+ export function stableItemId(normalizedUrl: string): string {
111
+ return createHash("sha256").update(normalizedUrl, "utf8").digest("hex").slice(0, 16);
112
+ }
@@ -0,0 +1,114 @@
1
+ // Pure classification for built-in Claude tool_result events.
2
+ // Decides whether a given tool result should be stored in the session
3
+ // jsonl as a pointer to a real workspace file, inlined verbatim, or
4
+ // inlined with truncation. No filesystem access — callers do I/O
5
+ // separately and feed the result here.
6
+ //
7
+ // See plans/done/feat-tool-trace-persistence.md for the design rationale.
8
+
9
+ import { isRecord } from "../../utils/types.js";
10
+
11
+ export type Classification = { kind: "pointer"; contentRef: string } | { kind: "inline"; content: string; truncated: boolean };
12
+
13
+ // Max characters kept when content is stored inline in the jsonl.
14
+ // Picked to keep per-turn jsonl size sane while still capturing
15
+ // enough of a small Bash/Grep output to be useful for debugging.
16
+ export const MAX_INLINE_CONTENT_CHARS = 4096;
17
+
18
+ // Tools whose `args.file_path` already points at an existing file —
19
+ // the jsonl can simply reference that path instead of duplicating the
20
+ // content. Matches Claude Code's built-in file tool names exactly.
21
+ const FILE_POINTER_TOOLS = new Set(["Read", "Write", "Edit"]);
22
+
23
+ // Image-generation MCP tools. The tool result already carries the
24
+ // saved path so we extract it; the raw bytes/base64 never leave the
25
+ // agent stream memory.
26
+ const IMAGE_TOOLS = new Set(["generateImage", "editImage"]);
27
+
28
+ // Tool name we always route through `writeSearch.ts` before
29
+ // classifying. Exposed so callers know which tools need a
30
+ // pre-computed `searchContentRef` injected.
31
+ export const WEB_SEARCH_TOOL_NAME = "WebSearch";
32
+
33
+ export interface ClassifyInput {
34
+ toolName: string;
35
+ args: unknown;
36
+ content: string;
37
+ // Optional pre-computed contentRef for WebSearch — the caller saves
38
+ // the result file first (in `writeSearch.ts`) and passes the
39
+ // workspace-relative path in here.
40
+ searchContentRef?: string;
41
+ }
42
+
43
+ export function classifyToolResult(input: ClassifyInput): Classification {
44
+ const { toolName, args, content, searchContentRef } = input;
45
+
46
+ if (toolName === WEB_SEARCH_TOOL_NAME && searchContentRef) {
47
+ return { kind: "pointer", contentRef: searchContentRef };
48
+ }
49
+
50
+ if (FILE_POINTER_TOOLS.has(toolName)) {
51
+ const ref = filePointerFromArgs(args);
52
+ if (ref) return { kind: "pointer", contentRef: ref };
53
+ }
54
+
55
+ if (IMAGE_TOOLS.has(toolName)) {
56
+ const ref = imagePointerFromContent(content);
57
+ if (ref) return { kind: "pointer", contentRef: ref };
58
+ }
59
+
60
+ return inlineWithTruncation(content);
61
+ }
62
+
63
+ function filePointerFromArgs(args: unknown): string | null {
64
+ if (!isRecord(args)) return null;
65
+ const record = args;
66
+ const raw = record.file_path;
67
+ if (typeof raw !== "string" || raw.length === 0) return null;
68
+ return normalizeWorkspacePath(raw);
69
+ }
70
+
71
+ // Image MCP tool results typically include a saved path somewhere in
72
+ // the stringified result. Be conservative: only treat it as a pointer
73
+ // when we can confidently extract an `images/` or absolute path. No
74
+ // match → fall back to inline (truncated) handling so the record
75
+ // still carries *something* useful.
76
+ function imagePointerFromContent(content: string): string | null {
77
+ // Matches a JSON-ish "filePath": "..." or "path": "..." value
78
+ // without using regex backtracking. Look for known keys then scan
79
+ // the quoted value.
80
+ for (const key of ['"filePath":', '"path":', '"file":']) {
81
+ const idx = content.indexOf(key);
82
+ if (idx === -1) continue;
83
+ const afterKey = content.slice(idx + key.length);
84
+ const quoteStart = afterKey.indexOf('"');
85
+ if (quoteStart === -1) continue;
86
+ const quoteEnd = afterKey.indexOf('"', quoteStart + 1);
87
+ if (quoteEnd === -1) continue;
88
+ const raw = afterKey.slice(quoteStart + 1, quoteEnd);
89
+ if (raw.length === 0) continue;
90
+ return normalizeWorkspacePath(raw);
91
+ }
92
+ return null;
93
+ }
94
+
95
+ // Strip a leading "/" or "./" so the stored ref is workspace-relative
96
+ // regardless of how the tool happened to quote it. Leave "../"
97
+ // prefixes alone — a relative escape is a bug and we want it visible
98
+ // rather than silently fixed up.
99
+ function normalizeWorkspacePath(p: string): string {
100
+ if (p.startsWith("./")) return p.slice(2);
101
+ if (p.startsWith("/")) return p.slice(1);
102
+ return p;
103
+ }
104
+
105
+ function inlineWithTruncation(content: string): Classification {
106
+ if (content.length <= MAX_INLINE_CONTENT_CHARS) {
107
+ return { kind: "inline", content, truncated: false };
108
+ }
109
+ return {
110
+ kind: "inline",
111
+ content: content.slice(0, MAX_INLINE_CONTENT_CHARS),
112
+ truncated: true,
113
+ };
114
+ }