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,209 @@
1
+ // Per-session indexing logic. `indexSession` summarizes a single
2
+ // session jsonl and writes both a per-session file and a manifest
3
+ // upsert to workspace/chat/index/. `readManifest` is a tiny helper
4
+ // the sessions route uses to join entries into its /api/sessions
5
+ // response.
6
+ //
7
+ // All functions take an explicit `workspaceRoot` so tests can point
8
+ // at a `mkdtempSync` directory without touching the real
9
+ // ~/mulmoclaude.
10
+
11
+ import { readdir, readFile } from "node:fs/promises";
12
+ import { defaultSummarize, loadJsonlInput, type SummarizeFn } from "./summarizer.js";
13
+ import { chatDirFor, indexEntryPathFor, manifestPathFor, sessionJsonlPathFor, sessionMetaPathFor } from "./paths.js";
14
+ import type { ChatIndexEntry, ChatIndexManifest } from "./types.js";
15
+ import { writeJsonAtomic } from "../../utils/files/index.js";
16
+ import { DEFAULT_ROLE_ID } from "../../../src/config/roles.js";
17
+ import { ONE_MINUTE_MS } from "../../utils/time.js";
18
+ import { isRecord } from "../../utils/types.js";
19
+
20
+ // Freshness throttle: a session whose existing index entry is
21
+ // newer than this is skipped. The 15-minute window is a compromise
22
+ // — long enough that a single conversation doesn't re-summarize
23
+ // every turn, short enough that a user who leaves for lunch and
24
+ // comes back sees the title refresh.
25
+ export const MIN_INDEX_INTERVAL_MS = 15 * ONE_MINUTE_MS;
26
+
27
+ // Injection points for tests. Defaults are the production spawn +
28
+ // wall-clock.
29
+ export interface IndexerDeps {
30
+ summarize?: SummarizeFn;
31
+ now?: () => number;
32
+ minIntervalMs?: number;
33
+ // Bypass the `isFresh` freshness throttle. Used by the
34
+ // backfill helper and the debug trigger endpoint so a manual
35
+ // "rebuild everything" run doesn't silently skip entries that
36
+ // happen to be within the 15-minute window.
37
+ force?: boolean;
38
+ }
39
+
40
+ // --- manifest I/O ---------------------------------------------------
41
+
42
+ export async function readManifest(workspaceRoot: string): Promise<ChatIndexManifest> {
43
+ try {
44
+ const raw = await readFile(manifestPathFor(workspaceRoot), "utf-8");
45
+ const parsed: unknown = JSON.parse(raw);
46
+ if (isManifest(parsed)) return parsed;
47
+ return { version: 1, entries: [] };
48
+ } catch {
49
+ return { version: 1, entries: [] };
50
+ }
51
+ }
52
+
53
+ function isManifest(raw: unknown): raw is ChatIndexManifest {
54
+ if (!isRecord(raw)) return false;
55
+ const o = raw as Record<string, unknown>;
56
+ return o.version === 1 && Array.isArray(o.entries);
57
+ }
58
+
59
+ // In-process mutex serializing the read-modify-write sequence on
60
+ // the shared manifest file. Two concurrent `indexSession` calls
61
+ // for different session ids would otherwise both read an empty
62
+ // manifest, each append their own entry, and the last writer would
63
+ // clobber the first. Chain-based mutex keeps it simple and fits
64
+ // this module's single-process assumption.
65
+ let manifestMutex: Promise<void> = Promise.resolve();
66
+
67
+ async function withManifestLock<T>(fn: () => Promise<T>): Promise<T> {
68
+ const prev = manifestMutex;
69
+ let release: () => void = () => {};
70
+ manifestMutex = new Promise<void>((resolve) => {
71
+ release = resolve;
72
+ });
73
+ try {
74
+ await prev;
75
+ return await fn();
76
+ } finally {
77
+ release();
78
+ }
79
+ }
80
+
81
+ // Atomic write: stage to a per-call unique tmp file and rename.
82
+ // The unique suffix is belt-and-suspenders — the mutex above
83
+ // already serializes callers within this process, but a unique
84
+ // name means the rename can't collide even if a stray .tmp file
85
+ // is left behind by a previous crashed run.
86
+ async function writeManifestAtomic(workspaceRoot: string, m: ChatIndexManifest): Promise<void> {
87
+ // `uniqueTmp` belt-and-suspenders: the in-process mutex above
88
+ // already serializes callers, but a unique tmp name means the
89
+ // rename can't collide even if a stray .tmp file is left behind
90
+ // by a previous crashed run.
91
+ await writeJsonAtomic(manifestPathFor(workspaceRoot), m, {
92
+ uniqueTmp: true,
93
+ });
94
+ }
95
+
96
+ // Read, mutate, and write the manifest under the in-process lock
97
+ // so concurrent callers cannot lose each other's updates.
98
+ export async function updateManifest(workspaceRoot: string, mutator: (m: ChatIndexManifest) => ChatIndexManifest): Promise<ChatIndexManifest> {
99
+ return withManifestLock(async () => {
100
+ const current = await readManifest(workspaceRoot);
101
+ const next = mutator(current);
102
+ await writeManifestAtomic(workspaceRoot, next);
103
+ return next;
104
+ });
105
+ }
106
+
107
+ // --- freshness check ------------------------------------------------
108
+
109
+ // A session is "fresh" when its per-session index file exists and
110
+ // was written less than `minIntervalMs` ago. Fresh sessions are
111
+ // skipped so a long conversation doesn't spam the CLI on every
112
+ // turn.
113
+ export async function isFresh(workspaceRoot: string, sessionId: string, now: number, minIntervalMs: number): Promise<boolean> {
114
+ try {
115
+ const raw = await readFile(indexEntryPathFor(workspaceRoot, sessionId), "utf-8");
116
+ const entry: unknown = JSON.parse(raw);
117
+ if (!isRecord(entry)) return false;
118
+ const indexedAt = (entry as Record<string, unknown>).indexedAt;
119
+ if (typeof indexedAt !== "string") return false;
120
+ const ts = Date.parse(indexedAt);
121
+ if (Number.isNaN(ts)) return false;
122
+ return now - ts < minIntervalMs;
123
+ } catch {
124
+ return false;
125
+ }
126
+ }
127
+
128
+ // --- session metadata ----------------------------------------------
129
+
130
+ interface SessionMeta {
131
+ roleId?: string;
132
+ startedAt?: string;
133
+ }
134
+
135
+ async function readSessionMeta(workspaceRoot: string, sessionId: string): Promise<SessionMeta> {
136
+ try {
137
+ const raw = await readFile(sessionMetaPathFor(workspaceRoot, sessionId), "utf-8");
138
+ const parsed: unknown = JSON.parse(raw);
139
+ if (!isRecord(parsed)) return {};
140
+ const o = parsed as Record<string, unknown>;
141
+ return {
142
+ roleId: typeof o.roleId === "string" ? o.roleId : undefined,
143
+ startedAt: typeof o.startedAt === "string" ? o.startedAt : undefined,
144
+ };
145
+ } catch {
146
+ return {};
147
+ }
148
+ }
149
+
150
+ // List every session id that has a .jsonl file in the workspace
151
+ // chat dir. Used by the backfill helper.
152
+ export async function listSessionIds(workspaceRoot: string): Promise<string[]> {
153
+ try {
154
+ const files = await readdir(chatDirFor(workspaceRoot));
155
+ return files.filter((f) => f.endsWith(".jsonl")).map((f) => f.slice(0, -".jsonl".length));
156
+ } catch {
157
+ return [];
158
+ }
159
+ }
160
+
161
+ // --- the core indexSession call ------------------------------------
162
+
163
+ // Index (or re-index) a single session. Returns the entry on
164
+ // success, or null if the session was skipped (fresh, empty,
165
+ // missing). The only exception that escapes is
166
+ // `ClaudeCliNotFoundError` — the caller uses it to disable the
167
+ // module for the rest of the process lifetime.
168
+ export async function indexSession(workspaceRoot: string, sessionId: string, deps: IndexerDeps = {}): Promise<ChatIndexEntry | null> {
169
+ const summarize = deps.summarize ?? defaultSummarize;
170
+ const now = (deps.now ?? Date.now)();
171
+ const minInterval = deps.minIntervalMs ?? MIN_INDEX_INTERVAL_MS;
172
+ const force = deps.force === true;
173
+
174
+ if (!force && (await isFresh(workspaceRoot, sessionId, now, minInterval))) {
175
+ return null;
176
+ }
177
+
178
+ const input = await loadJsonlInput(sessionJsonlPathFor(workspaceRoot, sessionId));
179
+ if (!input.trim()) return null;
180
+
181
+ const summary = await summarize(input);
182
+ const meta = await readSessionMeta(workspaceRoot, sessionId);
183
+
184
+ const entry: ChatIndexEntry = {
185
+ id: sessionId,
186
+ roleId: meta.roleId ?? DEFAULT_ROLE_ID,
187
+ startedAt: meta.startedAt ?? new Date(now).toISOString(),
188
+ indexedAt: new Date(now).toISOString(),
189
+ title: summary.title,
190
+ summary: summary.summary,
191
+ keywords: summary.keywords,
192
+ };
193
+
194
+ // Per-session file is written first so partial progress survives
195
+ // a crash between the two writes: the next run can still observe
196
+ // the fresh entry via isFresh and skip it.
197
+ await writeJsonAtomic(indexEntryPathFor(workspaceRoot, sessionId), entry);
198
+
199
+ // Upsert into manifest under the in-process lock: replace any
200
+ // prior entry with the same id, sort newest-first by startedAt.
201
+ await updateManifest(workspaceRoot, (current) => {
202
+ const filtered = current.entries.filter((e) => e.id !== sessionId);
203
+ filtered.push(entry);
204
+ filtered.sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt));
205
+ return { version: 1, entries: filtered };
206
+ });
207
+
208
+ return entry;
209
+ }
@@ -0,0 +1,34 @@
1
+ // Pure path helpers for the chat index cache. Kept in their own
2
+ // file so tests can compute expected paths without needing the
3
+ // summarizer / indexer modules (which transitively pull in the
4
+ // claude CLI spawn code).
5
+
6
+ import path from "node:path";
7
+
8
+ export const CHAT_DIR = "chat";
9
+ export const INDEX_DIR = "index";
10
+ export const MANIFEST_FILE = "manifest.json";
11
+
12
+ export function chatDirFor(workspaceRoot: string): string {
13
+ return path.join(workspaceRoot, CHAT_DIR);
14
+ }
15
+
16
+ export function indexDirFor(workspaceRoot: string): string {
17
+ return path.join(chatDirFor(workspaceRoot), INDEX_DIR);
18
+ }
19
+
20
+ export function sessionJsonlPathFor(workspaceRoot: string, sessionId: string): string {
21
+ return path.join(chatDirFor(workspaceRoot), `${sessionId}.jsonl`);
22
+ }
23
+
24
+ export function sessionMetaPathFor(workspaceRoot: string, sessionId: string): string {
25
+ return path.join(chatDirFor(workspaceRoot), `${sessionId}.json`);
26
+ }
27
+
28
+ export function indexEntryPathFor(workspaceRoot: string, sessionId: string): string {
29
+ return path.join(indexDirFor(workspaceRoot), `${sessionId}.json`);
30
+ }
31
+
32
+ export function manifestPathFor(workspaceRoot: string): string {
33
+ return path.join(indexDirFor(workspaceRoot), MANIFEST_FILE);
34
+ }
@@ -0,0 +1,247 @@
1
+ // Summarizes a single session jsonl into a title / summary /
2
+ // keywords triple using the Claude Code CLI. Cherry-picked and
3
+ // trimmed from the closed PR #94.
4
+ //
5
+ // Splits cleanly into three layers so tests can exercise the pure
6
+ // bits without spawning the CLI:
7
+ //
8
+ // extractText / truncate — jsonl → prompt input
9
+ // parseClaudeJsonResult — CLI stdout → SummaryResult
10
+ // validateSummaryResult — unknown → SummaryResult
11
+ //
12
+ // `defaultSummarize` composes them with the real spawn; tests
13
+ // inject their own SummarizeFn via `IndexerDeps.summarize`.
14
+
15
+ import { spawn } from "node:child_process";
16
+ import { EVENT_TYPES } from "../../../src/types/events.js";
17
+ import { readFile } from "node:fs/promises";
18
+ import { formatSpawnFailure } from "../../utils/spawn.js";
19
+ import { tmpdir } from "node:os";
20
+ import { ClaudeCliNotFoundError } from "../journal/archivist.js";
21
+ import { errorMessage } from "../../utils/errors.js";
22
+ import type { SummaryResult } from "./types.js";
23
+ import { ONE_MINUTE_MS } from "../../utils/time.js";
24
+ import { isRecord } from "../../utils/types.js";
25
+
26
+ const SYSTEM_PROMPT =
27
+ "You summarize a single chat session. Output strict JSON matching the provided schema. " +
28
+ "Rules: title <= 60 characters in the source language, summary <= 200 characters in the same language, " +
29
+ "5 to 10 short lowercase keywords useful for search. Respond with structured output only.";
30
+
31
+ const SUMMARY_SCHEMA = {
32
+ type: "object",
33
+ properties: {
34
+ title: { type: "string" },
35
+ summary: { type: "string" },
36
+ keywords: { type: "array", items: { type: "string" } },
37
+ },
38
+ required: ["title", "summary", "keywords"],
39
+ };
40
+
41
+ // Prompt-building constants.
42
+ const MAX_INPUT_CHARS = 8000;
43
+ const HEAD_CHARS = 3000;
44
+ const TAIL_CHARS = 5000;
45
+ const PER_MESSAGE_MAX = 500;
46
+
47
+ // Spawn / budget constants.
48
+ const DEFAULT_TIMEOUT_MS = 2 * ONE_MINUTE_MS;
49
+ // Budget cap per summarization call, forwarded to `claude
50
+ // --max-budget-usd`. Previously 0.05 but that was tight enough
51
+ // that a first-burst call — which pays a one-time cache creation
52
+ // cost on haiku (~28k cache-creation tokens) — would trip the cap
53
+ // and fail with `error_max_budget_usd` even for tiny 600-char
54
+ // transcripts. 0.15 leaves comfortable headroom for cache
55
+ // creation + a generous output allowance while still capping a
56
+ // full 100-session backfill to well under $20.
57
+ const MAX_BUDGET_USD = 0.15;
58
+
59
+ // Any module that wants to drive the summarizer — including the
60
+ // indexer — takes a SummarizeFn so tests can supply a deterministic
61
+ // fake. Production path is `defaultSummarize` below.
62
+ export type SummarizeFn = (input: string) => Promise<SummaryResult>;
63
+
64
+ interface JsonlEntry {
65
+ source?: string;
66
+ type?: string;
67
+ message?: string;
68
+ }
69
+
70
+ function trimMessage(text: string): string {
71
+ if (text.length <= PER_MESSAGE_MAX) return text;
72
+ return `${text.slice(0, PER_MESSAGE_MAX)}…`;
73
+ }
74
+
75
+ // Walk a session jsonl and keep only the user / assistant text
76
+ // turns, joined into a compact transcript. Tool results are
77
+ // skipped because they are noisy and rarely contribute to a useful
78
+ // summary title.
79
+ export function extractText(jsonlContent: string): string {
80
+ const lines = jsonlContent.split("\n").filter(Boolean);
81
+ const parts: string[] = [];
82
+ for (const line of lines) {
83
+ let entry: JsonlEntry;
84
+ try {
85
+ entry = JSON.parse(line);
86
+ } catch {
87
+ continue;
88
+ }
89
+ const source = entry.source;
90
+ if ((source === "user" || source === "assistant") && entry.type === EVENT_TYPES.text && typeof entry.message === "string") {
91
+ parts.push(`[${source}] ${trimMessage(entry.message)}`);
92
+ }
93
+ }
94
+ return parts.join("\n\n");
95
+ }
96
+
97
+ // Long sessions are truncated to first ~3000 + last ~5000 chars so
98
+ // claude sees both the original topic and the most recent state.
99
+ export function truncate(text: string): string {
100
+ if (text.length <= MAX_INPUT_CHARS) return text;
101
+ const head = text.slice(0, HEAD_CHARS);
102
+ const tail = text.slice(-TAIL_CHARS);
103
+ return `${head}\n\n…\n\n${tail}`;
104
+ }
105
+
106
+ interface ClaudeJsonResult {
107
+ type?: string;
108
+ is_error?: boolean;
109
+ structured_output?: unknown;
110
+ result?: string;
111
+ }
112
+
113
+ // Parse the JSON envelope that `claude --output-format json`
114
+ // prints, raising a useful error if the envelope is malformed or
115
+ // the CLI reported an error.
116
+ export function parseClaudeJsonResult(stdout: string): SummaryResult {
117
+ let parsed: ClaudeJsonResult;
118
+ try {
119
+ parsed = JSON.parse(stdout.trim());
120
+ } catch (err) {
121
+ throw new Error(`[chat-index] failed to parse claude json output: ${errorMessage(err)}`);
122
+ }
123
+ if (parsed.is_error) {
124
+ throw new Error(`[chat-index] claude returned error: ${parsed.result ?? "unknown"}`);
125
+ }
126
+ return validateSummaryResult(parsed.structured_output);
127
+ }
128
+
129
+ // Build the error message for a non-zero `claude` CLI exit.
130
+ //
131
+ // The claude CLI writes its structured result — including error
132
+ // envelopes like `{"is_error":true,"subtype":"error_max_budget_usd",
133
+ // "errors":["Reached maximum budget ($0.05)"]}` — to **stdout**,
134
+ // not stderr. Our previous handler only inspected stderr, so
135
+ // budget-exhaustion and similar failures surfaced as
136
+ // `claude summarize exited 1:` with no details at all, making
137
+ // them impossible to diagnose from the log.
138
+ //
139
+ // Strategy: try to parse stdout as a claude JSON envelope first
140
+ // and extract a human-readable reason from `errors[]` /
141
+ // `subtype` / `result`; fall back to stderr, then to a raw
142
+ // stdout slice, then to a generic "no error output".
143
+ export function formatSpawnError(code: number | null, stdout: string, stderr: string): string {
144
+ return formatSpawnFailure("[chat-index]", code, stdout, stderr);
145
+ }
146
+
147
+ // Runtime-validate an arbitrary value into a SummaryResult. Missing
148
+ // or wrong-typed fields fall back to safe defaults rather than
149
+ // crashing the indexer — a degraded title is better than a dropped
150
+ // session.
151
+ export function validateSummaryResult(obj: unknown): SummaryResult {
152
+ if (!isRecord(obj)) {
153
+ throw new Error("[chat-index] summary result is not an object");
154
+ }
155
+ const o = obj as Record<string, unknown>;
156
+ const title = typeof o.title === "string" ? o.title : "";
157
+ const summary = typeof o.summary === "string" ? o.summary : "";
158
+ const keywords = Array.isArray(o.keywords) ? o.keywords.filter((k): k is string => typeof k === "string") : [];
159
+ return { title, summary, keywords };
160
+ }
161
+
162
+ // Read a jsonl file and produce the pre-truncated transcript that
163
+ // goes into the CLI prompt. Returns the empty string for an empty
164
+ // or unreadable file so the caller can decide whether to skip.
165
+ export async function loadJsonlInput(jsonlPath: string): Promise<string> {
166
+ try {
167
+ const content = await readFile(jsonlPath, "utf-8");
168
+ return truncate(extractText(content));
169
+ } catch {
170
+ return "";
171
+ }
172
+ }
173
+
174
+ // --- spawn layer ----------------------------------------------------
175
+
176
+ function spawnClaudeSummarize(input: string, timeoutMs: number): Promise<string> {
177
+ return new Promise((resolve, reject) => {
178
+ const args = [
179
+ "--print",
180
+ "--no-session-persistence",
181
+ "--output-format",
182
+ "json",
183
+ "--model",
184
+ "haiku",
185
+ "--max-budget-usd",
186
+ String(MAX_BUDGET_USD),
187
+ "--json-schema",
188
+ JSON.stringify(SUMMARY_SCHEMA),
189
+ "--system-prompt",
190
+ SYSTEM_PROMPT,
191
+ "-p",
192
+ input,
193
+ ];
194
+ // Run from tmpdir so claude does not load the project's
195
+ // CLAUDE.md / plugins / memory and inflate the context.
196
+ const proc = spawn("claude", args, {
197
+ cwd: tmpdir(),
198
+ stdio: ["ignore", "pipe", "pipe"],
199
+ });
200
+
201
+ let stdout = "";
202
+ let stderr = "";
203
+ let settled = false;
204
+
205
+ const timer = setTimeout(() => {
206
+ if (settled) return;
207
+ settled = true;
208
+ proc.kill("SIGKILL");
209
+ reject(new Error(`[chat-index] claude summarize timed out after ${timeoutMs}ms`));
210
+ }, timeoutMs);
211
+
212
+ proc.stdout.on("data", (chunk: Buffer) => {
213
+ stdout += chunk.toString();
214
+ });
215
+ proc.stderr.on("data", (chunk: Buffer) => {
216
+ stderr += chunk.toString();
217
+ });
218
+ proc.on("error", (err: Error & { code?: string }) => {
219
+ if (settled) return;
220
+ settled = true;
221
+ clearTimeout(timer);
222
+ if (err.code === "ENOENT") {
223
+ reject(new ClaudeCliNotFoundError());
224
+ } else {
225
+ reject(err);
226
+ }
227
+ });
228
+ proc.on("close", (code) => {
229
+ if (settled) return;
230
+ settled = true;
231
+ clearTimeout(timer);
232
+ if (code !== 0) {
233
+ reject(new Error(formatSpawnError(code, stdout, stderr)));
234
+ return;
235
+ }
236
+ resolve(stdout);
237
+ });
238
+ });
239
+ }
240
+
241
+ // Production SummarizeFn: prepare the input from a jsonl path and
242
+ // drive the CLI. Tests inject their own SummarizeFn that bypasses
243
+ // the CLI entirely.
244
+ export const defaultSummarize: SummarizeFn = async (input: string) => {
245
+ const stdout = await spawnClaudeSummarize(input, DEFAULT_TIMEOUT_MS);
246
+ return parseClaudeJsonResult(stdout);
247
+ };
@@ -0,0 +1,38 @@
1
+ // On-disk shapes for the per-session chat summaries cached under
2
+ // workspace/chat/index/. These power the title + summary shown for
3
+ // past sessions in the sidebar history pane. The full design lives
4
+ // in plans/done/feat-session-index-titles.md.
5
+
6
+ export interface SummaryResult {
7
+ // <= 60 chars in the source language
8
+ title: string;
9
+ // <= 200 chars in the source language
10
+ summary: string;
11
+ // 5-10 short lowercase keywords
12
+ keywords: string[];
13
+ }
14
+
15
+ // One cached summary per session. Written to chat/index/<id>.json
16
+ // and also mirrored into manifest.json for bulk-read from the
17
+ // /api/sessions route.
18
+ export interface ChatIndexEntry {
19
+ id: string;
20
+ roleId: string;
21
+ startedAt: string;
22
+ // ISO timestamp of when this summary was produced. Used by the
23
+ // freshness throttle — we skip re-summarizing a session whose
24
+ // existing entry is less than MIN_INDEX_INTERVAL_MS old, so a
25
+ // 20-turn conversation over 30 min summarizes ~twice, not 20
26
+ // times. See `isFresh` in indexer.ts.
27
+ indexedAt: string;
28
+ title: string;
29
+ summary: string;
30
+ keywords: string[];
31
+ }
32
+
33
+ export interface ChatIndexManifest {
34
+ version: 1;
35
+ // Sorted newest-first by startedAt so the sidebar gets them in
36
+ // display order without a second sort pass.
37
+ entries: ChatIndexEntry[];
38
+ }