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,61 @@
1
+ // Helpers for the most common error-response pattern in route
2
+ // handlers:
3
+ //
4
+ // return res.status(400).json({ error: "..." });
5
+ //
6
+ // Before consolidation this appeared in ~100 places, each handler
7
+ // hand-rolling the `{ error: string }` body and picking a status
8
+ // code. The helpers below keep the call site to one line while
9
+ // centralising the response shape so cross-cutting concerns
10
+ // (e.g. adding a `requestId` or `timestamp` later) only need to
11
+ // change here.
12
+ //
13
+ // All helpers return the `Response` object so callers can write
14
+ // either of:
15
+ //
16
+ // return badRequest(res, "filePath is required");
17
+ //
18
+ // badRequest(res, "filePath is required");
19
+ // return;
20
+ //
21
+ // Non-`{ error: string }` shapes (e.g. `{ success: false, message }`
22
+ // returned by a handful of legacy routes, or multi-field error
23
+ // bodies) stay as explicit `res.status(N).json(...)` calls — the
24
+ // helpers intentionally cover only the dominant pattern.
25
+
26
+ import type { Response } from "express";
27
+
28
+ /** Send a `{ error: string }` body with the given HTTP status. */
29
+ export function sendError(res: Response, status: number, error: string): Response {
30
+ return res.status(status).json({ error });
31
+ }
32
+
33
+ /** 400 Bad Request — malformed input, missing required field, etc. */
34
+ export function badRequest(res: Response, error: string): Response {
35
+ return sendError(res, 400, error);
36
+ }
37
+
38
+ /** 401 Unauthorized — missing or invalid credentials. */
39
+ export function unauthorized(res: Response, error: string): Response {
40
+ return sendError(res, 401, error);
41
+ }
42
+
43
+ /** 403 Forbidden — auth present but not authorised for the resource. */
44
+ export function forbidden(res: Response, error: string): Response {
45
+ return sendError(res, 403, error);
46
+ }
47
+
48
+ /** 404 Not Found — resource doesn't exist. */
49
+ export function notFound(res: Response, error: string): Response {
50
+ return sendError(res, 404, error);
51
+ }
52
+
53
+ /** 409 Conflict — duplicate, concurrent modification, already running, etc. */
54
+ export function conflict(res: Response, error: string): Response {
55
+ return sendError(res, 409, error);
56
+ }
57
+
58
+ /** 500 Internal Server Error — unexpected failure on the server side. */
59
+ export function serverError(res: Response, error: string): Response {
60
+ return sendError(res, 500, error);
61
+ }
@@ -0,0 +1,16 @@
1
+ // Unique-ID generation for persisted records. Previously duplicated
2
+ // in schedulerHandlers, todosHandlers, and todosItemsHandlers —
3
+ // consolidated here as part of the server/utils grouping (#350 CLAUDE.md).
4
+
5
+ import { randomBytes } from "crypto";
6
+
7
+ /**
8
+ * Generate a short, unique, human-scannable ID.
9
+ *
10
+ * Format: `<prefix>_<epochMs>_<6 random hex chars>`. The prefix
11
+ * is required so IDs from different domains (todo, scheduler, column)
12
+ * are visually distinguishable in logs and JSON files.
13
+ */
14
+ export function makeId(prefix: string): string {
15
+ return `${prefix}_${Date.now()}_${randomBytes(3).toString("hex")}`;
16
+ }
@@ -0,0 +1,83 @@
1
+ // Tolerant JSON extraction from Claude CLI text output. Claude
2
+ // often wraps JSON in a ```json fenced block or precedes it with
3
+ // conversational text. These helpers find and parse the first
4
+ // valid JSON object regardless of surrounding prose.
5
+ //
6
+ // Previously lived in workspace/journal/archivist.ts; moved here
7
+ // so any module that calls the Claude CLI can reuse them.
8
+
9
+ /**
10
+ * Extract the first JSON object from a Claude CLI response.
11
+ *
12
+ * Strategy:
13
+ * 1. Look for a ```json fenced block — most reliable when present.
14
+ * 2. Fall back to the first balanced `{...}` block in the raw text.
15
+ * 3. Return `null` if neither yields valid JSON.
16
+ */
17
+ export function extractJsonObject(raw: string): unknown | null {
18
+ const fencedBody = findFencedJsonBody(raw);
19
+ if (fencedBody !== null) {
20
+ try {
21
+ return JSON.parse(fencedBody);
22
+ } catch {
23
+ // fall through to scan
24
+ }
25
+ }
26
+ const balanced = findBalancedBraceBlock(raw);
27
+ if (balanced === null) return null;
28
+ try {
29
+ return JSON.parse(balanced);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Find the first balanced `{...}` substring, respecting JSON string
37
+ * escapes. Uses a char-by-char scan (no regex) to avoid slow-regex
38
+ * lint warnings and backtracking risks on large LLM output.
39
+ */
40
+ export function findBalancedBraceBlock(raw: string): string | null {
41
+ const start = raw.indexOf("{");
42
+ if (start === -1) return null;
43
+ let depth = 0;
44
+ let inString = false;
45
+ let escape = false;
46
+ for (let i = start; i < raw.length; i++) {
47
+ const ch = raw[i];
48
+ if (escape) {
49
+ escape = false;
50
+ continue;
51
+ }
52
+ if (ch === "\\") {
53
+ escape = true;
54
+ continue;
55
+ }
56
+ if (ch === '"') {
57
+ inString = !inString;
58
+ continue;
59
+ }
60
+ if (inString) continue;
61
+ if (ch === "{") depth++;
62
+ if (ch === "}" && --depth === 0) return raw.slice(start, i + 1);
63
+ }
64
+ return null;
65
+ }
66
+
67
+ /**
68
+ * Extract the body of the first ` ```json ... ``` ` fenced block.
69
+ * Returns `null` if no fenced block is found.
70
+ */
71
+ export function findFencedJsonBody(raw: string): string | null {
72
+ const OPEN = "```json";
73
+ const CLOSE = "```";
74
+ const openIdx = raw.indexOf(OPEN);
75
+ if (openIdx === -1) return null;
76
+ const afterOpen = openIdx + OPEN.length;
77
+ const bodyStart = raw.indexOf("\n", afterOpen);
78
+ if (bodyStart === -1) return null;
79
+ const closeIdx = raw.indexOf(CLOSE, bodyStart + 1);
80
+ if (closeIdx === -1) return null;
81
+ const bodyEnd = raw[closeIdx - 1] === "\n" ? closeIdx - 1 : closeIdx;
82
+ return raw.slice(bodyStart + 1, bodyEnd);
83
+ }
@@ -0,0 +1,22 @@
1
+ import { log } from "../system/logger/index.js";
2
+
3
+ /**
4
+ * Build a `.catch` handler for a fire-and-forget background job that
5
+ * logs the failure under the given prefix. Consolidates the
6
+ * "unexpected error in background" pattern used across journal,
7
+ * chat-index, wiki-backlinks, tool-trace, etc.
8
+ *
9
+ * Usage:
10
+ *
11
+ * maybeRunJournal({ ... }).catch(logBackgroundError("journal"));
12
+ *
13
+ * The handler never rethrows — the caller's promise chain is
14
+ * terminated cleanly so nothing propagates into the request path.
15
+ */
16
+ export function logBackgroundError(prefix: string): (err: unknown) => void {
17
+ return (err) => {
18
+ log.warn(prefix, "unexpected error in background", {
19
+ error: String(err),
20
+ });
21
+ };
22
+ }
@@ -0,0 +1,82 @@
1
+ // Markdown link rewriting utilities. Originally in
2
+ // workspace/journal/linkRewrite.ts; moved to utils/ so any module
3
+ // (journal, wiki, sources) can reuse them.
4
+ //
5
+ // All functions are pure — no filesystem access.
6
+
7
+ import path from "node:path";
8
+
9
+ /**
10
+ * Rewrite every `[text](/workspace/path)` link in `content` to a
11
+ * true-relative path computed from the given current-file location.
12
+ * Non-workspace-absolute links (true relative, external URLs,
13
+ * anchors) are left untouched.
14
+ */
15
+ export function rewriteWorkspaceLinks(currentFileWsPath: string, content: string): string {
16
+ const currentDir = path.posix.dirname(currentFileWsPath);
17
+ return rewriteMarkdownLinks(content, (href) => {
18
+ if (href.startsWith("//")) return href;
19
+ if (!href.startsWith("/")) return href;
20
+ const target = href.slice(1);
21
+ if (target.length === 0) return href;
22
+ const { pathPart, suffix } = splitFragmentAndQuery(target);
23
+ const rel = path.posix.relative(currentDir, pathPart);
24
+ const safeRel = rel.length > 0 ? rel : ".";
25
+ return `${safeRel}${suffix}`;
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Walk through `input` and invoke `rewrite` for every `[text](href)`
31
+ * it encounters, substituting the returned href. Character-level scan
32
+ * (no regex) to stay lint-clean.
33
+ */
34
+ export function rewriteMarkdownLinks(input: string, rewrite: (href: string) => string): string {
35
+ const parts: string[] = [];
36
+ let i = 0;
37
+ while (i < input.length) {
38
+ if (input[i] !== "[") {
39
+ parts.push(input[i]);
40
+ i++;
41
+ continue;
42
+ }
43
+ const closeBracket = input.indexOf("]", i + 1);
44
+ if (closeBracket === -1) {
45
+ parts.push(input.slice(i));
46
+ break;
47
+ }
48
+ if (input[closeBracket + 1] !== "(") {
49
+ parts.push(input.slice(i, closeBracket + 1));
50
+ i = closeBracket + 1;
51
+ continue;
52
+ }
53
+ const openParen = closeBracket + 1;
54
+ const closeParen = input.indexOf(")", openParen + 1);
55
+ if (closeParen === -1) {
56
+ parts.push(input.slice(i));
57
+ break;
58
+ }
59
+ const linkText = input.slice(i + 1, closeBracket);
60
+ const href = input.slice(openParen + 1, closeParen);
61
+ parts.push(`[${linkText}](${rewrite(href)})`);
62
+ i = closeParen + 1;
63
+ }
64
+ return parts.join("");
65
+ }
66
+
67
+ /**
68
+ * Split a trailing `#fragment` or `?query` off a path so the caller
69
+ * can rewrite the path portion and concatenate the suffix back.
70
+ */
71
+ export function splitFragmentAndQuery(s: string): {
72
+ pathPart: string;
73
+ suffix: string;
74
+ } {
75
+ const hashIdx = s.indexOf("#");
76
+ const queryIdx = s.indexOf("?");
77
+ let cut = -1;
78
+ if (hashIdx !== -1) cut = hashIdx;
79
+ if (queryIdx !== -1 && (cut === -1 || queryIdx < cut)) cut = queryIdx;
80
+ if (cut === -1) return { pathPart: s, suffix: "" };
81
+ return { pathPart: s.slice(0, cut), suffix: s.slice(cut) };
82
+ }
@@ -0,0 +1,29 @@
1
+ // Express request helpers — shared query/param extraction.
2
+ //
3
+ // Centralizes patterns that were duplicated across route handlers
4
+ // (3+ different ways to read `req.query.session`).
5
+
6
+ // Use a minimal interface so the helpers work with any Express
7
+ // Request generic (Request<object, ...>, Request<Params, ...>, etc.)
8
+ // without type incompatibility.
9
+ interface HasQuery {
10
+ query: Record<string, unknown>;
11
+ }
12
+
13
+ /**
14
+ * Extract the session ID from `req.query.session`.
15
+ * Returns the string value, or "" if missing/non-string.
16
+ */
17
+ export function getSessionQuery(req: HasQuery): string {
18
+ const raw = req.query.session;
19
+ return typeof raw === "string" ? raw : "";
20
+ }
21
+
22
+ /**
23
+ * Extract an optional string query parameter.
24
+ * Returns the string value, or undefined if missing/non-string.
25
+ */
26
+ export function getOptionalStringQuery(req: HasQuery, key: string): string | undefined {
27
+ const raw = req.query[key];
28
+ return typeof raw === "string" ? raw : undefined;
29
+ }
@@ -0,0 +1,50 @@
1
+ import { createHash } from "crypto";
2
+
3
+ // Bits of sha256 kept as the non-ASCII fallback id. 16 base64url chars =
4
+ // 96 bits; birthday-collision expectation lives at ~2^48 entries, so
5
+ // collisions are effectively impossible for any realistic workspace.
6
+ const NON_ASCII_HASH_LEN = 16;
7
+
8
+ // eslint-disable-next-line no-control-regex
9
+ const NON_ASCII_RE = /[^\x00-\x7F]/;
10
+
11
+ export function hasNonAscii(input: string): boolean {
12
+ return NON_ASCII_RE.test(input);
13
+ }
14
+
15
+ // Deterministic short hash for inputs that can't be represented as an
16
+ // ASCII slug. base64url is URL-safe and denser than hex.
17
+ export function hashSlug(input: string, length: number = NON_ASCII_HASH_LEN): string {
18
+ return createHash("sha256").update(input, "utf-8").digest("base64url").slice(0, length);
19
+ }
20
+
21
+ // Validates a slug: lowercase alphanumeric + hyphens, 1–64 chars,
22
+ // no leading/trailing hyphen, no consecutive hyphens. Previously
23
+ // duplicated in sources/paths.ts and skills/paths.ts.
24
+ export function isValidSlug(slug: string): boolean {
25
+ if (typeof slug !== "string") return false;
26
+ if (slug.length === 0 || slug.length > 64) return false;
27
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(slug)) return false;
28
+ if (slug.includes("--")) return false;
29
+ return true;
30
+ }
31
+
32
+ export function slugify(title: string, defaultSlug = "page", maxLength = 60): string {
33
+ const asciiSlug = title
34
+ .toLowerCase()
35
+ .replace(/[^a-z0-9]+/g, "-")
36
+ .replace(/^-|-$/g, "")
37
+ .slice(0, maxLength);
38
+
39
+ if (!hasNonAscii(title)) return asciiSlug || defaultSlug;
40
+
41
+ const hash = hashSlug(title.trim());
42
+ // Preserve a meaningful ASCII prefix (e.g. "doing (進行中)" → "doing-<hash>")
43
+ // only when at least 3 chars survived the sanitise step — a shorter
44
+ // prefix wouldn't help readers distinguish entries.
45
+ if (asciiSlug.length >= 3) {
46
+ const prefixMax = Math.max(0, maxLength - hash.length - 1);
47
+ return `${asciiSlug.slice(0, prefixMax)}-${hash}`;
48
+ }
49
+ return hash;
50
+ }
@@ -0,0 +1,62 @@
1
+ // Helpers for formatting errors from `spawn`-ed Claude CLI subprocesses.
2
+ // Previously duplicated in chat-index/summarizer, sources/summarize,
3
+ // and sources/classifier — each with its own log prefix but identical
4
+ // logic. Consolidated as part of the server/utils grouping.
5
+
6
+ import { isRecord } from "./types.js";
7
+
8
+ const PREVIEW_LEN = 500;
9
+
10
+ /**
11
+ * Extract a structured error message from Claude CLI JSON stdout.
12
+ *
13
+ * The Claude CLI writes a JSON envelope on stdout when it exits
14
+ * with an error (budget exhaustion, auth failure, etc.). This
15
+ * function extracts the human-readable reason from that envelope.
16
+ * Returns `null` if stdout is not parseable JSON or the envelope
17
+ * does not indicate an error.
18
+ */
19
+ export function extractClaudeErrorMessage(stdout: string): string | null {
20
+ const text = stdout.trim();
21
+ if (!text) return null;
22
+ let parsed: unknown;
23
+ try {
24
+ parsed = JSON.parse(text);
25
+ } catch {
26
+ return null;
27
+ }
28
+ if (!isRecord(parsed)) return null;
29
+ if (parsed.is_error !== true) return null;
30
+ if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
31
+ const joined = parsed.errors.filter((e): e is string => typeof e === "string").join("; ");
32
+ if (joined.length > 0) return joined;
33
+ }
34
+ const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
35
+ const result = typeof parsed.result === "string" ? parsed.result : "";
36
+ if (subtype && result) return `${subtype}: ${result}`;
37
+ return subtype || result || null;
38
+ }
39
+
40
+ /**
41
+ * Build a human-readable error message from a Claude CLI spawn failure.
42
+ *
43
+ * Tries structured JSON extraction first (stdout), then falls back to
44
+ * stderr (plain text), then stdout as a last resort. The `prefix` is
45
+ * prepended for log-grep-ability (e.g. `"[chat-index]"`,
46
+ * `"[sources/classifier]"`).
47
+ */
48
+ export function formatSpawnFailure(prefix: string, code: number | null, stdout: string, stderr: string): string {
49
+ const structured = extractClaudeErrorMessage(stdout);
50
+ if (structured) {
51
+ return `${prefix} claude exited ${code}: ${structured}`;
52
+ }
53
+ const trimmedStderr = stderr.trim();
54
+ if (trimmedStderr.length > 0) {
55
+ return `${prefix} claude exited ${code}: ${trimmedStderr.slice(0, PREVIEW_LEN)}`;
56
+ }
57
+ const trimmedStdout = stdout.trim();
58
+ if (trimmedStdout.length > 0) {
59
+ return `${prefix} claude exited ${code}: ${trimmedStdout.slice(0, PREVIEW_LEN)}`;
60
+ }
61
+ return `${prefix} claude exited ${code}: no error output`;
62
+ }
@@ -0,0 +1,34 @@
1
+ // Common time constants in milliseconds. Avoids magic numbers like
2
+ // 3_600_000 scattered across the codebase.
3
+ //
4
+ // All server-side code should import from here instead of using raw
5
+ // numeric literals. When a specific duration is needed (e.g. a
6
+ // 5-second timeout), express it as `5 * ONE_SECOND_MS`.
7
+
8
+ export const ONE_SECOND_MS = 1_000;
9
+ export const ONE_MINUTE_MS = 60_000;
10
+ export const ONE_HOUR_MS = 3_600_000;
11
+ export const ONE_DAY_MS = 86_400_000;
12
+
13
+ /** Map time-unit suffixes (s/m/h) to milliseconds. */
14
+ export const TIME_UNIT_MS: Record<string, number> = {
15
+ s: ONE_SECOND_MS,
16
+ m: ONE_MINUTE_MS,
17
+ h: ONE_HOUR_MS,
18
+ };
19
+
20
+ // ── Common timeout presets ──────────────────────────────────────
21
+ // Named timeouts for recurring patterns. Prefer these over inline
22
+ // `5 * ONE_SECOND_MS` when the same value is used in 3+ places.
23
+
24
+ /** Quick subprocess probe (docker ps, libreoffice --version, etc.) */
25
+ export const SUBPROCESS_PROBE_TIMEOUT_MS = 5 * ONE_SECOND_MS;
26
+
27
+ /** Heavy subprocess work (libreoffice conversion, etc.) */
28
+ export const SUBPROCESS_WORK_TIMEOUT_MS = ONE_MINUTE_MS;
29
+
30
+ /** CLI subprocess timeout (claude -p for summarization, etc.) */
31
+ export const CLI_SUBPROCESS_TIMEOUT_MS = 5 * ONE_MINUTE_MS;
32
+
33
+ /** Maximum one-shot notification delay */
34
+ export const MAX_NOTIFICATION_DELAY_SEC = 3_600; // 1 hour in seconds
@@ -0,0 +1,47 @@
1
+ // Shared runtime type guards (#504).
2
+ //
3
+ // Centralised here to eliminate 40+ hand-written inline checks
4
+ // scattered across server/ and src/. Import from this module
5
+ // instead of writing `typeof x !== "object" || x === null`.
6
+
7
+ /** Narrow `unknown` to a plain object (not null, not array). */
8
+ export function isRecord(value: unknown): value is Record<string, unknown> {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+
12
+ /** Narrow `unknown` to any object (not null, arrays allowed).
13
+ * Use `isRecord` when you need to access string keys. */
14
+ export function isObj(value: unknown): value is object {
15
+ return typeof value === "object" && value !== null;
16
+ }
17
+
18
+ /** Non-empty string after trimming whitespace. */
19
+ export function isNonEmptyString(value: unknown): value is string {
20
+ return typeof value === "string" && value.trim().length > 0;
21
+ }
22
+
23
+ /** Record whose values are all strings. */
24
+ export function isStringRecord(value: unknown): value is Record<string, string> {
25
+ if (!isRecord(value)) return false;
26
+ return Object.values(value).every((v) => typeof v === "string");
27
+ }
28
+
29
+ /** String array (every element is a string). */
30
+ export function isStringArray(value: unknown): value is string[] {
31
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
32
+ }
33
+
34
+ /** Error-like object with a `code` property (e.g. Node.js fs errors). */
35
+ export function isErrorWithCode(value: unknown): value is { code: string; message?: string } {
36
+ return isRecord(value) && typeof value.code === "string";
37
+ }
38
+
39
+ /** Check that a record has a specific key with a string value. */
40
+ export function hasStringProp<K extends string>(value: unknown, key: K): value is Record<K, string> & Record<string, unknown> {
41
+ return isRecord(value) && typeof value[key] === "string";
42
+ }
43
+
44
+ /** Check that a record has a specific key with a number value. */
45
+ export function hasNumberProp<K extends string>(value: unknown, key: K): value is Record<K, number> & Record<string, unknown> {
46
+ return isRecord(value) && typeof value[key] === "number";
47
+ }
@@ -0,0 +1,153 @@
1
+ // Public entry point for the chat index. The agent route calls
2
+ // `maybeIndexSession({ sessionId, activeSessionIds })` from its
3
+ // `finally` block — fire-and-forget. This module:
4
+ //
5
+ // - skips sessions still being written by a concurrent request
6
+ // - holds a per-session lock so double-fires for the same id
7
+ // become no-ops (two sessions can still index in parallel)
8
+ // - catches ClaudeCliNotFoundError and disables itself for the
9
+ // rest of the process lifetime to avoid spamming warnings
10
+ // - catches unexpected errors and logs them so nothing bubbles
11
+ // back into the request handler
12
+ //
13
+ // All functions accept an explicit `workspaceRoot` so tests can
14
+ // point at a `mkdtempSync` directory.
15
+
16
+ import { workspacePath as defaultWorkspacePath } from "../workspace.js";
17
+ import { ClaudeCliNotFoundError } from "../journal/archivist.js";
18
+ import { indexSession, listSessionIds, type IndexerDeps } from "./indexer.js";
19
+ import { log } from "../../system/logger/index.js";
20
+
21
+ // Per-session lock. Indexing different sessions in parallel is
22
+ // fine; indexing the same session twice concurrently would just
23
+ // burn CLI budget for no benefit.
24
+ const running = new Set<string>();
25
+
26
+ // Flipped once we hit ENOENT on the `claude` CLI so we stop
27
+ // trying for the lifetime of the server process. Reset on
28
+ // restart.
29
+ let disabled = false;
30
+
31
+ export interface MaybeIndexSessionOptions {
32
+ sessionId: string;
33
+ // Skip indexing if the session is still being appended to by a
34
+ // concurrent /api/agent request — the jsonl may be mid-write.
35
+ // Ignored when `force` is true so manual rebuild runs can
36
+ // re-index even a live session (accepting that the transcript
37
+ // may be slightly out of date).
38
+ activeSessionIds?: ReadonlySet<string>;
39
+ workspaceRoot?: string;
40
+ deps?: IndexerDeps;
41
+ // Bypass the activeSessionIds guard and the isFresh throttle
42
+ // for this call. The per-session lock and the `disabled`
43
+ // sentinel are still respected — forcing doesn't help if the
44
+ // claude CLI is missing or the same session is already in
45
+ // flight.
46
+ force?: boolean;
47
+ }
48
+
49
+ // Fire-and-forget entry point. Errors are swallowed here; a
50
+ // defensive `.catch(...)` at the call site is still recommended.
51
+ export async function maybeIndexSession(opts: MaybeIndexSessionOptions): Promise<void> {
52
+ if (disabled) return;
53
+
54
+ const { sessionId } = opts;
55
+ const force = opts.force === true;
56
+ if (!force && opts.activeSessionIds?.has(sessionId)) return;
57
+ if (running.has(sessionId)) return;
58
+
59
+ // Thread `force` through the indexer via IndexerDeps so the
60
+ // freshness throttle is also bypassed on forced runs.
61
+ const effectiveDeps: IndexerDeps = {
62
+ ...(opts.deps ?? {}),
63
+ ...(force ? { force: true } : {}),
64
+ };
65
+
66
+ running.add(sessionId);
67
+ try {
68
+ await indexSession(opts.workspaceRoot ?? defaultWorkspacePath, sessionId, effectiveDeps);
69
+ } catch (err) {
70
+ if (err instanceof ClaudeCliNotFoundError) {
71
+ disabled = true;
72
+ log.warn("chat-index", err.message);
73
+ return;
74
+ }
75
+ log.warn("chat-index", "unexpected failure, continuing", {
76
+ error: String(err),
77
+ });
78
+ } finally {
79
+ running.delete(sessionId);
80
+ }
81
+ }
82
+
83
+ // Debug helper: index every session jsonl under workspace/chat/
84
+ // sequentially with `force: true`. Used by the manual rebuild
85
+ // endpoint and the CHAT_INDEX_FORCE_RUN_ON_STARTUP switch so the
86
+ // user can populate titles for existing sessions without waiting
87
+ // for each one to be revisited.
88
+ //
89
+ // Returns counts for logging. Errors on individual sessions do
90
+ // not stop the walk — the failure is logged and processing
91
+ // continues.
92
+ export interface BackfillResult {
93
+ total: number;
94
+ indexed: number;
95
+ skipped: number;
96
+ }
97
+
98
+ export async function backfillAllSessions(
99
+ opts: {
100
+ workspaceRoot?: string;
101
+ deps?: IndexerDeps;
102
+ } = {},
103
+ ): Promise<BackfillResult> {
104
+ const workspaceRoot = opts.workspaceRoot ?? defaultWorkspacePath;
105
+ const ids = await listSessionIds(workspaceRoot);
106
+ const result: BackfillResult = {
107
+ total: ids.length,
108
+ indexed: 0,
109
+ skipped: 0,
110
+ };
111
+ for (const sessionId of ids) {
112
+ if (disabled) {
113
+ result.skipped++;
114
+ continue;
115
+ }
116
+ try {
117
+ const entry = await indexSession(workspaceRoot, sessionId, {
118
+ ...(opts.deps ?? {}),
119
+ force: true,
120
+ });
121
+ if (entry) {
122
+ result.indexed++;
123
+ log.info("chat-index", "indexed", {
124
+ sessionId,
125
+ title: entry.title,
126
+ });
127
+ } else {
128
+ result.skipped++;
129
+ }
130
+ } catch (err) {
131
+ if (err instanceof ClaudeCliNotFoundError) {
132
+ disabled = true;
133
+ log.warn("chat-index", err.message);
134
+ result.skipped++;
135
+ continue;
136
+ }
137
+ result.skipped++;
138
+ log.warn("chat-index", "failed to index", {
139
+ sessionId,
140
+ error: String(err),
141
+ });
142
+ }
143
+ }
144
+ return result;
145
+ }
146
+
147
+ // Internal hook: tests need to reset the module-level `disabled`
148
+ // and `running` state between cases because node:test doesn't
149
+ // reload modules. Not part of the public runtime contract.
150
+ export function __resetForTests(): void {
151
+ disabled = false;
152
+ running.clear();
153
+ }