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,329 @@
1
+ // Host-credential mounts for the Docker sandbox (#259).
2
+ //
3
+ // Two independent opt-in mechanisms, composable:
4
+ //
5
+ // SANDBOX_SSH_AGENT_FORWARD=1
6
+ // Bind-mounts $SSH_AUTH_SOCK into the container and sets
7
+ // SSH_AUTH_SOCK to the container path. Private keys stay on the
8
+ // host — the agent on the host signs on behalf of the container.
9
+ //
10
+ // SANDBOX_MOUNT_CONFIGS=gh,gitconfig
11
+ // CSV of allowlisted config mounts. Each name resolves to a fixed
12
+ // host path via the server-side ALLOWED_CONFIG_MOUNTS map; users
13
+ // cannot pass arbitrary paths.
14
+ //
15
+ // See docs/sandbox-credentials.md for the user-facing contract.
16
+
17
+ import path from "node:path";
18
+ import fs from "node:fs";
19
+ import { execFileSync } from "node:child_process";
20
+ import { homedir } from "node:os";
21
+ import { log } from "../system/logger/index.js";
22
+ import { SUBPROCESS_PROBE_TIMEOUT_MS } from "../utils/time.js";
23
+
24
+ // ── Config-mount allowlist ──────────────────────────────────────────
25
+
26
+ export interface SandboxMountSpec {
27
+ /** The short name users type in SANDBOX_MOUNT_CONFIGS. */
28
+ name: string;
29
+ /** Absolute path on the host. Resolved from `$HOME` at lookup. */
30
+ hostPath: string;
31
+ /** Absolute path inside the container (must match where the tool looks). */
32
+ containerPath: string;
33
+ /** Whether the host path is expected to be a file or a directory. */
34
+ kind: "file" | "dir";
35
+ /** Short human description — shown in docs and in startup logs. */
36
+ description: string;
37
+ }
38
+
39
+ /**
40
+ * Build the allowlist. Parameterized on `home` so tests can inject a
41
+ * temp directory without touching the real filesystem.
42
+ *
43
+ * To add a new tool:
44
+ * 1. Append a row here with the host path the tool reads on startup
45
+ * and the container path it should find the same file at.
46
+ * 2. Add a row in docs/sandbox-credentials.md.
47
+ * 3. That's it — no env var changes, no parser changes.
48
+ */
49
+ export function buildAllowedConfigMounts(home: string = homedir()): Record<string, SandboxMountSpec> {
50
+ return {
51
+ gh: {
52
+ name: "gh",
53
+ hostPath: path.join(home, ".config", "gh"),
54
+ containerPath: "/home/node/.config/gh",
55
+ kind: "dir",
56
+ description: "GitHub CLI auth token + hosts config",
57
+ },
58
+ gitconfig: {
59
+ name: "gitconfig",
60
+ hostPath: path.join(home, ".gitconfig"),
61
+ containerPath: "/home/node/.gitconfig",
62
+ kind: "file",
63
+ description: "Git user identity (name, email, signing key)",
64
+ },
65
+ };
66
+ }
67
+
68
+ // ── Name parsing / validation ──────────────────────────────────────
69
+
70
+ export interface ParsedMountList {
71
+ /** Names that resolved to a spec. Order preserved. */
72
+ resolved: SandboxMountSpec[];
73
+ /** Names the user requested that aren't in the allowlist. */
74
+ unknown: string[];
75
+ /** Names whose host path does not exist — silently skipped. */
76
+ missing: SandboxMountSpec[];
77
+ }
78
+
79
+ /**
80
+ * Parse a CSV list of mount names, resolve against the allowlist,
81
+ * check that each host path exists. The three output buckets let the
82
+ * caller decide what to error on (unknown) vs warn on (missing).
83
+ */
84
+ export function resolveMountNames(names: readonly string[], allowed: Record<string, SandboxMountSpec> = buildAllowedConfigMounts()): ParsedMountList {
85
+ const resolved: SandboxMountSpec[] = [];
86
+ const unknown: string[] = [];
87
+ const missing: SandboxMountSpec[] = [];
88
+
89
+ for (const raw of names) {
90
+ const name = raw.trim();
91
+ if (!name) continue;
92
+ const spec = allowed[name];
93
+ if (!spec) {
94
+ unknown.push(name);
95
+ continue;
96
+ }
97
+ if (!hostPathExists(spec)) {
98
+ missing.push(spec);
99
+ continue;
100
+ }
101
+ resolved.push(spec);
102
+ }
103
+ return { resolved, unknown, missing };
104
+ }
105
+
106
+ function hostPathExists(spec: SandboxMountSpec): boolean {
107
+ try {
108
+ const stat = fs.statSync(spec.hostPath);
109
+ return spec.kind === "dir" ? stat.isDirectory() : stat.isFile();
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ // ── Docker arg generation ──────────────────────────────────────────
116
+
117
+ /**
118
+ * Return the `-v ...` argument pairs for the given resolved mounts.
119
+ * Always read-only. The caller splices these into the full docker
120
+ * argv in `buildDockerSpawnArgs`.
121
+ */
122
+ export function configMountArgs(resolved: readonly SandboxMountSpec[]): string[] {
123
+ const args: string[] = [];
124
+ for (const spec of resolved) {
125
+ args.push("-v", `${toDockerPath(spec.hostPath)}:${spec.containerPath}:ro`);
126
+ }
127
+ return args;
128
+ }
129
+
130
+ // ── SSH agent forward ──────────────────────────────────────────────
131
+
132
+ /** Absolute container path the agent socket is bound to. */
133
+ export const SSH_AGENT_CONTAINER_SOCK = "/ssh-agent";
134
+
135
+ export interface SshAgentForwardResult {
136
+ args: string[];
137
+ /** When null, forward was requested but not possible; caller decides
138
+ * whether to log once (we always log in the production driver). */
139
+ skippedReason: string | null;
140
+ }
141
+
142
+ // Docker Desktop for Mac exposes the host SSH agent through a
143
+ // well-known magic socket inside the VM. Direct bind-mounting the
144
+ // macOS $SSH_AUTH_SOCK (/private/tmp/…) fails with "operation not
145
+ // supported" because Docker's Linux VM can't mkdir a Unix socket.
146
+ // Using the magic path sidesteps the issue entirely and works on
147
+ // Docker Desktop ≥ 2.3.0 (2020+).
148
+ const DOCKER_DESKTOP_MAC_SSH_SOCK = "/run/host-services/ssh-auth.sock";
149
+
150
+ /**
151
+ * Return the docker argv fragment that forwards the host SSH agent
152
+ * into the container. On macOS + Docker Desktop, the built-in
153
+ * magic socket is used instead of a raw bind-mount. On Linux, the
154
+ * host `$SSH_AUTH_SOCK` is bind-mounted directly.
155
+ *
156
+ * Skipped (empty args + reason) when:
157
+ * - the flag is off
158
+ * - $SSH_AUTH_SOCK isn't set (no agent running on host) — on
159
+ * non-macOS only; macOS always has the magic socket available
160
+ * when Docker Desktop is running, regardless of $SSH_AUTH_SOCK
161
+ */
162
+ export function sshAgentForwardArgs(
163
+ enabled: boolean,
164
+ sshAuthSock: string | undefined,
165
+ platform: typeof process.platform = process.platform,
166
+ ): SshAgentForwardResult {
167
+ if (!enabled) return { args: [], skippedReason: null };
168
+
169
+ // macOS + Docker Desktop: use the magic VM-internal socket.
170
+ if (platform === "darwin") {
171
+ return {
172
+ args: ["-v", `${DOCKER_DESKTOP_MAC_SSH_SOCK}:${SSH_AGENT_CONTAINER_SOCK}`, "-e", `SSH_AUTH_SOCK=${SSH_AGENT_CONTAINER_SOCK}`],
173
+ skippedReason: null,
174
+ };
175
+ }
176
+
177
+ // Linux / other: bind-mount the host socket directly.
178
+ if (!sshAuthSock || sshAuthSock.length === 0) {
179
+ return {
180
+ args: [],
181
+ skippedReason: "SSH_AUTH_SOCK not set on host",
182
+ };
183
+ }
184
+ if (!fs.existsSync(sshAuthSock)) {
185
+ return {
186
+ args: [],
187
+ skippedReason: `SSH_AUTH_SOCK=${sshAuthSock} not found on host`,
188
+ };
189
+ }
190
+ return {
191
+ args: ["-v", `${toDockerPath(sshAuthSock)}:${SSH_AGENT_CONTAINER_SOCK}`, "-e", `SSH_AUTH_SOCK=${SSH_AGENT_CONTAINER_SOCK}`],
192
+ skippedReason: null,
193
+ };
194
+ }
195
+
196
+ // ── Top-level resolver used by buildDockerSpawnArgs ────────────────
197
+
198
+ export interface ResolvedSandboxAuth {
199
+ /** docker argv additions: a list of `-v` / `-e` tokens. */
200
+ args: string[];
201
+ /** Descriptions the caller can log once to show what got mounted. */
202
+ appliedDescriptions: string[];
203
+ }
204
+
205
+ export interface ResolveSandboxAuthParams {
206
+ sshAgentForward: boolean;
207
+ /** Comma-separated host whitelist for the SSH agent. Default
208
+ * "github.com". Passed to the container as
209
+ * `SANDBOX_SSH_ALLOWED_HOSTS` and consumed by the entrypoint
210
+ * to generate a restrictive `~/.ssh/config`. */
211
+ sshAllowedHosts?: string;
212
+ configMountNames: readonly string[];
213
+ sshAuthSock?: string | undefined;
214
+ home?: string;
215
+ }
216
+
217
+ /**
218
+ * Combine the two mechanisms. Emits a `log.warn` for unknown names
219
+ * (configuration error the user should fix), a `log.info` for missing
220
+ * paths (expected when a user hasn't set up the tool), and a
221
+ * `log.info` line listing what actually got mounted so the startup
222
+ * log shows the sandbox's effective auth posture.
223
+ */
224
+ export function resolveSandboxAuth(params: ResolveSandboxAuthParams): ResolvedSandboxAuth {
225
+ const home = params.home ?? homedir();
226
+ const allowed = buildAllowedConfigMounts(home);
227
+ const parsed = resolveMountNames(params.configMountNames, allowed);
228
+
229
+ if (parsed.unknown.length > 0) {
230
+ log.warn("sandbox", "unknown SANDBOX_MOUNT_CONFIGS entries ignored", {
231
+ unknown: parsed.unknown,
232
+ allowed: Object.keys(allowed),
233
+ });
234
+ }
235
+ for (const spec of parsed.missing) {
236
+ log.info("sandbox", "config mount skipped (host path missing)", {
237
+ name: spec.name,
238
+ hostPath: spec.hostPath,
239
+ });
240
+ }
241
+
242
+ const sshResult = sshAgentForwardArgs(params.sshAgentForward, params.sshAuthSock);
243
+ if (sshResult.skippedReason !== null) {
244
+ log.warn("sandbox", "SSH agent forward requested but skipped", {
245
+ reason: sshResult.skippedReason,
246
+ });
247
+ }
248
+
249
+ // Pass the allowed-hosts whitelist to the container so the
250
+ // entrypoint can generate a restrictive ~/.ssh/config. Only
251
+ // included when SSH agent forward is actually active.
252
+ const sshAllowedHostsArgs = sshResult.args.length > 0 && params.sshAllowedHosts ? ["-e", `SANDBOX_SSH_ALLOWED_HOSTS=${params.sshAllowedHosts}`] : [];
253
+
254
+ // gh CLI keyring fallback (#259 + #164). When the user opted in
255
+ // to `gh` via SANDBOX_MOUNT_CONFIGS but the file mount succeeded
256
+ // with a keyring-based token (macOS), the mounted hosts.yml won't
257
+ // contain the actual token. Detect this and inject GH_TOKEN env
258
+ // var instead. Only runs when "gh" was explicitly requested.
259
+ const ghTokenArgs = resolveGhTokenFallback(params.configMountNames, parsed);
260
+
261
+ const args = [...configMountArgs(parsed.resolved), ...sshResult.args, ...sshAllowedHostsArgs, ...ghTokenArgs.args];
262
+ const allowedHostsSuffix = sshResult.args.length > 0 && params.sshAllowedHosts ? ` → hosts: ${params.sshAllowedHosts}` : "";
263
+ const appliedDescriptions = [
264
+ ...parsed.resolved.map((s) => `${s.name} (${s.description})`),
265
+ ...(sshResult.args.length > 0 ? [`ssh-agent forward${allowedHostsSuffix}`] : []),
266
+ ...(ghTokenArgs.args.length > 0 ? ["gh CLI (GH_TOKEN fallback)"] : []),
267
+ ];
268
+
269
+ if (appliedDescriptions.length > 0) {
270
+ log.info("sandbox", "host credentials attached to container", {
271
+ mounts: appliedDescriptions,
272
+ });
273
+ }
274
+
275
+ return { args, appliedDescriptions };
276
+ }
277
+
278
+ // ── GitHub CLI token fallback ──────────────────────────────────────
279
+
280
+ // When the user opted in to `gh` via SANDBOX_MOUNT_CONFIGS, the
281
+ // file mount may not carry a usable token — macOS stores it in the
282
+ // system keyring, not in ~/.config/gh/hosts.yml. In that case we
283
+ // extract the token via `gh auth token` on the host and pass it as
284
+ // GH_TOKEN env var. This only runs when "gh" was explicitly
285
+ // requested (#259 opt-in principle).
286
+ function resolveGhTokenFallback(requestedNames: readonly string[], parsed: ParsedMountList): { args: string[] } {
287
+ const ghRequested = requestedNames.some((n) => n.trim() === "gh");
288
+ if (!ghRequested) return { args: [] };
289
+
290
+ // If an explicit GH_TOKEN is already in the environment, pass it.
291
+ if (process.env.GH_TOKEN) {
292
+ return { args: ["-e", `GH_TOKEN=${process.env.GH_TOKEN}`] };
293
+ }
294
+
295
+ // If the file mount resolved (hosts.yml exists), the token might
296
+ // be in the file. Check if it's keyring-based by looking for
297
+ // "oauth_token" in the hosts.yml — if missing, fall back.
298
+ const ghResolved = parsed.resolved.some((s) => s.name === "gh");
299
+ const ghMissing = parsed.missing.some((s) => s.name === "gh");
300
+
301
+ // gh dir doesn't exist at all → try extracting from keyring
302
+ // gh dir exists (mounted) → still try, since keyring auth leaves
303
+ // the file with no usable token
304
+ if (ghResolved || ghMissing || !ghResolved) {
305
+ try {
306
+ const token = execFileSync("gh", ["auth", "token"], {
307
+ encoding: "utf-8",
308
+ timeout: SUBPROCESS_PROBE_TIMEOUT_MS,
309
+ }).trim();
310
+ if (token.length > 0) {
311
+ log.info("sandbox", "gh token extracted from host keyring (GH_TOKEN fallback)");
312
+ return { args: ["-e", `GH_TOKEN=${token}`] };
313
+ }
314
+ } catch {
315
+ log.info("sandbox", "gh auth token failed — gh CLI may not work in sandbox");
316
+ }
317
+ }
318
+
319
+ return { args: [] };
320
+ }
321
+
322
+ // ── Utilities ──────────────────────────────────────────────────────
323
+
324
+ // Docker accepts POSIX-style paths even on Windows when using
325
+ // Docker Desktop, and the rest of the codebase already uses this
326
+ // helper in buildDockerSpawnArgs.
327
+ function toDockerPath(p: string): string {
328
+ return p.replace(/\\/g, "/");
329
+ }
@@ -0,0 +1,194 @@
1
+ import { EVENT_TYPES } from "../../src/types/events.js";
2
+
3
+ export type AgentEvent =
4
+ | { type: typeof EVENT_TYPES.status; message: string }
5
+ | { type: typeof EVENT_TYPES.text; message: string }
6
+ | { type: typeof EVENT_TYPES.toolResult; result: unknown }
7
+ | { type: typeof EVENT_TYPES.switchRole; roleId: string }
8
+ | { type: typeof EVENT_TYPES.error; message: string }
9
+ | {
10
+ type: typeof EVENT_TYPES.toolCall;
11
+ toolUseId: string;
12
+ toolName: string;
13
+ args: unknown;
14
+ }
15
+ | {
16
+ type: typeof EVENT_TYPES.toolCallResult;
17
+ toolUseId: string;
18
+ content: string;
19
+ }
20
+ | { type: typeof EVENT_TYPES.claudeSessionId; id: string };
21
+
22
+ export interface ClaudeContentBlock {
23
+ type: string;
24
+ id?: string;
25
+ name?: string;
26
+ input?: unknown;
27
+ tool_use_id?: string;
28
+ content?: unknown;
29
+ /** Text content — present in `text` type blocks. */
30
+ text?: string;
31
+ }
32
+
33
+ export interface ClaudeMessage {
34
+ content?: ClaudeContentBlock[];
35
+ }
36
+
37
+ export type ClaudeStreamEvent =
38
+ | { type: "assistant"; message: ClaudeMessage }
39
+ | { type: "user"; message: ClaudeMessage }
40
+ | { type: "result"; result: string; session_id?: string };
41
+
42
+ // stream_event sub-types emitted when --include-partial-messages is on.
43
+ export interface StreamEventDelta {
44
+ type: "content_block_delta";
45
+ index: number;
46
+ delta: { type: string; text?: string };
47
+ }
48
+
49
+ export interface RawStreamEvent {
50
+ type: string;
51
+ message?: ClaudeMessage;
52
+ result?: string;
53
+ session_id?: string;
54
+ /** Present when type === "stream_event". Carries partial text
55
+ * deltas for real-time streaming. */
56
+ event?: StreamEventDelta | { type: string };
57
+ }
58
+
59
+ export function blockToEvent(block: ClaudeContentBlock): AgentEvent | null {
60
+ if (block.type === "text" && typeof block.text === "string") {
61
+ return {
62
+ type: EVENT_TYPES.text,
63
+ message: block.text,
64
+ };
65
+ }
66
+ if (block.type === "tool_use" && block.id && block.name) {
67
+ return {
68
+ type: EVENT_TYPES.toolCall,
69
+ toolUseId: block.id,
70
+ toolName: block.name,
71
+ args: block.input,
72
+ };
73
+ }
74
+ if (block.type === "tool_result" && block.tool_use_id) {
75
+ const raw = block.content;
76
+ const content = typeof raw === "string" ? raw : raw === undefined ? "" : JSON.stringify(raw);
77
+ return {
78
+ type: EVENT_TYPES.toolCallResult,
79
+ toolUseId: block.tool_use_id,
80
+ content,
81
+ };
82
+ }
83
+ return null;
84
+ }
85
+
86
+ // Extract a text delta from a stream_event, or null if the event
87
+ // isn't a text delta. Keeps the main parse function under the
88
+ // cognitive-complexity cap.
89
+ function extractTextDelta(event: RawStreamEvent): string | null {
90
+ if (event.type !== "stream_event" || !event.event) return null;
91
+ const inner = event.event;
92
+ if (inner.type !== "content_block_delta" || !("delta" in inner) || inner.delta.type !== "text_delta" || typeof inner.delta.text !== "string") {
93
+ return null;
94
+ }
95
+ return inner.delta.text;
96
+ }
97
+
98
+ // Filter assistant block events: when deltas already streamed the
99
+ // text, remove text-type events to prevent duplication.
100
+ function filterAssistantBlocks(blockEvents: AgentEvent[], deltaStreamed: boolean): AgentEvent[] {
101
+ return deltaStreamed ? blockEvents.filter((e) => e.type !== EVENT_TYPES.text) : blockEvents;
102
+ }
103
+
104
+ // Stateful parser that deduplicates text across the three stages
105
+ // Claude CLI emits: stream_event deltas → assistant content blocks
106
+ // → result full text. Uses two flags:
107
+ //
108
+ // textStreamedFromDeltas — true once text_delta chunks have been
109
+ // emitted from stream_event. Controls whether the full-text
110
+ // `assistant` block is filtered as a duplicate of those chunks.
111
+ //
112
+ // textEmitted — true once ANY text (delta or assistant block) has
113
+ // been emitted, so the `result` event can suppress its duplicate
114
+ // full-text copy. Prevents text loss when `assistant` arrives
115
+ // without preceding `stream_event` deltas (short replies, CLI
116
+ // version without `--include-partial-messages`, etc.).
117
+ export function createStreamParser(): {
118
+ parse: (event: RawStreamEvent) => AgentEvent[];
119
+ } {
120
+ let textStreamedFromDeltas = false;
121
+ let textEmitted = false;
122
+
123
+ function parse(event: RawStreamEvent): AgentEvent[] {
124
+ // Handle streaming text deltas from --include-partial-messages.
125
+ const delta = extractTextDelta(event);
126
+ if (delta !== null) {
127
+ textStreamedFromDeltas = true;
128
+ textEmitted = true;
129
+ return [{ type: EVENT_TYPES.text, message: delta }];
130
+ }
131
+ if (event.type === "stream_event") return [];
132
+
133
+ if (event.type === "result") {
134
+ const events: AgentEvent[] = [];
135
+ if (!textEmitted && event.result) {
136
+ events.push({ type: EVENT_TYPES.text, message: event.result });
137
+ }
138
+ if (event.session_id) {
139
+ events.push({
140
+ type: EVENT_TYPES.claudeSessionId,
141
+ id: event.session_id,
142
+ });
143
+ }
144
+ textStreamedFromDeltas = false;
145
+ textEmitted = false;
146
+ return events;
147
+ }
148
+
149
+ if (event.type !== "assistant" && event.type !== "user") {
150
+ return [];
151
+ }
152
+
153
+ const content = event.message?.content;
154
+ const blockEvents = Array.isArray(content) ? content.map(blockToEvent).filter((e): e is AgentEvent => e !== null) : [];
155
+
156
+ if (event.type === "assistant") {
157
+ const filtered = filterAssistantBlocks(blockEvents, textStreamedFromDeltas);
158
+ if (filtered.some((e) => e.type === EVENT_TYPES.text)) {
159
+ textEmitted = true;
160
+ }
161
+ return [{ type: EVENT_TYPES.status, message: "Thinking..." }, ...filtered];
162
+ }
163
+ return blockEvents;
164
+ }
165
+
166
+ return { parse };
167
+ }
168
+
169
+ // Stateless convenience — used by tests and one-off parsing.
170
+ // For the agent loop, use createStreamParser() to get dedup.
171
+ export function parseStreamEvent(event: RawStreamEvent): AgentEvent[] {
172
+ if (event.type === "result" && event.result) {
173
+ const events: AgentEvent[] = [{ type: EVENT_TYPES.text, message: event.result }];
174
+ if (event.session_id) {
175
+ events.push({
176
+ type: EVENT_TYPES.claudeSessionId,
177
+ id: event.session_id,
178
+ });
179
+ }
180
+ return events;
181
+ }
182
+
183
+ if (event.type !== "assistant" && event.type !== "user") {
184
+ return [];
185
+ }
186
+
187
+ const content = event.message?.content;
188
+ const blockEvents = Array.isArray(content) ? content.map(blockToEvent).filter((e): e is AgentEvent => e !== null) : [];
189
+
190
+ if (event.type === "assistant") {
191
+ return [{ type: EVENT_TYPES.status, message: "Thinking..." }, ...blockEvents];
192
+ }
193
+ return blockEvents;
194
+ }
@@ -0,0 +1,61 @@
1
+ import { timingSafeEqual } from "crypto";
2
+
3
+ function safeEqual(a: string, b: string): boolean {
4
+ if (a.length !== b.length) return false;
5
+ return timingSafeEqual(Buffer.from(a), Buffer.from(b));
6
+ }
7
+
8
+ // Bearer token middleware (#272). Reject any `/api/*` request whose
9
+ // `Authorization: Bearer <token>` header doesn't match the current
10
+ // server token.
11
+ //
12
+ // This is the local-process isolation layer. `csrfGuard.ts` handles
13
+ // cross-origin browser attacks (layered on top, both must pass). This
14
+ // middleware handles the case a sibling process on the same machine
15
+ // (malicious program, another user, confused script) tries to hit
16
+ // `/api/*`: without the startup-regenerated token, every request is
17
+ // 401'd.
18
+ //
19
+ // Design choices:
20
+ // - **One exemption**: `/api/files/*` is bearer-exempt because `<img>`
21
+ // tags in rendered markdown (`presentDocument`, wiki) can't attach
22
+ // an `Authorization` header — the browser makes a plain GET. These
23
+ // endpoints are still CSRF-guarded (origin check) and the server
24
+ // binds to loopback only, so the exposure is localhost-scoped.
25
+ // The exemption is applied via a regex in `server/index.ts`.
26
+ // - **No token in logs**. Reject messages are generic ("unauthorized")
27
+ // so a leaked log line doesn't reveal whether "no header" vs
28
+ // "wrong token" — matches common auth-hardening guidance.
29
+ // - **Token comparison is `===`**. These are 64-char hex strings of
30
+ // identical length, so early-exit timing on length is moot. A
31
+ // length-mismatched header is already caught at the shape check,
32
+ // leaving only equal-length compares for real candidates.
33
+
34
+ import type { Request, Response, NextFunction } from "express";
35
+ import { getCurrentToken } from "./token.js";
36
+ import { unauthorized } from "../../utils/httpError.js";
37
+
38
+ const BEARER_PREFIX = "Bearer ";
39
+
40
+ export function bearerAuth(req: Request, res: Response, next: NextFunction): void {
41
+ const expected = getCurrentToken();
42
+ if (expected === null) {
43
+ // Server hasn't finished bootstrap. This can only happen if a
44
+ // request beats `generateAndWriteToken()` to completion — the
45
+ // server fixes that by generating before `app.listen`, but we
46
+ // still defend the middleware against out-of-order init.
47
+ unauthorized(res, "unauthorized");
48
+ return;
49
+ }
50
+ const header = req.headers.authorization;
51
+ if (typeof header !== "string" || !header.startsWith(BEARER_PREFIX)) {
52
+ unauthorized(res, "unauthorized");
53
+ return;
54
+ }
55
+ const provided = header.slice(BEARER_PREFIX.length);
56
+ if (!safeEqual(provided, expected)) {
57
+ unauthorized(res, "unauthorized");
58
+ return;
59
+ }
60
+ next();
61
+ }
@@ -0,0 +1,98 @@
1
+ // Bearer auth token (#272). One 32-byte hex token per server startup,
2
+ // held in memory and mirrored to a 0600 file at
3
+ // `WORKSPACE_PATHS.sessionToken`.
4
+ //
5
+ // **Why file-backed**: the token must travel out-of-process to (a) the
6
+ // Vite dev server's `transformIndexHtml` plugin so it can embed the
7
+ // token in the HTML Vue receives, and (b) CLI bridges (Phase 2) that
8
+ // share the workspace but live in a different process. Memory-only
9
+ // would force every reader to go through HTTP, which is the
10
+ // chicken-and-egg problem bearer auth is trying to fix.
11
+ //
12
+ // **Lifecycle**: generate on startup, write atomic, delete on graceful
13
+ // shutdown. A stale file after a crash is harmless — the next startup
14
+ // generates a fresh in-memory token and overwrites, so a stolen stale
15
+ // file value fails 401 against the running server.
16
+ //
17
+ // **Env override (#316)**: `MULMOCLAUDE_AUTH_TOKEN` (read via `env.ts`)
18
+ // pins the token across restarts so long-running bridges don't need a
19
+ // relaunch every time the server bounces. The client-side readers
20
+ // (`@mulmobridge/client` token.ts, Vite plugin) already honour the same var;
21
+ // setting it once on both sides survives restarts.
22
+
23
+ import { randomBytes } from "crypto";
24
+ import fs from "fs";
25
+ import { writeFileAtomic } from "../../utils/files/index.js";
26
+ import { log } from "../../system/logger/index.js";
27
+ import { isNonEmptyString } from "../../utils/types.js";
28
+ import { WORKSPACE_PATHS } from "../../workspace/paths.js";
29
+
30
+ const TOKEN_BYTES = 32; // 64 hex chars
31
+ // Below this length a random 32-byte token would be 64 hex chars;
32
+ // anything shorter from the env override is almost certainly a
33
+ // placeholder like "test" that leaked into production. Warn, don't
34
+ // block — the operator might have reasons we don't see.
35
+ const MIN_RECOMMENDED_CHARS = 32;
36
+
37
+ let currentToken: string | null = null;
38
+
39
+ /**
40
+ * The token the server is currently using. Null until
41
+ * `generateAndWriteToken` has been called. `bearerAuth` reads this on
42
+ * every request.
43
+ */
44
+ export function getCurrentToken(): string | null {
45
+ return currentToken;
46
+ }
47
+
48
+ /**
49
+ * Generate (or take from the env override) the startup token, store
50
+ * it in memory, and mirror it to the workspace file (mode 0600,
51
+ * atomic).
52
+ *
53
+ * @param tokenPath Injected for tests so they can target a tmp
54
+ * directory; production callers rely on the default
55
+ * `WORKSPACE_PATHS.sessionToken`.
56
+ * @param override Injected for tests. Production callers pass
57
+ * `env.authTokenOverride` from `server/env.ts`. When non-empty the
58
+ * override is used verbatim instead of generating random bytes.
59
+ */
60
+ export async function generateAndWriteToken(tokenPath: string = WORKSPACE_PATHS.sessionToken, override?: string): Promise<string> {
61
+ const token = resolveToken(override);
62
+ currentToken = token;
63
+ await writeFileAtomic(tokenPath, token, { mode: 0o600 });
64
+ return token;
65
+ }
66
+
67
+ function resolveToken(override: string | undefined): string {
68
+ if (isNonEmptyString(override)) {
69
+ if (override.length < MIN_RECOMMENDED_CHARS) {
70
+ // Visible on startup so a half-typed override doesn't silently
71
+ // become a security hole in dev.
72
+ log.warn("auth", "MULMOCLAUDE_AUTH_TOKEN is shorter than the recommended 32 characters", { length: override.length });
73
+ }
74
+ return override;
75
+ }
76
+ return randomBytes(TOKEN_BYTES).toString("hex");
77
+ }
78
+
79
+ /**
80
+ * Best-effort removal of the token file. Never throws; a missing file
81
+ * is a success for our purposes (nothing to clean up). Caller is
82
+ * responsible for not using the in-memory token after calling this.
83
+ */
84
+ export async function deleteTokenFile(tokenPath: string = WORKSPACE_PATHS.sessionToken): Promise<void> {
85
+ try {
86
+ await fs.promises.unlink(tokenPath);
87
+ } catch {
88
+ /* already gone — nothing to do */
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Test-only: reset module state so a suite can simulate fresh startup
94
+ * without reloading the module. Not exported to production callers.
95
+ */
96
+ export function __resetForTests(): void {
97
+ currentToken = null;
98
+ }