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,572 @@
1
+ import "dotenv/config";
2
+ import express, { Request, Response, NextFunction } from "express";
3
+ import net from "net";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ import agentRoutes from "./api/routes/agent.js";
7
+ import todosRoutes from "./api/routes/todos.js";
8
+ import schedulerRoutes from "./api/routes/scheduler.js";
9
+ import sessionsRoutes from "./api/routes/sessions.js";
10
+ import chatIndexRoutes from "./api/routes/chat-index.js";
11
+ import sourcesRoutes from "./api/routes/sources.js";
12
+ import pluginsRoutes from "./api/routes/plugins.js";
13
+ import imageRoutes from "./api/routes/image.js";
14
+ import presentHtmlRoutes from "./api/routes/presentHtml.js";
15
+ import chartRoutes from "./api/routes/chart.js";
16
+ import rolesRoutes from "./api/routes/roles.js";
17
+ import { DEFAULT_ROLE_ID } from "../src/config/roles.js";
18
+ import mulmoScriptRoutes from "./api/routes/mulmo-script.js";
19
+ import wikiRoutes from "./api/routes/wiki.js";
20
+ import pdfRoutes from "./api/routes/pdf.js";
21
+ import filesRoutes from "./api/routes/files.js";
22
+ import configRoutes from "./api/routes/config.js";
23
+ import skillsRoutes from "./api/routes/skills.js";
24
+ import { createNotificationsRouter } from "./api/routes/notifications.js";
25
+ import { type NotificationDeps, initNotifications } from "./events/notifications.js";
26
+ import { createChatService } from "@mulmobridge/chat-service";
27
+ import { loadAllSessions } from "./api/routes/sessions.js";
28
+ import { readSessionJsonl } from "./utils/files/session-io.js";
29
+ import { onSessionEvent } from "./events/session-store/index.js";
30
+ import { getRole, loadAllRoles } from "./workspace/roles.js";
31
+ import { WORKSPACE_PATHS } from "./workspace/paths.js";
32
+ import { serverError } from "./utils/httpError.js";
33
+ import { mcpToolsRouter, mcpTools, isMcpToolEnabled } from "./agent/mcp-tools/index.js";
34
+ import { initWorkspace, workspacePath } from "./workspace/workspace.js";
35
+ import { env, isGeminiAvailable } from "./system/env.js";
36
+ import { buildSandboxStatus } from "./api/sandboxStatus.js";
37
+ import fs from "fs";
38
+ import os from "os";
39
+ import { isDockerAvailable, ensureSandboxImage } from "./system/docker.js";
40
+ import { maybeRunJournal } from "./workspace/journal/index.js";
41
+ import { backfillAllSessions } from "./workspace/chat-index/index.js";
42
+ import { createPubSub } from "./events/pub-sub/index.js";
43
+ import { PUBSUB_CHANNELS } from "../src/config/pubsubChannels.js";
44
+ import { createTaskManager } from "./events/task-manager/index.js";
45
+ import type { ITaskManager } from "./events/task-manager/index.js";
46
+ import { initScheduler, type SystemTaskDef } from "./events/scheduler-adapter.js";
47
+ import schedulerTasksRoutes from "./api/routes/schedulerTasks.js";
48
+ import { loadSchedulerOverrides, UTC_HH_MM_RE } from "./utils/files/scheduler-overrides-io.js";
49
+ import type { IPubSub } from "./events/pub-sub/index.js";
50
+ import { initSessionStore } from "./events/session-store/index.js";
51
+ import { connectRelay } from "./events/relay-client.js";
52
+ import { requireSameOrigin } from "./api/csrfGuard.js";
53
+ import { bearerAuth } from "./api/auth/bearerAuth.js";
54
+ import { deleteTokenFile, generateAndWriteToken, getCurrentToken } from "./api/auth/token.js";
55
+ import { log } from "./system/logger/index.js";
56
+ import { startChat } from "./api/routes/agent.js";
57
+ import { registerScheduledSkills } from "./workspace/skills/scheduler.js";
58
+ import { registerUserTasks } from "./workspace/skills/user-tasks.js";
59
+ import { API_ROUTES } from "../src/config/apiRoutes.js";
60
+ import { EVENT_TYPES } from "../src/types/events.js";
61
+ import { SESSION_ORIGINS } from "../src/types/session.js";
62
+ import { ONE_SECOND_MS, ONE_MINUTE_MS, ONE_HOUR_MS } from "./utils/time.js";
63
+ import { SCHEDULE_TYPES, MISSED_RUN_POLICIES } from "@receptron/task-scheduler";
64
+
65
+ const HTML_TOKEN_PLACEHOLDER = "__MULMOCLAUDE_AUTH_TOKEN__";
66
+
67
+ const __filename = fileURLToPath(import.meta.url);
68
+ const __dirname = path.dirname(__filename);
69
+
70
+ const debugMode = process.argv.includes("--debug");
71
+
72
+ initWorkspace();
73
+
74
+ let sandboxEnabled = false;
75
+
76
+ const app = express();
77
+ const PORT = env.port;
78
+
79
+ app.disable("x-powered-by");
80
+ // No `cors()` middleware. The Vite dev proxy forwards `/api/*`
81
+ // from :5173 to :3001 server-side, and in production Express
82
+ // serves the built client from the same origin, so every
83
+ // legitimate request is same-origin and doesn't need CORS
84
+ // headers at all. Dropping the middleware means a page at
85
+ // `http://evil.example` can still send a request to
86
+ // `localhost:3001` but the browser refuses to expose the
87
+ // response to the calling script (no
88
+ // `Access-Control-Allow-Origin` header). See
89
+ // plans/done/fix-server-lockdown-cors-localhost.md for the threat
90
+ // model.
91
+ app.use(express.json({ limit: "50mb" }));
92
+ // CSRF guard: reject state-changing requests that arrive with a
93
+ // non-localhost Origin header. Allows missing Origin (server-to-
94
+ // server / CLI callers) because the listener is already bound to
95
+ // localhost (#148); if that ever changes, tighten this middleware
96
+ // too. See plans/done/fix-server-csrf-origin-check.md.
97
+ app.use(requireSameOrigin);
98
+
99
+ // Bearer token auth: every `/api/*` request must carry
100
+ // `Authorization: Bearer <token>` matching the per-startup token.
101
+ // Layered *on top of* CSRF guard so we catch both cross-origin
102
+ // browser attacks (origin check) and local sibling processes that
103
+ // bypass browser CORS (bearer check). See #272 and
104
+ // plans/feat-bearer-token-auth.md.
105
+ //
106
+ // /api/files/* is exempt because <img src="/api/files/raw?path=...">
107
+ // tags in rendered markdown can't attach Authorization headers.
108
+ // The CSRF origin check + loopback-only binding still apply.
109
+ app.use("/api", (req, res, next) => {
110
+ if (req.path.startsWith("/files/")) return next();
111
+ bearerAuth(req, res, next);
112
+ });
113
+
114
+ app.get(API_ROUTES.health, (_req: Request, res: Response) => {
115
+ res.json({
116
+ status: "OK",
117
+ geminiAvailable: isGeminiAvailable(),
118
+ sandboxEnabled,
119
+ });
120
+ });
121
+
122
+ // Sandbox credential-forwarding state (#329). Returns `{}` when the
123
+ // sandbox is disabled — the popup already renders a distinct
124
+ // "No sandbox" branch in that case and extra fields would be noise.
125
+ // When enabled, returns `{ sshAgent, mounts }`; full debug detail
126
+ // (host paths, skip reasons, unknown names) stays in the server log.
127
+ app.get(API_ROUTES.sandbox, (_req: Request, res: Response) => {
128
+ const status = buildSandboxStatus({
129
+ sandboxEnabled,
130
+ sshAgentForward: env.sandboxSshAgentForward,
131
+ configMountNames: env.sandboxMountConfigs,
132
+ sshAuthSock: process.env.SSH_AUTH_SOCK,
133
+ });
134
+ res.json(status ?? {});
135
+ });
136
+
137
+ // Routers register FULL `/api/...` paths internally (see
138
+ // `src/config/apiRoutes.ts`), so they mount at root. The previous
139
+ // `app.use("/api", ...)` prefix was dropped when #289 part 1 moved
140
+ // the `/api` literal into each `router.post(API_ROUTES.…)` call.
141
+ app.use(agentRoutes);
142
+ app.use(todosRoutes);
143
+ app.use(schedulerRoutes);
144
+ app.use(sessionsRoutes);
145
+ app.use(chatIndexRoutes);
146
+ app.use(sourcesRoutes);
147
+ app.use(pluginsRoutes);
148
+ app.use(imageRoutes);
149
+ app.use(presentHtmlRoutes);
150
+ app.use(chartRoutes);
151
+ app.use(rolesRoutes);
152
+ app.use(mulmoScriptRoutes);
153
+ app.use(wikiRoutes);
154
+ app.use(pdfRoutes);
155
+ app.use(filesRoutes);
156
+ app.use(configRoutes);
157
+ app.use(skillsRoutes);
158
+ async function listSessionsForBridge(opts: { limit: number; offset: number }) {
159
+ const rows = await loadAllSessions();
160
+ const sorted = rows.sort((a, b) => b.changeMs - a.changeMs);
161
+ const total = sorted.length;
162
+ const sessions = sorted.slice(opts.offset, opts.offset + opts.limit).map((r) => ({
163
+ id: r.summary.id,
164
+ roleId: r.summary.roleId,
165
+ preview: r.summary.preview,
166
+ updatedAt: r.summary.updatedAt,
167
+ }));
168
+ return { sessions, total };
169
+ }
170
+ async function getSessionHistoryForBridge(sessionId: string, opts: { limit: number; offset: number }) {
171
+ const content = await readSessionJsonl(sessionId);
172
+ if (!content) return { messages: [], total: 0 };
173
+ const allMessages: Array<{ source: string; text: string }> = [];
174
+ const lines = content.split("\n").filter(Boolean);
175
+ // Collect all text events newest-first
176
+ for (let i = lines.length - 1; i >= 0; i--) {
177
+ try {
178
+ const entry = JSON.parse(lines[i]);
179
+ if (entry.type === EVENT_TYPES.text && typeof entry.message === "string") {
180
+ allMessages.push({
181
+ source: entry.source ?? "unknown",
182
+ text: entry.message,
183
+ });
184
+ }
185
+ } catch {
186
+ // skip malformed lines
187
+ }
188
+ }
189
+ const total = allMessages.length;
190
+ const messages = allMessages.slice(opts.offset, opts.offset + opts.limit);
191
+ return { messages, total };
192
+ }
193
+ const chatService = createChatService({
194
+ startChat,
195
+ onSessionEvent,
196
+ loadAllRoles,
197
+ getRole,
198
+ defaultRoleId: DEFAULT_ROLE_ID,
199
+ transportsDir: WORKSPACE_PATHS.transports,
200
+ logger: log,
201
+ // Socket.io handshake (see #268 Phase A) needs to validate the
202
+ // same bearer token the HTTP middleware enforces.
203
+ tokenProvider: getCurrentToken,
204
+ listSessions: listSessionsForBridge,
205
+ getSessionHistory: getSessionHistoryForBridge,
206
+ });
207
+ app.use(chatService.router);
208
+
209
+ // Notifications router. The route file needs the pub-sub publisher
210
+ // (only created inside `startRuntimeServices` after `app.listen`) and
211
+ // the chat-service push handle (available at module scope). We mount
212
+ // the router now so it sits behind the same bearer middleware as
213
+ // every other /api route, and back-fill the pub-sub dep once
214
+ // `startRuntimeServices` has it. Calls that arrive before fill-in
215
+ // (impossible in practice — the HTTP server isn't listening yet)
216
+ // would no-op on publish but still queue the bridge push.
217
+ const notificationDeps: NotificationDeps = {
218
+ publish: () => {
219
+ /* replaced by startRuntimeServices */
220
+ },
221
+ pushToBridge: chatService.pushToBridge,
222
+ };
223
+ app.use(createNotificationsRouter(notificationDeps));
224
+ app.use(mcpToolsRouter);
225
+ app.use(schedulerTasksRoutes);
226
+
227
+ if (env.isProduction) {
228
+ // `{ index: false }` so express.static doesn't intercept `GET /`
229
+ // with the built index.html. We need our own handler that reads
230
+ // the file and substitutes the bearer token placeholder on each
231
+ // request — see the wildcard fallback below.
232
+ app.use(express.static(path.join(__dirname, "../client"), { index: false }));
233
+ const indexHtmlPath = path.join(__dirname, "../client/index.html");
234
+ app.get("/{*splat}", (_req: Request, res: Response) => {
235
+ let html: string;
236
+ try {
237
+ html = fs.readFileSync(indexHtmlPath, "utf-8");
238
+ } catch (err) {
239
+ log.error("server", "failed to read index.html", { error: String(err) });
240
+ serverError(res, "Internal Server Error");
241
+ return;
242
+ }
243
+ const token = getCurrentToken() ?? "";
244
+ html = html.replace(HTML_TOKEN_PLACEHOLDER, token);
245
+ res.set("Content-Type", "text/html; charset=utf-8");
246
+ res.send(html);
247
+ });
248
+ }
249
+
250
+ app.use((err: Error, _req: Request, res: Response, __next: NextFunction) => {
251
+ log.error("express", "unhandled error", {
252
+ error: err.message,
253
+ stack: err.stack,
254
+ });
255
+ serverError(res, "Internal Server Error");
256
+ });
257
+
258
+ function isPortFree(port: number): Promise<boolean> {
259
+ return new Promise((resolve) => {
260
+ const server = net.createServer();
261
+ server.once("error", () => resolve(false));
262
+ server.once("listening", () => {
263
+ server.close(() => resolve(true));
264
+ });
265
+ // Probe the same interface we'll actually bind to so a port
266
+ // held by a different process on a different interface doesn't
267
+ // give us a false "free" reading.
268
+ server.listen(port, "127.0.0.1");
269
+ });
270
+ }
271
+
272
+ async function ensureCredentialsAvailable(): Promise<void> {
273
+ const credentialsPath = path.join(os.homedir(), ".claude", ".credentials.json");
274
+ if (fs.existsSync(credentialsPath)) return;
275
+
276
+ if (process.platform === "darwin") {
277
+ const { refreshCredentials } = await import("./system/credentials.js");
278
+ const ok = await refreshCredentials();
279
+ if (ok) return;
280
+ log.error("sandbox", "Failed to export credentials from macOS Keychain. Run `npm run sandbox:login` manually.");
281
+ process.exit(1);
282
+ }
283
+ log.error("sandbox", "Missing credentials file at ~/.claude/.credentials.json. Run `claude auth login` to authenticate Claude Code.");
284
+ process.exit(1);
285
+ }
286
+
287
+ async function setupSandbox(): Promise<boolean> {
288
+ if (env.disableSandbox) {
289
+ log.info("sandbox", "DISABLE_SANDBOX=1 — running unrestricted (debug mode)");
290
+ return false;
291
+ }
292
+ try {
293
+ const dockerAvailable = await isDockerAvailable();
294
+ if (!dockerAvailable) {
295
+ log.info("sandbox", "Docker not found — claude will run unrestricted");
296
+ return false;
297
+ }
298
+ await ensureCredentialsAvailable();
299
+ log.info("sandbox", "Docker available — building sandbox image if needed");
300
+ await ensureSandboxImage();
301
+ log.info("sandbox", "Sandbox ready");
302
+ return true;
303
+ } catch (err) {
304
+ log.error("sandbox", "Failed to set up sandbox, running unrestricted", {
305
+ error: String(err),
306
+ });
307
+ return false;
308
+ }
309
+ }
310
+
311
+ function logMcpStatus(): void {
312
+ const enabledMcpTools = mcpTools.filter(isMcpToolEnabled);
313
+ const disabledMcpTools = mcpTools.filter((t) => !isMcpToolEnabled(t));
314
+ if (enabledMcpTools.length > 0) {
315
+ log.info("mcp", "Available", {
316
+ tools: enabledMcpTools.map((t) => t.definition.name).join(", "),
317
+ });
318
+ }
319
+ if (disabledMcpTools.length > 0) {
320
+ const names = disabledMcpTools.map((t) => t.definition.name + " (" + (t.requiredEnv ?? []).join(", ") + ")").join(", ");
321
+ log.info("mcp", "Unavailable (missing env)", { tools: names });
322
+ }
323
+ }
324
+
325
+ function maybeForceJournalRun(): void {
326
+ // Debug switch: set JOURNAL_FORCE_RUN_ON_STARTUP=1 to run a full
327
+ // journal pass immediately without waiting for a session end or
328
+ // the hourly interval. Fire-and-forget — journal errors never
329
+ // propagate out of maybeRunJournal.
330
+ if (!env.journalForceRunOnStartup) return;
331
+ log.info("journal", "JOURNAL_FORCE_RUN_ON_STARTUP=1 — running now");
332
+ maybeRunJournal({ force: true }).catch((err) => {
333
+ log.warn("journal", "forced startup run failed", { error: String(err) });
334
+ });
335
+ }
336
+
337
+ function maybeForceChatIndexBackfill(): void {
338
+ // Companion switch for the chat indexer: force-rebuild every
339
+ // session's title summary on startup. Useful the first time the
340
+ // feature is rolled out over an existing workspace, or when
341
+ // debugging the indexer itself.
342
+ if (!env.chatIndexForceRunOnStartup) return;
343
+ log.info("chat-index", "CHAT_INDEX_FORCE_RUN_ON_STARTUP=1 — running now");
344
+ backfillAllSessions()
345
+ .then((result) => {
346
+ log.info("chat-index", "startup backfill complete", {
347
+ indexed: result.indexed,
348
+ total: result.total,
349
+ skipped: result.skipped,
350
+ });
351
+ })
352
+ .catch((err) => {
353
+ log.warn("chat-index", "forced startup backfill failed", {
354
+ error: String(err),
355
+ });
356
+ });
357
+ }
358
+
359
+ function startRuntimeServices(httpServer: ReturnType<typeof app.listen>): void {
360
+ log.info("server", "listening", { port: PORT });
361
+
362
+ // --- Pub/Sub ---
363
+ const pubsub = createPubSub(httpServer);
364
+ // Back-fill the notifications router with the live publisher (see
365
+ // module-scope placeholder above).
366
+ notificationDeps.publish = (channel, payload) => pubsub.publish(channel, payload);
367
+
368
+ // --- Notification system (#144) ---
369
+ initNotifications({
370
+ publish: (channel, payload) => pubsub.publish(channel, payload),
371
+ pushToBridge: chatService.pushToBridge,
372
+ });
373
+
374
+ // --- Chat socket transport (Phase A of #268) ---
375
+ chatService.attachSocket(httpServer);
376
+
377
+ // --- Relay WebSocket client ---
378
+ if (env.relayUrl && env.relayToken) {
379
+ connectRelay({
380
+ relayUrl: env.relayUrl,
381
+ relayToken: env.relayToken,
382
+ relay: chatService.relay,
383
+ logger: log,
384
+ });
385
+ }
386
+
387
+ // --- Session Store ---
388
+ initSessionStore(pubsub);
389
+
390
+ // --- Task Manager ---
391
+ const taskManager = createTaskManager({
392
+ tickMs: debugMode ? ONE_SECOND_MS : ONE_MINUTE_MS,
393
+ });
394
+
395
+ if (debugMode) {
396
+ registerDebugTasks(taskManager, pubsub);
397
+ }
398
+
399
+ // --- Scheduler (Phase 1 of #357) ---
400
+ // Register system tasks with persistence + catch-up. The journal
401
+ // and chat-index also fire from the agent finally-hook for
402
+ // responsiveness; the scheduler ensures catch-up after gaps.
403
+ const systemTasks: SystemTaskDef[] = [
404
+ {
405
+ id: "system:journal",
406
+ name: "Journal daily pass",
407
+ description: "Summarize recent chat sessions into daily + topic files",
408
+ schedule: { type: SCHEDULE_TYPES.interval, intervalMs: ONE_HOUR_MS },
409
+ missedRunPolicy: MISSED_RUN_POLICIES.runOnce,
410
+ run: () => maybeRunJournal({}),
411
+ },
412
+ {
413
+ id: "system:chat-index",
414
+ name: "Chat index backfill",
415
+ description: "Generate AI titles + summaries for un-indexed sessions",
416
+ schedule: { type: SCHEDULE_TYPES.interval, intervalMs: ONE_HOUR_MS },
417
+ missedRunPolicy: MISSED_RUN_POLICIES.runOnce,
418
+ run: () => backfillAllSessions().then(() => {}),
419
+ },
420
+ ];
421
+
422
+ // Apply user-configurable schedule overrides from
423
+ // config/scheduler/overrides.json. Missing file or unknown keys
424
+ // are silently ignored — the hardcoded defaults above remain.
425
+ const overrides = loadSchedulerOverrides();
426
+ for (const task of systemTasks) {
427
+ const ovr = overrides[task.id];
428
+ if (!ovr) continue;
429
+ if (task.schedule.type === SCHEDULE_TYPES.interval && typeof ovr.intervalMs === "number" && ovr.intervalMs > 0) {
430
+ log.info("scheduler", "applying override", {
431
+ id: task.id,
432
+ intervalMs: ovr.intervalMs,
433
+ });
434
+ task.schedule = {
435
+ type: SCHEDULE_TYPES.interval,
436
+ intervalMs: ovr.intervalMs,
437
+ };
438
+ }
439
+ if (task.schedule.type === SCHEDULE_TYPES.daily && typeof ovr.time === "string" && UTC_HH_MM_RE.test(ovr.time)) {
440
+ log.info("scheduler", "applying override", {
441
+ id: task.id,
442
+ time: ovr.time,
443
+ });
444
+ task.schedule = { type: SCHEDULE_TYPES.daily, time: ovr.time };
445
+ }
446
+ }
447
+
448
+ initScheduler(taskManager, systemTasks).catch((err) => {
449
+ log.error("scheduler", "init failed (non-fatal)", {
450
+ error: String(err),
451
+ });
452
+ });
453
+
454
+ // Register skills with schedule: frontmatter as scheduled tasks.
455
+ // Fire-and-forget — skill scan errors are logged but don't block
456
+ // server startup.
457
+ registerScheduledSkills({
458
+ taskManager,
459
+ workspaceRoot: workspacePath,
460
+ startChat,
461
+ })
462
+ .then((count) => {
463
+ if (count > 0) {
464
+ log.info("skills", "scheduled skills registered", { count });
465
+ }
466
+ })
467
+ .catch((err) => {
468
+ log.warn("skills", "failed to register scheduled skills", {
469
+ error: String(err),
470
+ });
471
+ });
472
+
473
+ // Register user-created scheduled tasks from tasks.json.
474
+ registerUserTasks({ taskManager, startChat })
475
+ .then((count) => {
476
+ if (count > 0) {
477
+ log.info("user-tasks", "user tasks registered", { count });
478
+ }
479
+ })
480
+ .catch((err) => {
481
+ log.warn("user-tasks", "failed to register user tasks", {
482
+ error: String(err),
483
+ });
484
+ });
485
+
486
+ taskManager.start();
487
+
488
+ maybeForceJournalRun();
489
+ maybeForceChatIndexBackfill();
490
+ }
491
+
492
+ // Graceful shutdown: best-effort cleanup of the auth token file so
493
+ // other readers (Vite plugin, future bridges) don't latch onto a
494
+ // dead token. Crashes that skip this are harmless — see
495
+ // plans/feat-bearer-token-auth.md; the next startup overwrites and
496
+ // the stale file's token no longer matches the live in-memory one.
497
+ let isShuttingDown = false;
498
+ async function gracefulShutdown(signal: string): Promise<void> {
499
+ if (isShuttingDown) return;
500
+ isShuttingDown = true;
501
+ log.info("server", "shutting down", { signal });
502
+ await deleteTokenFile();
503
+ process.exit(0);
504
+ }
505
+ process.on("SIGINT", () => {
506
+ gracefulShutdown("SIGINT").catch(() => process.exit(1));
507
+ });
508
+ process.on("SIGTERM", () => {
509
+ gracefulShutdown("SIGTERM").catch(() => process.exit(1));
510
+ });
511
+
512
+ (async () => {
513
+ const portFree = await isPortFree(PORT);
514
+ if (!portFree) {
515
+ log.error("server", `Port ${PORT} is already in use. Stop the other process and try again.`);
516
+ process.exit(1);
517
+ }
518
+
519
+ // Generate the bearer token before `app.listen` so the first
520
+ // request cannot race an uninitialised `getCurrentToken()`. The
521
+ // middleware defensively handles the null case anyway (401).
522
+ // `env.authTokenOverride` (#316) pins the token across restarts
523
+ // when set; otherwise a fresh random one is written.
524
+ await generateAndWriteToken(undefined, env.authTokenOverride);
525
+ log.info("auth", "bearer token written", {
526
+ path: WORKSPACE_PATHS.sessionToken,
527
+ source: env.authTokenOverride ? "env" : "random",
528
+ });
529
+
530
+ sandboxEnabled = await setupSandbox();
531
+ logMcpStatus();
532
+
533
+ // Bind to localhost-only. Using `0.0.0.0` would expose the dev
534
+ // server to the entire LAN (anyone on the same Wi-Fi could reach
535
+ // `http://<laptop-ip>:3001/api/*`), which combined with the
536
+ // workspace file API is a credential-theft risk. Personal dev
537
+ // tool — localhost is the right default.
538
+ const httpServer = app.listen(PORT, "127.0.0.1", () => {
539
+ startRuntimeServices(httpServer);
540
+ });
541
+ })();
542
+
543
+ function registerDebugTasks(taskManager: ITaskManager, pubsub: IPubSub) {
544
+ let tick = 0;
545
+
546
+ taskManager.registerTask({
547
+ id: "debug.auto-chat",
548
+ description: "Debug — toggles title color 10 times then starts a General-mode chat, then self-removes",
549
+ schedule: { type: SCHEDULE_TYPES.interval, intervalMs: ONE_SECOND_MS },
550
+ run: async () => {
551
+ tick++;
552
+ const last = tick === 10;
553
+ log.info("debug", `auto-chat countdown ${tick}/10`);
554
+ pubsub.publish(PUBSUB_CHANNELS.debugBeat, { count: tick, last });
555
+
556
+ if (!last) return;
557
+
558
+ taskManager.removeTask("debug.auto-chat");
559
+ const chatSessionId = crypto.randomUUID();
560
+ log.info("debug", "starting auto-chat", { chatSessionId });
561
+ const result = await startChat({
562
+ message: "Tell me about this app, MulmoClaude.",
563
+ roleId: DEFAULT_ROLE_ID,
564
+ chatSessionId,
565
+ origin: SESSION_ORIGINS.scheduler,
566
+ });
567
+ log.info("debug", "auto-chat result", { kind: result.kind });
568
+ },
569
+ });
570
+
571
+ log.info("debug", "Debug mode active — registered debug tasks");
572
+ }