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,53 @@
1
+ // In-memory index of `wiki/pages/*.md` keyed by slug (= filename
2
+ // without the `.md` extension). Kept fresh via `pagesDir` mtime —
3
+ // adding, removing, or renaming a file under `pagesDir` advances
4
+ // the directory's mtime on every major filesystem we target
5
+ // (macOS APFS, Linux ext4, Windows NTFS), so one cheap `stat()`
6
+ // per request is enough to decide whether to rebuild.
7
+ //
8
+ // Eliminates the sync `readdirSync` + linear `find()` in the old
9
+ // `resolvePagePath` — see #201.
10
+
11
+ import { readDirSafeAsync, statSafeAsync } from "../../../utils/files/safe.js";
12
+
13
+ export interface PageIndex {
14
+ mtimeMs: number;
15
+ /** slug → filename (e.g. "sakura-internet" → "sakura-internet.md"). */
16
+ slugs: Map<string, string>;
17
+ }
18
+
19
+ let cache: PageIndex | null = null;
20
+
21
+ /**
22
+ * Get the page index for `pagesDir`. Returns a cached value as long
23
+ * as the directory's mtime hasn't advanced; otherwise rebuilds.
24
+ *
25
+ * Safe to call concurrently — racing builds produce the same result.
26
+ */
27
+ export async function getPageIndex(pagesDir: string): Promise<PageIndex> {
28
+ const stat = await statSafeAsync(pagesDir);
29
+ if (!stat) {
30
+ // Dir doesn't exist yet (never ingested). Return empty but
31
+ // don't cache a stale-forever value — the next call will
32
+ // re-stat and pick up the dir once it lands.
33
+ return { mtimeMs: 0, slugs: new Map() };
34
+ }
35
+ if (cache && cache.mtimeMs >= stat.mtimeMs) {
36
+ return cache;
37
+ }
38
+ const entries = await readDirSafeAsync(pagesDir);
39
+ const slugs = new Map<string, string>();
40
+ for (const entry of entries) {
41
+ if (!entry.isFile()) continue;
42
+ const name = entry.name;
43
+ if (!name.endsWith(".md")) continue;
44
+ slugs.set(name.slice(0, -".md".length), name);
45
+ }
46
+ cache = { mtimeMs: stat.mtimeMs, slugs };
47
+ return cache;
48
+ }
49
+
50
+ /** Test-only: drop the module-level cache. */
51
+ export function __resetPageIndexCache(): void {
52
+ cache = null;
53
+ }
@@ -0,0 +1,363 @@
1
+ import { Router, Request, Response } from "express";
2
+ import path from "path";
3
+ import { WORKSPACE_PATHS } from "../../workspace/paths.js";
4
+ import { readTextSafeSync, readTextSafe } from "../../utils/files/safe.js";
5
+ import { getPageIndex } from "./wiki/pageIndex.js";
6
+ import { badRequest } from "../../utils/httpError.js";
7
+ import { API_ROUTES } from "../../../src/config/apiRoutes.js";
8
+
9
+ const router = Router();
10
+
11
+ const pagesDir = () => WORKSPACE_PATHS.wikiPages;
12
+ const indexFile = () => WORKSPACE_PATHS.wikiIndex;
13
+ const logFile = () => WORKSPACE_PATHS.wikiLog;
14
+
15
+ function readFileOrEmpty(absPath: string): string {
16
+ return readTextSafeSync(absPath) ?? "";
17
+ }
18
+
19
+ export interface WikiPageEntry {
20
+ title: string;
21
+ slug: string;
22
+ description: string;
23
+ }
24
+
25
+ // Slug rules: lowercase, spaces to hyphens, strip everything that
26
+ // isn't a-z / 0-9 / hyphen. Used for both index parsing and page
27
+ // lookup so the two stay consistent.
28
+ export function wikiSlugify(text: string): string {
29
+ return text
30
+ .toLowerCase()
31
+ .replace(/\s+/g, "-")
32
+ .replace(/[^a-z0-9-]/g, "");
33
+ }
34
+
35
+ const TABLE_SEPARATOR_PATTERN = /^\|[\s|:-]+\|$/;
36
+ // Capture the href (group 2) alongside the title (group 1) so we can
37
+ // derive the slug from the file name instead of re-slugifying the
38
+ // title. This matters for non-ASCII titles like "さくらインターネット"
39
+ // where `wikiSlugify` returns "" and the slug would otherwise be lost.
40
+ const BULLET_LINK_PATTERN = /^[-*]\s+\[([^\]]+)\]\(([^)]*)\)(?:\s*[—–-]\s*(.*))?/;
41
+ const BULLET_WIKI_LINK_PATTERN = /^[-*]\s+\[\[([^\]]+)\]\](?:\s*[—–-]\s*(.*))?/;
42
+
43
+ // Each parser returns the entry it produced (if any). The parent
44
+ // loop tries them in order; the first non-null result wins.
45
+ function parseTableRow(trimmed: string): WikiPageEntry | null {
46
+ const cols = trimmed
47
+ .split("|")
48
+ .slice(1, -1)
49
+ .map((c) => c.trim().replace(/^`|`$/g, ""));
50
+ if (cols.length < 2) return null;
51
+ const slug = cols[0];
52
+ const title = cols[1] || slug;
53
+ const desc = cols[2] ?? "";
54
+ if (!slug || !title) return null;
55
+ return { title, slug, description: desc };
56
+ }
57
+
58
+ // Extract the slug segment from a bullet link's href. Accepts the
59
+ // canonical `pages/<slug>.md`, a bare `<slug>.md`, or just `<slug>`
60
+ // — the three forms produced by different historical writers of
61
+ // index.md. Returns "" for hrefs that don't look like a wiki page
62
+ // reference (e.g. `https://example.com`) so the caller can fall
63
+ // back to title-based slugification.
64
+ export function extractSlugFromBulletHref(rawHref: string): string {
65
+ const href = rawHref.trim();
66
+ if (!href) return "";
67
+ if (/^[a-z]+:\/\//i.test(href)) return "";
68
+ const lastSegment = href.split("/").pop() ?? href;
69
+ return lastSegment.replace(/\.md$/i, "");
70
+ }
71
+
72
+ function parseBulletLinkRow(trimmed: string): WikiPageEntry | null {
73
+ const m = BULLET_LINK_PATTERN.exec(trimmed);
74
+ if (!m) return null;
75
+ const title = m[1].trim();
76
+ const href = m[2] ?? "";
77
+ const desc = m[3]?.trim() ?? "";
78
+ // Prefer the slug embedded in the href so non-ASCII titles keep
79
+ // a navigable slug. Fall back to slugifying the title only when
80
+ // the href has no recognisable slug (rare — usually means the
81
+ // author put an external URL here).
82
+ const slug = extractSlugFromBulletHref(href) || wikiSlugify(title);
83
+ return { title, slug, description: desc };
84
+ }
85
+
86
+ function parseBulletWikiLinkRow(trimmed: string): WikiPageEntry | null {
87
+ const m = BULLET_WIKI_LINK_PATTERN.exec(trimmed);
88
+ if (!m) return null;
89
+ const title = m[1].trim();
90
+ const desc = m[2]?.trim() ?? "";
91
+ return { title, slug: wikiSlugify(title), description: desc };
92
+ }
93
+
94
+ // Parse entries from index.md — supports three formats:
95
+ // 1. Table: | `slug` | Title | Summary | Date |
96
+ // 2. Bullet link: - [Title](pages/slug.md) — description
97
+ // 3. Wiki link: - [[Title]] — description
98
+ export function parseIndexEntries(content: string): WikiPageEntry[] {
99
+ const entries: WikiPageEntry[] = [];
100
+ let inTable = false;
101
+
102
+ for (const line of content.split("\n")) {
103
+ const trimmed = line.trim();
104
+
105
+ if (trimmed.startsWith("|")) {
106
+ // Header / separator rows just toggle the in-table flag and
107
+ // produce no entry.
108
+ if (TABLE_SEPARATOR_PATTERN.test(trimmed) || !inTable) {
109
+ inTable = true;
110
+ continue;
111
+ }
112
+ const entry = parseTableRow(trimmed);
113
+ if (entry) entries.push(entry);
114
+ continue;
115
+ }
116
+
117
+ inTable = false;
118
+
119
+ const bullet = parseBulletLinkRow(trimmed) ?? parseBulletWikiLinkRow(trimmed);
120
+ if (bullet) entries.push(bullet);
121
+ }
122
+ return entries;
123
+ }
124
+
125
+ // Resolve a page name to an absolute `.md` path using the in-memory
126
+ // page index (see ./wiki/pageIndex.ts). Index is kept fresh via
127
+ // pagesDir mtime, so zero readdir cost on cache hit.
128
+ async function resolvePagePath(pageName: string): Promise<string | null> {
129
+ const dir = pagesDir();
130
+ const { slugs } = await getPageIndex(dir);
131
+ if (slugs.size === 0) return null;
132
+
133
+ const slug = wikiSlugify(pageName);
134
+
135
+ const exact = slugs.get(slug);
136
+ if (exact) return path.join(dir, exact);
137
+
138
+ // Fuzzy: same `includes` semantics as the old sync path — iterate
139
+ // the index's keys, no filesystem access.
140
+ for (const [key, file] of slugs) {
141
+ if (slug.includes(key) || key.includes(slug)) {
142
+ return path.join(dir, file);
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
148
+ router.get(API_ROUTES.wiki.base, async (req: Request, res: Response<WikiResponse | ErrorResponse>) => {
149
+ const slug = typeof req.query.slug === "string" ? req.query.slug : undefined;
150
+ if (slug) {
151
+ const filePath = await resolvePagePath(slug);
152
+ const content = filePath ? readFileOrEmpty(filePath) : "";
153
+ const resolvedTitle = filePath ? path.basename(filePath, ".md") : slug;
154
+ res.json({
155
+ data: {
156
+ action: "page",
157
+ title: resolvedTitle,
158
+ content,
159
+ pageName: resolvedTitle,
160
+ error: content ? undefined : `Page not found: ${slug}`,
161
+ },
162
+ message: content ? `Showing page: ${resolvedTitle}` : `Page not found: ${slug}`,
163
+ title: resolvedTitle,
164
+ instructions: "The wiki page is now displayed on the canvas.",
165
+ updating: true,
166
+ });
167
+ } else {
168
+ const content = readFileOrEmpty(indexFile());
169
+ const pageEntries = parseIndexEntries(content);
170
+ res.json({
171
+ data: { action: "index", title: "Wiki Index", content, pageEntries },
172
+ message: content ? `Wiki index — ${pageEntries.length} page(s)` : "Wiki index is empty.",
173
+ title: "Wiki Index",
174
+ instructions: "The wiki index is now displayed on the canvas.",
175
+ updating: true,
176
+ });
177
+ }
178
+ });
179
+
180
+ interface WikiBody {
181
+ action: string;
182
+ pageName?: string;
183
+ }
184
+
185
+ interface WikiData {
186
+ action: string;
187
+ title: string;
188
+ content: string;
189
+ pageEntries?: WikiPageEntry[];
190
+ pageName?: string;
191
+ error?: string;
192
+ }
193
+
194
+ interface WikiResponse {
195
+ data: WikiData;
196
+ message: string;
197
+ title: string;
198
+ instructions: string;
199
+ updating: boolean;
200
+ }
201
+
202
+ interface ErrorResponse {
203
+ error: string;
204
+ }
205
+
206
+ function buildIndexResponse(action: string): WikiResponse {
207
+ const content = readFileOrEmpty(indexFile());
208
+ const pageEntries = parseIndexEntries(content);
209
+ return {
210
+ data: { action, title: "Wiki Index", content, pageEntries },
211
+ message: content ? `Wiki index — ${pageEntries.length} page(s)` : "Wiki index is empty.",
212
+ title: "Wiki Index",
213
+ instructions: "The wiki index is now displayed on the canvas.",
214
+ updating: true,
215
+ };
216
+ }
217
+
218
+ async function buildPageResponse(action: string, pageName: string): Promise<WikiResponse> {
219
+ const filePath = await resolvePagePath(pageName);
220
+ const content = filePath ? readFileOrEmpty(filePath) : "";
221
+ const resolvedTitle = filePath ? path.basename(filePath, ".md") : pageName;
222
+ const found = !!content;
223
+ return {
224
+ data: {
225
+ action,
226
+ title: resolvedTitle,
227
+ content,
228
+ pageName: resolvedTitle,
229
+ error: found ? undefined : `Page not found: ${pageName}`,
230
+ },
231
+ message: found ? `Showing page: ${resolvedTitle}` : `Page not found: ${pageName}`,
232
+ title: resolvedTitle,
233
+ instructions: found
234
+ ? "The wiki page is now displayed on the canvas."
235
+ : `Page not found: wiki/pages/${wikiSlugify(pageName)}.md does not exist. You can create it or check the slug in wiki/index.md.`,
236
+ updating: true,
237
+ };
238
+ }
239
+
240
+ function buildLogResponse(action: string): WikiResponse {
241
+ const content = readFileOrEmpty(logFile());
242
+ return {
243
+ data: { action, title: "Activity Log", content },
244
+ message: content ? "Wiki activity log" : "Activity log is empty.",
245
+ title: "Activity Log",
246
+ instructions: "The wiki activity log is now displayed on the canvas.",
247
+ updating: true,
248
+ };
249
+ }
250
+
251
+ const WIKI_LINK_PATTERN = /\[\[([^\][\r\n]{1,200})\]\]/g;
252
+
253
+ // Pure helpers extracted from the lint pass — they take what they
254
+ // need as plain inputs so each rule can be unit-tested without
255
+ // touching the filesystem.
256
+
257
+ export function findOrphanPages(fileSlugs: ReadonlySet<string>, indexedSlugs: ReadonlySet<string>): string[] {
258
+ const issues: string[] = [];
259
+ for (const slug of fileSlugs) {
260
+ if (!indexedSlugs.has(slug)) {
261
+ issues.push(`- **Orphan page**: \`${slug}.md\` exists but is missing from index.md`);
262
+ }
263
+ }
264
+ return issues;
265
+ }
266
+
267
+ export function findMissingFiles(pageEntries: readonly WikiPageEntry[], fileSlugs: ReadonlySet<string>): string[] {
268
+ const issues: string[] = [];
269
+ for (const entry of pageEntries) {
270
+ if (!fileSlugs.has(entry.slug)) {
271
+ issues.push(`- **Missing file**: index.md references \`${entry.slug}\` but the file does not exist`);
272
+ }
273
+ }
274
+ return issues;
275
+ }
276
+
277
+ export function findBrokenLinksInPage(fileName: string, content: string, fileSlugs: ReadonlySet<string>): string[] {
278
+ const issues: string[] = [];
279
+ const wikiLinks = [...content.matchAll(WIKI_LINK_PATTERN)].map((m) => m[1]);
280
+ for (const link of wikiLinks) {
281
+ const linkSlug = wikiSlugify(link);
282
+ if (!fileSlugs.has(linkSlug)) {
283
+ issues.push(`- **Broken link** in \`${fileName}\`: [[${link}]] → \`${linkSlug}.md\` not found`);
284
+ }
285
+ }
286
+ return issues;
287
+ }
288
+
289
+ export function formatLintReport(issues: readonly string[]): string {
290
+ if (issues.length === 0) {
291
+ return "# Wiki Lint Report\n\n✓ No issues found. Wiki is healthy.";
292
+ }
293
+ const noun = `issue${issues.length !== 1 ? "s" : ""}`;
294
+ return `# Wiki Lint Report\n\n${issues.length} ${noun} found:\n\n${issues.join("\n")}`;
295
+ }
296
+
297
+ async function collectLintIssues(): Promise<string[]> {
298
+ const dir = pagesDir();
299
+ const { slugs } = await getPageIndex(dir);
300
+ if (slugs.size === 0) {
301
+ return ["- Wiki `pages/` directory does not exist yet. Start ingesting sources."];
302
+ }
303
+ const indexContent = readFileOrEmpty(indexFile());
304
+ const pageEntries = parseIndexEntries(indexContent);
305
+ const indexedSlugs = new Set(pageEntries.map((e) => e.slug));
306
+ const pageFiles = [...slugs.values()];
307
+ const fileSlugs = new Set(slugs.keys());
308
+
309
+ const issues: string[] = [];
310
+ issues.push(...findOrphanPages(fileSlugs, indexedSlugs));
311
+ issues.push(...findMissingFiles(pageEntries, fileSlugs));
312
+ // Parallel read: N small markdown files, ~50 KB each. Bounded by
313
+ // the number of wiki pages, not by CPU.
314
+ const contents = await Promise.all(
315
+ pageFiles.map(async (f) => {
316
+ const content = await readTextSafe(path.join(dir, f));
317
+ return content ?? "";
318
+ }),
319
+ );
320
+ for (let i = 0; i < pageFiles.length; i++) {
321
+ issues.push(...findBrokenLinksInPage(pageFiles[i], contents[i], fileSlugs));
322
+ }
323
+ return issues;
324
+ }
325
+
326
+ async function buildLintReportResponse(action: string): Promise<WikiResponse> {
327
+ const issues = await collectLintIssues();
328
+ const report = formatLintReport(issues);
329
+ const healthy = issues.length === 0;
330
+ return {
331
+ data: { action, title: "Wiki Lint Report", content: report },
332
+ message: healthy ? "Wiki is healthy" : `${issues.length} issue(s) found`,
333
+ title: "Wiki Lint Report",
334
+ instructions: healthy ? "Wiki is healthy — no issues found." : `${issues.length} issue(s) found that need fixing:\n${issues.join("\n")}`,
335
+ updating: true,
336
+ };
337
+ }
338
+
339
+ router.post(API_ROUTES.wiki.base, async (req: Request<object, unknown, WikiBody>, res: Response<WikiResponse | ErrorResponse>) => {
340
+ const { action, pageName } = req.body;
341
+ switch (action) {
342
+ case "index":
343
+ res.json(buildIndexResponse(action));
344
+ return;
345
+ case "page":
346
+ if (!pageName) {
347
+ badRequest(res, "pageName required for page action");
348
+ return;
349
+ }
350
+ res.json(await buildPageResponse(action, pageName));
351
+ return;
352
+ case "log":
353
+ res.json(buildLogResponse(action));
354
+ return;
355
+ case "lint_report":
356
+ res.json(await buildLintReportResponse(action));
357
+ return;
358
+ default:
359
+ badRequest(res, `Unknown action: ${action}`);
360
+ }
361
+ });
362
+
363
+ export default router;
@@ -0,0 +1,64 @@
1
+ // Compute the sandbox-auth snapshot exposed via GET /api/sandbox.
2
+ //
3
+ // The popup (`src/components/LockStatusPopup.vue`) consumes this to
4
+ // surface what credentials are actually attached to the Docker
5
+ // container, so users can confirm their `SANDBOX_SSH_AGENT_FORWARD` /
6
+ // `SANDBOX_MOUNT_CONFIGS` env vars took effect without grepping the
7
+ // startup log.
8
+ //
9
+ // Keep the payload **minimum**: names only, no host paths, no skip
10
+ // reasons, no unknown-entry lists. Full detail already lives in the
11
+ // server log via the `log.warn` / `log.info` calls inside
12
+ // `resolveSandboxAuth`. Exposing host paths to the browser is an
13
+ // intentional non-goal (see #329).
14
+
15
+ import { buildAllowedConfigMounts, resolveMountNames, sshAgentForwardArgs } from "../agent/sandboxMounts.js";
16
+
17
+ export interface SandboxStatus {
18
+ /** True iff the host SSH agent socket is bound into the container. */
19
+ sshAgent: boolean;
20
+ /**
21
+ * Allowlisted config mount names that actually got attached — i.e.
22
+ * the user requested them AND the host path exists. Order preserved
23
+ * from `SANDBOX_MOUNT_CONFIGS`.
24
+ */
25
+ mounts: string[];
26
+ }
27
+
28
+ export interface BuildSandboxStatusParams {
29
+ /** Output of `setupSandbox()` — true when Docker is running AND
30
+ * `DISABLE_SANDBOX` is unset. When false, the builder returns null
31
+ * so the handler serializes an empty `{}` body. */
32
+ sandboxEnabled: boolean;
33
+ sshAgentForward: boolean;
34
+ configMountNames: readonly string[];
35
+ sshAuthSock?: string | undefined;
36
+ home?: string;
37
+ /** Injected for tests; defaults to `process.platform`. */
38
+ platform?: typeof process.platform;
39
+ }
40
+
41
+ /**
42
+ * Returns `null` when the sandbox is disabled — the caller (Express
43
+ * handler) serializes that as `{}`, matching the agreed API contract
44
+ * (#329). When enabled, returns the structured `{ sshAgent, mounts }`
45
+ * snapshot.
46
+ *
47
+ * Pure: no logging, no side effects beyond filesystem existence
48
+ * probes done by `resolveMountNames` (same probes the agent spawner
49
+ * already runs per request).
50
+ */
51
+ export function buildSandboxStatus(params: BuildSandboxStatusParams): SandboxStatus | null {
52
+ if (!params.sandboxEnabled) return null;
53
+
54
+ const allowed = buildAllowedConfigMounts(params.home);
55
+ const parsed = resolveMountNames(params.configMountNames, allowed);
56
+
57
+ const ssh = sshAgentForwardArgs(params.sshAgentForward, params.sshAuthSock, params.platform);
58
+ const sshAgent = ssh.args.length > 0;
59
+
60
+ return {
61
+ sshAgent,
62
+ mounts: parsed.resolved.map((m) => m.name),
63
+ };
64
+ }
@@ -0,0 +1,160 @@
1
+ // Notification system (#144).
2
+ //
3
+ // Publishes NotificationPayload to:
4
+ // 1. Web pub-sub → bell badge + panel
5
+ // 2. Chat-service bridge → Telegram / CLI
6
+ //
7
+ // Callers (trigger sources) use `publishNotification()` to fire.
8
+ // In-memory store keeps the last N notifications for the bell panel.
9
+ // publishNotification() is wrapped in try-catch so failures never
10
+ // propagate to callers (e.g. endRun in session-store).
11
+
12
+ import { PUBSUB_CHANNELS } from "../../src/config/pubsubChannels.js";
13
+ import {
14
+ NOTIFICATION_KINDS,
15
+ NOTIFICATION_ICONS,
16
+ NOTIFICATION_ACTION_TYPES,
17
+ NOTIFICATION_PRIORITIES,
18
+ type NotificationPayload,
19
+ type NotificationKind,
20
+ type NotificationAction,
21
+ type NotificationPriority,
22
+ } from "../../src/types/notification.js";
23
+ import { ONE_SECOND_MS, MAX_NOTIFICATION_DELAY_SEC } from "../utils/time.js";
24
+ import { log } from "../system/logger/index.js";
25
+
26
+ // ── Dependencies (injected at startup) ──────────────────────────
27
+
28
+ export interface NotificationDeps {
29
+ publish: (channel: string, payload: unknown) => void;
30
+ pushToBridge: (transportId: string, chatId: string, message: string) => void;
31
+ }
32
+
33
+ let deps: NotificationDeps | null = null;
34
+
35
+ export function initNotifications(d: NotificationDeps): void {
36
+ deps = d;
37
+ }
38
+
39
+ // ── In-memory store ─────────────────────────────────────────────
40
+
41
+ const MAX_STORED = 50;
42
+ const store: NotificationPayload[] = [];
43
+
44
+ export function getRecentNotifications(): readonly NotificationPayload[] {
45
+ return store;
46
+ }
47
+
48
+ // ── Publish ─────────────────────────────────────────────────────
49
+
50
+ export interface PublishNotificationOpts {
51
+ kind: NotificationKind;
52
+ title: string;
53
+ body?: string;
54
+ action?: NotificationAction;
55
+ priority?: NotificationPriority;
56
+ sessionId?: string;
57
+ transportId?: string;
58
+ }
59
+
60
+ export function publishNotification(opts: PublishNotificationOpts): void {
61
+ try {
62
+ const payload: NotificationPayload = {
63
+ id: crypto.randomUUID(),
64
+ kind: opts.kind,
65
+ title: opts.title,
66
+ body: opts.body,
67
+ icon: NOTIFICATION_ICONS[opts.kind],
68
+ action: opts.action ?? { type: NOTIFICATION_ACTION_TYPES.none },
69
+ firedAt: new Date().toISOString(),
70
+ priority: opts.priority ?? NOTIFICATION_PRIORITIES.normal,
71
+ sessionId: opts.sessionId,
72
+ transportId: opts.transportId,
73
+ };
74
+
75
+ // Store for bell panel
76
+ store.unshift(payload);
77
+ if (store.length > MAX_STORED) store.length = MAX_STORED;
78
+
79
+ // Push to Web UI via pub-sub
80
+ if (deps) {
81
+ deps.publish(PUBSUB_CHANNELS.notifications, payload);
82
+ }
83
+
84
+ // Push to bridge (Telegram/CLI)
85
+ if (deps && opts.transportId) {
86
+ deps.pushToBridge(opts.transportId, "notifications", formatBridgeMessage(payload));
87
+ }
88
+
89
+ log.info("notifications", "published", {
90
+ kind: payload.kind,
91
+ title: payload.title,
92
+ id: payload.id,
93
+ });
94
+ } catch (err) {
95
+ // Never let notification failures break the caller (e.g. endRun).
96
+ log.warn("notifications", "publish failed", { error: String(err) });
97
+ }
98
+ }
99
+
100
+ function formatBridgeMessage(p: NotificationPayload): string {
101
+ const icon = p.kind === NOTIFICATION_KINDS.agent ? "\u2705" : "\u{1F514}";
102
+ const parts = [icon, p.title];
103
+ if (p.body) parts.push(p.body);
104
+ return parts.join(" ");
105
+ }
106
+
107
+ // ── Legacy test notification (kept for PoC endpoint) ────────────
108
+
109
+ export const DEFAULT_NOTIFICATION_MESSAGE = "Test notification";
110
+ export const DEFAULT_NOTIFICATION_TRANSPORT_ID = "cli";
111
+ export const DEFAULT_NOTIFICATION_CHAT_ID = "notifications";
112
+
113
+ export interface ScheduleNotificationOptions {
114
+ message?: string;
115
+ delaySeconds?: number;
116
+ transportId?: string;
117
+ chatId?: string;
118
+ }
119
+
120
+ export interface ScheduledNotification {
121
+ firesAt: string;
122
+ delaySeconds: number;
123
+ cancel: () => void;
124
+ }
125
+
126
+ export function scheduleTestNotification(opts: ScheduleNotificationOptions, legacyDeps: NotificationDeps): ScheduledNotification {
127
+ const message = opts.message ?? DEFAULT_NOTIFICATION_MESSAGE;
128
+ const transportId = opts.transportId ?? DEFAULT_NOTIFICATION_TRANSPORT_ID;
129
+ const chatId = opts.chatId ?? DEFAULT_NOTIFICATION_CHAT_ID;
130
+ const delaySeconds = clampDelay(opts.delaySeconds);
131
+ const delayMs = delaySeconds * ONE_SECOND_MS;
132
+
133
+ const firesAt = new Date(Date.now() + delayMs).toISOString();
134
+
135
+ const timer = setTimeout(() => {
136
+ publishNotification({
137
+ kind: NOTIFICATION_KINDS.push,
138
+ title: message,
139
+ priority: NOTIFICATION_PRIORITIES.normal,
140
+ });
141
+ legacyDeps.pushToBridge(transportId, chatId, message);
142
+ }, delayMs);
143
+
144
+ return {
145
+ firesAt,
146
+ delaySeconds,
147
+ cancel: () => clearTimeout(timer),
148
+ };
149
+ }
150
+
151
+ const DEFAULT_DELAY_SECONDS = 60;
152
+
153
+ function clampDelay(raw: number | undefined): number {
154
+ if (typeof raw !== "number" || !Number.isFinite(raw)) {
155
+ return DEFAULT_DELAY_SECONDS;
156
+ }
157
+ if (raw < 0) return 0;
158
+ if (raw > MAX_NOTIFICATION_DELAY_SEC) return MAX_NOTIFICATION_DELAY_SEC;
159
+ return Math.floor(raw);
160
+ }
@@ -0,0 +1,45 @@
1
+ import http from "http";
2
+ import { Server as IOServer } from "socket.io";
3
+
4
+ export interface IPubSub {
5
+ /** Publish data to all clients subscribed to this channel. */
6
+ publish(channel: string, data: unknown): void;
7
+ }
8
+
9
+ // Channel names are treated as socket.io rooms — one room per
10
+ // channel. Subscribe/unsubscribe is plain `socket.join` /
11
+ // `socket.leave`. Publish broadcasts to the room. Reconnect /
12
+ // heartbeat / multi-transport fallback are handled by socket.io
13
+ // itself, which is why we switched off raw ws.
14
+
15
+ export function createPubSub(server: http.Server): IPubSub {
16
+ const io = new IOServer(server, {
17
+ path: "/ws/pubsub",
18
+ // Server binds to 127.0.0.1 only, so CORS is moot — but
19
+ // socket.io defaults to rejecting cross-origin upgrade
20
+ // requests. Allow same-origin explicitly to cover the
21
+ // dev-proxy case (vite serves on a different port than the
22
+ // API server during `yarn dev`).
23
+ cors: { origin: true, credentials: true },
24
+ // Skip the long-poll transport negotiation: loopback-only
25
+ // deployment can always upgrade to WebSocket, and starting
26
+ // there avoids the 200ms long-poll round trip on first
27
+ // connection.
28
+ transports: ["websocket"],
29
+ });
30
+
31
+ io.on("connection", (socket) => {
32
+ socket.on("subscribe", (channel: unknown) => {
33
+ if (typeof channel === "string") socket.join(channel);
34
+ });
35
+ socket.on("unsubscribe", (channel: unknown) => {
36
+ if (typeof channel === "string") socket.leave(channel);
37
+ });
38
+ });
39
+
40
+ return {
41
+ publish(channel: string, data: unknown): void {
42
+ io.to(channel).emit("data", { channel, data });
43
+ },
44
+ };
45
+ }