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,414 @@
1
+ import { join } from "path";
2
+ import { homedir, tmpdir } from "os";
3
+ import type { Role } from "../../src/config/roles.js";
4
+ import { mcpTools, isMcpToolEnabled } from "./mcp-tools/index.js";
5
+ import { MCP_PLUGIN_NAMES } from "./plugin-names.js";
6
+ import type { McpServerSpec } from "../system/config.js";
7
+ import { getCurrentToken } from "../api/auth/token.js";
8
+ import type { Attachment } from "@mulmobridge/protocol";
9
+ import { isImageMime, isNativeAttachmentMime } from "@mulmobridge/client";
10
+ import { convertAttachment } from "./attachmentConverter.js";
11
+ import { log } from "../system/logger/index.js";
12
+
13
+ export const CONTAINER_WORKSPACE_PATH = "/home/node/mulmoclaude";
14
+
15
+ const BASE_ALLOWED_TOOLS = ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "WebFetch", "WebSearch"];
16
+
17
+ const MCP_PLUGINS = new Set([...MCP_PLUGIN_NAMES, ...mcpTools.filter(isMcpToolEnabled).map((t) => t.definition.name)]);
18
+
19
+ export function getActivePlugins(role: Role): string[] {
20
+ return role.availablePlugins.filter((p) => MCP_PLUGINS.has(p));
21
+ }
22
+
23
+ export interface McpConfigParams {
24
+ /** Stable chat session ID (not the per-run UUID). Used as SESSION_ID
25
+ * env var so the MCP server's /internal/* callbacks address the
26
+ * session store by chatSessionId. */
27
+ chatSessionId: string;
28
+ port: number;
29
+ activePlugins: string[];
30
+ roleIds: string[];
31
+ useDocker?: boolean;
32
+ // User-defined MCP servers from <workspace>/config/mcp.json.
33
+ // Keys become the server id in the generated --mcp-config file;
34
+ // values are the standard Claude CLI server spec (HTTP or stdio).
35
+ userServers?: Record<string, McpServerSpec>;
36
+ }
37
+
38
+ // In Docker mode the sandbox container can't reach the host's
39
+ // `localhost` / `127.0.0.1` — those refer to the container's own
40
+ // loopback interface. Rewriting to `host.docker.internal` keeps
41
+ // user-configured local MCP servers reachable.
42
+ export function rewriteLocalhostForDocker(url: string, useDocker: boolean): string {
43
+ if (!useDocker) return url;
44
+ return url.replace(/^(https?:\/\/)(localhost|127\.0\.0\.1)(?=[:/]|$)/, "$1host.docker.internal");
45
+ }
46
+
47
+ function prepareUserHttpServer(spec: Extract<McpServerSpec, { type: "http" }>, useDocker: boolean): McpServerSpec {
48
+ return {
49
+ ...spec,
50
+ url: rewriteLocalhostForDocker(spec.url, useDocker),
51
+ };
52
+ }
53
+
54
+ // Rewrite stdio args so paths that point inside the host workspace are
55
+ // translated to their container equivalents. Paths outside the
56
+ // workspace are left alone — the caller surfaces a warning in the UI
57
+ // before they get this far.
58
+ function prepareUserStdioServer(spec: Extract<McpServerSpec, { type: "stdio" }>, useDocker: boolean, hostWorkspacePath: string): McpServerSpec {
59
+ if (!useDocker) return spec;
60
+ const normalisedWs = hostWorkspacePath.endsWith("/") ? hostWorkspacePath : `${hostWorkspacePath}/`;
61
+ const args = spec.args?.map((arg) => {
62
+ if (arg === hostWorkspacePath) return CONTAINER_WORKSPACE_PATH;
63
+ if (arg.startsWith(normalisedWs)) {
64
+ const rel = arg.slice(normalisedWs.length);
65
+ return `${CONTAINER_WORKSPACE_PATH}/${rel}`;
66
+ }
67
+ return arg;
68
+ });
69
+ return { ...spec, args };
70
+ }
71
+
72
+ export function prepareUserServers(userServers: Record<string, McpServerSpec>, useDocker: boolean, hostWorkspacePath: string): Record<string, McpServerSpec> {
73
+ const out: Record<string, McpServerSpec> = {};
74
+ for (const [id, spec] of Object.entries(userServers)) {
75
+ if (spec.enabled === false) continue;
76
+ if (spec.type === "http") {
77
+ out[id] = prepareUserHttpServer(spec, useDocker);
78
+ } else {
79
+ out[id] = prepareUserStdioServer(spec, useDocker, hostWorkspacePath);
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+
85
+ // When running in Docker the MCP server subprocess won't inherit the host
86
+ // environment. Pass sentinel values for required env vars of enabled tools
87
+ // so isMcpToolEnabled() returns the same result inside the container.
88
+ // The actual API calls happen on the host server, so real values aren't needed.
89
+ function collectMcpToolSentinelEnv(): Record<string, string> {
90
+ const env: Record<string, string> = {};
91
+ for (const tool of mcpTools.filter(isMcpToolEnabled)) {
92
+ for (const key of tool.requiredEnv ?? []) {
93
+ if (process.env[key]) env[key] = "1";
94
+ }
95
+ }
96
+ return env;
97
+ }
98
+
99
+ function buildMulmoclaudeServer(params: { chatSessionId: string; port: number; activePlugins: string[]; roleIds: string[]; useDocker: boolean }): object {
100
+ const { chatSessionId, port, activePlugins, roleIds, useDocker } = params;
101
+ const projectRoot = process.cwd();
102
+ const command = useDocker ? "tsx" : join(projectRoot, "node_modules/.bin/tsx");
103
+ const mcpServerPath = useDocker ? "/app/server/agent/mcp-server.ts" : join(projectRoot, "server/agent/mcp-server.ts");
104
+
105
+ const dockerEnv = useDocker
106
+ ? {
107
+ MCP_HOST: "host.docker.internal",
108
+ NODE_PATH: "/app/node_modules",
109
+ ...collectMcpToolSentinelEnv(),
110
+ }
111
+ : {};
112
+
113
+ // Bearer token for MCP subprocess to call /api/* back to this server
114
+ // (#272). The MCP bridge also has a file-read fallback from
115
+ // <workspace>/.session-token, but env is faster and works in Docker
116
+ // where the token file may not be bind-mounted.
117
+ const token = getCurrentToken();
118
+ const authEnv = token ? { MULMOCLAUDE_AUTH_TOKEN: token } : {};
119
+
120
+ return {
121
+ command,
122
+ args: [mcpServerPath],
123
+ env: {
124
+ SESSION_ID: chatSessionId,
125
+ PORT: String(port),
126
+ PLUGIN_NAMES: activePlugins.join(","),
127
+ ROLE_IDS: roleIds.join(","),
128
+ ...authEnv,
129
+ ...dockerEnv,
130
+ },
131
+ };
132
+ }
133
+
134
+ // Never let a user-defined server shadow the built-in internal bridge —
135
+ // even if they pick "mulmoclaude" as the id. Drop the entry silently:
136
+ // the UI already validates ids against the slug pattern, so this is
137
+ // defence-in-depth.
138
+ function excludeReservedKeys(servers: Record<string, McpServerSpec>): Record<string, McpServerSpec> {
139
+ const out: Record<string, McpServerSpec> = {};
140
+ for (const [id, spec] of Object.entries(servers)) {
141
+ if (id === "mulmoclaude") continue;
142
+ out[id] = spec;
143
+ }
144
+ return out;
145
+ }
146
+
147
+ export function buildMcpConfig(params: McpConfigParams): object {
148
+ const { chatSessionId, port, activePlugins, roleIds, useDocker = false, userServers = {} } = params;
149
+ return {
150
+ mcpServers: {
151
+ mulmoclaude: buildMulmoclaudeServer({
152
+ chatSessionId,
153
+ port,
154
+ activePlugins,
155
+ roleIds,
156
+ useDocker,
157
+ }),
158
+ ...excludeReservedKeys(userServers),
159
+ },
160
+ };
161
+ }
162
+
163
+ // User-facing `mcp__<server>` wildcard form for --allowedTools. Enabled
164
+ // HTTP servers always participate; stdio servers only participate when
165
+ // we're running natively (since the sandbox image is minimal in Docker).
166
+ export function userServerAllowedToolNames(userServers: Record<string, McpServerSpec>, useDocker: boolean): string[] {
167
+ const names: string[] = [];
168
+ for (const [id, spec] of Object.entries(userServers)) {
169
+ if (spec.enabled === false) continue;
170
+ // Stdio servers are dropped under Docker because the sandbox
171
+ // image is too minimal to run most of them (see #162).
172
+ if (spec.type === "stdio" && useDocker) continue;
173
+ names.push(`mcp__${id}`);
174
+ }
175
+ return names;
176
+ }
177
+
178
+ export interface CliArgsParams {
179
+ systemPrompt: string;
180
+ activePlugins: string[];
181
+ claudeSessionId?: string;
182
+ mcpConfigPath?: string;
183
+ // Web UI-managed extension of the allowed-tools list. Merged with
184
+ // BASE_ALLOWED_TOOLS and the mcp__mulmoclaude__ plugin names.
185
+ extraAllowedTools?: string[];
186
+ }
187
+
188
+ export function buildCliArgs(params: CliArgsParams): string[] {
189
+ const { systemPrompt, activePlugins, claudeSessionId, mcpConfigPath, extraAllowedTools = [] } = params;
190
+
191
+ const mcpToolNames = activePlugins.map((p) => `mcp__mulmoclaude__${p}`);
192
+ const allowedTools = [...BASE_ALLOWED_TOOLS, ...extraAllowedTools, ...mcpToolNames];
193
+
194
+ // stream-json input mode: the user message is streamed through
195
+ // stdin (see `writeUserMessage` in server/agent.ts) rather than
196
+ // passed as a `-p <text>` argument. This path is required so that
197
+ // Claude resolves slash-command invocations (e.g. `/shiritori` from
198
+ // the manageSkills Run button) against `~/.claude/skills/`. In the
199
+ // old `-p <text>` mode the CLI treats the message as literal text
200
+ // and "/shiritori" never reaches the skill resolver.
201
+ const args = [
202
+ "--output-format",
203
+ "stream-json",
204
+ "--input-format",
205
+ "stream-json",
206
+ "--include-partial-messages",
207
+ "--verbose",
208
+ "--system-prompt",
209
+ systemPrompt,
210
+ "--allowedTools",
211
+ allowedTools.join(","),
212
+ "-p",
213
+ ];
214
+
215
+ if (claudeSessionId) {
216
+ args.push("--resume", claudeSessionId);
217
+ }
218
+
219
+ if (mcpConfigPath) {
220
+ args.push("--mcp-config", mcpConfigPath);
221
+ }
222
+
223
+ return args;
224
+ }
225
+
226
+ /** JSON line to write to the Claude CLI's stdin when running in
227
+ * stream-json input mode. One line per user turn.
228
+ *
229
+ * Supported attachment types:
230
+ * - `image/*` → vision content blocks (`type: "image"`)
231
+ * - `application/pdf` → document content blocks (`type: "document"`)
232
+ * - `text/*`, JSON, XML, YAML, CSV → decoded UTF-8 → text block
233
+ * - DOCX → mammoth text extraction → text block
234
+ * - XLSX → xlsx CSV extraction → text block
235
+ * - PPTX → libreoffice PDF conversion → document block (Docker only)
236
+ * - Other MIME types → skipped with a console hint.
237
+ *
238
+ * Without attachments, content is a plain string (smaller,
239
+ * backward-compatible). */
240
+ export async function buildUserMessageLine(message: string, attachments?: Attachment[]): Promise<string> {
241
+ const all = attachments ?? [];
242
+ if (all.length === 0) {
243
+ return (
244
+ JSON.stringify({
245
+ type: "user",
246
+ message: { role: "user", content: message },
247
+ }) + "\n"
248
+ );
249
+ }
250
+
251
+ const blocks: Array<Record<string, unknown>> = [];
252
+ const skippedReasons: string[] = [];
253
+
254
+ for (const att of all) {
255
+ // Native types: image and PDF go directly as content blocks
256
+ if (isNativeAttachmentMime(att.mimeType)) {
257
+ blocks.push(buildNativeBlock(att));
258
+ continue;
259
+ }
260
+ // Convertible types: text, docx, xlsx, pptx
261
+ const result = await convertAttachment(att);
262
+ if (result.kind === "converted") {
263
+ blocks.push(...result.blocks);
264
+ } else {
265
+ skippedReasons.push(result.reason);
266
+ }
267
+ }
268
+
269
+ if (skippedReasons.length > 0) {
270
+ log.warn("agent", "skipping unsupported attachment(s)", {
271
+ count: skippedReasons.length,
272
+ reasons: skippedReasons,
273
+ });
274
+ }
275
+
276
+ blocks.push({ type: "text", text: message });
277
+ return (
278
+ JSON.stringify({
279
+ type: "user",
280
+ message: { role: "user", content: blocks },
281
+ }) + "\n"
282
+ );
283
+ }
284
+
285
+ function buildNativeBlock(att: Attachment): Record<string, unknown> {
286
+ const blockType = isImageMime(att.mimeType) ? "image" : "document";
287
+ return {
288
+ type: blockType,
289
+ source: {
290
+ type: "base64",
291
+ media_type: att.mimeType,
292
+ data: att.data,
293
+ },
294
+ };
295
+ }
296
+
297
+ export interface McpConfigPaths {
298
+ // Where the file is actually written on the host filesystem.
299
+ hostPath: string;
300
+ // What gets passed to claude --mcp-config (container path under
301
+ // docker, identical to hostPath when running natively).
302
+ argPath: string;
303
+ }
304
+
305
+ export function resolveMcpConfigPaths(opts: { workspacePath: string; sessionId: string; useDocker: boolean }): McpConfigPaths {
306
+ if (opts.useDocker) {
307
+ const hostPath = join(opts.workspacePath, ".mulmoclaude", `mcp-${opts.sessionId}.json`);
308
+ const argPath = `${CONTAINER_WORKSPACE_PATH}/.mulmoclaude/mcp-${opts.sessionId}.json`;
309
+ return { hostPath, argPath };
310
+ }
311
+ const hostPath = join(tmpdir(), `mulmoclaude-mcp-${opts.sessionId}.json`);
312
+ return { hostPath, argPath: hostPath };
313
+ }
314
+
315
+ // Mirror NodeJS.Platform — re-declared so the file doesn't need a
316
+ // `NodeJS` global reference, which the no-undef rule doesn't see in
317
+ // type-only positions.
318
+ export type Platform = "aix" | "android" | "darwin" | "freebsd" | "haiku" | "linux" | "openbsd" | "sunos" | "win32" | "cygwin" | "netbsd";
319
+
320
+ export interface DockerSpawnArgsParams {
321
+ workspacePath: string;
322
+ cliArgs: string[];
323
+ uid: number;
324
+ gid: number;
325
+ platform: Platform;
326
+ projectRoot?: string;
327
+ homeDir?: string;
328
+ /** Extra `-v` / `-e` tokens for opt-in host credentials (#259).
329
+ * Built by `resolveSandboxAuth` in `sandboxMounts.ts`. Default []. */
330
+ sandboxAuthArgs?: readonly string[];
331
+ /** Whether SSH agent forwarding is active. When true, the container
332
+ * uses the entrypoint (root → setup → setpriv drop) instead of
333
+ * `--user`, and adds the minimum capabilities the entrypoint needs.
334
+ * When false (default), `--user uid:gid --cap-drop ALL` with zero
335
+ * capabilities — identical to the pre-#259 security posture. */
336
+ sshAgentForward?: boolean;
337
+ }
338
+
339
+ // Pure helper that returns the full `docker run ... claude <args>`
340
+ // argv array. Extracted from runAgent so the long flag list can be
341
+ // inspected and tested without spawning a real subprocess.
342
+ export function buildDockerSpawnArgs(params: DockerSpawnArgsParams): string[] {
343
+ const {
344
+ workspacePath,
345
+ cliArgs,
346
+ uid,
347
+ gid,
348
+ platform,
349
+ projectRoot = process.cwd(),
350
+ homeDir = homedir(),
351
+ sandboxAuthArgs = [],
352
+ sshAgentForward = false,
353
+ } = params;
354
+ const toDockerPath = (p: string): string => p.replace(/\\/g, "/");
355
+ const extraHosts: string[] = platform === "linux" ? ["--add-host", "host.docker.internal:host-gateway"] : [];
356
+
357
+ return [
358
+ "run",
359
+ "--rm",
360
+ // -i keeps the container's stdin open so the stream-json user
361
+ // message (see buildUserMessageLine) can flow through. Without
362
+ // this Docker detaches stdin and the CLI reads EOF on startup.
363
+ "-i",
364
+ "--cap-drop",
365
+ "ALL",
366
+ // When SSH agent forwarding is active, the entrypoint needs root
367
+ // to fix /etc/passwd, chown /home/node, and chmod the socket.
368
+ // These 5 caps are the minimum set; setpriv --inh-caps=-all
369
+ // drops them on exec so Claude runs with zero capabilities.
370
+ //
371
+ // When SSH is OFF, use the simpler `--user uid:gid` which runs
372
+ // the entire container as the host user — zero caps from the
373
+ // start, identical to the pre-#259 security posture.
374
+ ...(sshAgentForward
375
+ ? [
376
+ "--cap-add",
377
+ "CHOWN",
378
+ "--cap-add",
379
+ "FOWNER",
380
+ "--cap-add",
381
+ "DAC_OVERRIDE",
382
+ "--cap-add",
383
+ "SETUID",
384
+ "--cap-add",
385
+ "SETGID",
386
+ "-e",
387
+ `HOST_UID=${uid}`,
388
+ "-e",
389
+ `HOST_GID=${gid}`,
390
+ ]
391
+ : ["--user", `${uid}:${gid}`]),
392
+ "-e",
393
+ "HOME=/home/node",
394
+ "-v",
395
+ `${toDockerPath(projectRoot)}/node_modules:/app/node_modules:ro`,
396
+ "-v",
397
+ `${toDockerPath(projectRoot)}/server:/app/server:ro`,
398
+ "-v",
399
+ `${toDockerPath(projectRoot)}/src:/app/src:ro`,
400
+ "-v",
401
+ `${toDockerPath(projectRoot)}/packages:/app/packages:ro`,
402
+ "-v",
403
+ `${toDockerPath(workspacePath)}:${CONTAINER_WORKSPACE_PATH}`,
404
+ "-v",
405
+ `${toDockerPath(homeDir)}/.claude:/home/node/.claude`,
406
+ "-v",
407
+ `${toDockerPath(homeDir)}/.claude.json:/home/node/.claude.json`,
408
+ ...sandboxAuthArgs,
409
+ ...extraHosts,
410
+ "mulmoclaude-sandbox",
411
+ "claude",
412
+ ...cliArgs,
413
+ ];
414
+ }
@@ -0,0 +1,260 @@
1
+ import { spawn, type ChildProcessByStdio } from "child_process";
2
+ import { mkdir, writeFile, unlink } from "fs/promises";
3
+ import { dirname } from "path";
4
+ import type { Readable, Writable } from "stream";
5
+ import { isDockerAvailable } from "../system/docker.js";
6
+ import { refreshCredentials } from "../system/credentials.js";
7
+ import { loadMcpConfig, loadSettings } from "../system/config.js";
8
+ import type { Role } from "../../src/config/roles.js";
9
+ import { loadAllRoles } from "../workspace/roles.js";
10
+ import { buildSystemPrompt } from "./prompt.js";
11
+ import {
12
+ CONTAINER_WORKSPACE_PATH,
13
+ buildCliArgs,
14
+ buildDockerSpawnArgs,
15
+ buildMcpConfig,
16
+ buildUserMessageLine,
17
+ getActivePlugins,
18
+ prepareUserServers,
19
+ resolveMcpConfigPaths,
20
+ userServerAllowedToolNames,
21
+ } from "./config.js";
22
+ import type { Attachment } from "@mulmobridge/protocol";
23
+ import { createStreamParser, type AgentEvent, type RawStreamEvent } from "./stream.js";
24
+ import { log } from "../system/logger/index.js";
25
+ import { EVENT_TYPES } from "../../src/types/events.js";
26
+ import { env } from "../system/env.js";
27
+ import { resolveSandboxAuth } from "./sandboxMounts.js";
28
+ import { getCachedReferenceDirs, referenceDirMountArgs } from "../workspace/reference-dirs.js";
29
+
30
+ type ClaudeProc = ChildProcessByStdio<Writable, Readable, Readable>;
31
+
32
+ function spawnClaude(useDocker: boolean, workspacePath: string, cliArgs: string[]): ClaudeProc {
33
+ if (!useDocker) {
34
+ return spawn("claude", cliArgs, {
35
+ cwd: workspacePath,
36
+ stdio: ["pipe", "pipe", "pipe"],
37
+ });
38
+ }
39
+ const sandboxAuth = resolveSandboxAuth({
40
+ sshAgentForward: env.sandboxSshAgentForward,
41
+ sshAllowedHosts: env.sandboxSshAllowedHosts,
42
+ configMountNames: env.sandboxMountConfigs,
43
+ sshAuthSock: process.env.SSH_AUTH_SOCK,
44
+ });
45
+ const refDirArgs = referenceDirMountArgs(getCachedReferenceDirs());
46
+ const dockerArgs = buildDockerSpawnArgs({
47
+ workspacePath,
48
+ cliArgs,
49
+ uid: process.getuid?.() ?? 1000,
50
+ gid: process.getgid?.() ?? 1000,
51
+ platform: process.platform,
52
+ sandboxAuthArgs: [...sandboxAuth.args, ...refDirArgs],
53
+ sshAgentForward: env.sandboxSshAgentForward,
54
+ });
55
+ return spawn("docker", dockerArgs, { stdio: ["pipe", "pipe", "pipe"] });
56
+ }
57
+
58
+ // Track MCP tool usage to detect silent MCP server failures.
59
+ // If ToolSearch was called but no mcp__* tool was ever invoked,
60
+ // the MCP server likely crashed on startup (e.g. module resolution
61
+ // failure inside Docker). See #430.
62
+ function createMcpTracker() {
63
+ let toolSearchCalled = false;
64
+ let mcpToolCalled = false;
65
+ return {
66
+ track(event: AgentEvent) {
67
+ if (event.type !== EVENT_TYPES.toolCall) return;
68
+ if (event.toolName === "ToolSearch") toolSearchCalled = true;
69
+ if (event.toolName.startsWith("mcp__")) mcpToolCalled = true;
70
+ },
71
+ logIfSuspicious() {
72
+ if (toolSearchCalled && !mcpToolCalled) {
73
+ log.warn(
74
+ "agent",
75
+ "ToolSearch was used but no MCP tool was called — the MCP server may have crashed. " +
76
+ "Check Docker volume mounts and package.json exports. " +
77
+ "Run: npx tsx --test test/agent/test_mcp_docker_smoke.ts",
78
+ );
79
+ }
80
+ },
81
+ };
82
+ }
83
+
84
+ async function* readAgentEvents(proc: ClaudeProc): AsyncGenerator<AgentEvent> {
85
+ let stderrOutput = "";
86
+ let stderrBuffer = "";
87
+ proc.stderr.on("data", (chunk: Buffer) => {
88
+ const text = chunk.toString();
89
+ stderrOutput += text;
90
+ stderrBuffer += text;
91
+ const lines = stderrBuffer.split("\n");
92
+ stderrBuffer = lines.pop() ?? "";
93
+ for (const line of lines) {
94
+ if (line.trim()) log.error("agent-stderr", line);
95
+ }
96
+ });
97
+
98
+ // Stateful parser tracks whether text was already streamed via
99
+ // assistant content blocks so the final `result` event's duplicate
100
+ // text is suppressed. See createStreamParser() in stream.ts.
101
+ const parser = createStreamParser();
102
+
103
+ const mcpTracker = createMcpTracker();
104
+
105
+ let buffer = "";
106
+ for await (const chunk of proc.stdout) {
107
+ buffer += (chunk as Buffer).toString();
108
+ const lines = buffer.split("\n");
109
+ buffer = lines.pop() ?? "";
110
+
111
+ for (const line of lines) {
112
+ if (!line.trim()) continue;
113
+ let event: RawStreamEvent;
114
+ try {
115
+ event = JSON.parse(line);
116
+ } catch {
117
+ continue;
118
+ }
119
+ for (const agentEvent of parser.parse(event)) {
120
+ mcpTracker.track(agentEvent);
121
+ yield agentEvent;
122
+ }
123
+ }
124
+ }
125
+
126
+ const exitCode = await new Promise<number>((resolve) => proc.on("close", resolve));
127
+
128
+ if (stderrBuffer.trim()) log.error("agent-stderr", stderrBuffer);
129
+ log.info("agent", "claude exited", { exitCode });
130
+ mcpTracker.logIfSuspicious();
131
+
132
+ if (exitCode !== 0) {
133
+ yield {
134
+ type: EVENT_TYPES.error,
135
+ message: stderrOutput || `claude exited with code ${exitCode}`,
136
+ };
137
+ }
138
+ }
139
+
140
+ export interface RunAgentOptions {
141
+ message: string;
142
+ role: Role;
143
+ workspacePath: string;
144
+ sessionId: string;
145
+ port: number;
146
+ claudeSessionId?: string;
147
+ /** When aborted, the spawned Claude CLI process is killed. */
148
+ abortSignal?: AbortSignal;
149
+ }
150
+
151
+ export async function* runAgent(
152
+ message: string,
153
+ role: Role,
154
+ workspacePath: string,
155
+ sessionId: string,
156
+ port: number,
157
+ claudeSessionId?: string,
158
+ abortSignal?: AbortSignal,
159
+ attachments?: Attachment[],
160
+ ): AsyncGenerator<AgentEvent> {
161
+ const activePlugins = getActivePlugins(role);
162
+ const useDocker = await isDockerAvailable();
163
+
164
+ // User-defined MCP servers are read per invocation so Settings UI
165
+ // changes apply immediately. Disabled / malformed entries get
166
+ // filtered by prepareUserServers; remaining servers are merged into
167
+ // the --mcp-config payload below.
168
+ const userMcpRaw = loadMcpConfig().mcpServers;
169
+ const userServers = prepareUserServers(userMcpRaw, useDocker, workspacePath);
170
+ const hasUserServers = Object.keys(userServers).length > 0;
171
+ const hasMcp = activePlugins.length > 0 || hasUserServers;
172
+
173
+ // On macOS sandbox, always refresh credentials from Keychain before each
174
+ // agent run so that expired OAuth tokens are replaced transparently.
175
+ if (useDocker && process.platform === "darwin") {
176
+ await refreshCredentials();
177
+ }
178
+
179
+ const fullSystemPrompt = buildSystemPrompt({
180
+ role,
181
+ workspacePath: useDocker ? CONTAINER_WORKSPACE_PATH : workspacePath,
182
+ useDocker,
183
+ });
184
+
185
+ // In debug mode (--debug), dump the full system prompt on the first
186
+ // message of each session so developers can inspect what the LLM sees.
187
+ if (!claudeSessionId && process.argv.includes("--debug")) {
188
+ log.info("agent", "system prompt for new session:\n" + fullSystemPrompt);
189
+ }
190
+
191
+ const mcpPaths = resolveMcpConfigPaths({
192
+ workspacePath,
193
+ sessionId,
194
+ useDocker,
195
+ });
196
+ if (useDocker) {
197
+ await mkdir(dirname(mcpPaths.hostPath), { recursive: true });
198
+ }
199
+
200
+ if (hasMcp) {
201
+ const mcpConfig = buildMcpConfig({
202
+ chatSessionId: sessionId,
203
+ port,
204
+ activePlugins,
205
+ roleIds: loadAllRoles().map((r) => r.id),
206
+ useDocker,
207
+ userServers,
208
+ });
209
+ await writeFile(mcpPaths.hostPath, JSON.stringify(mcpConfig, null, 2));
210
+ }
211
+
212
+ // Fresh read on every invocation so the Settings UI can change
213
+ // allowedTools / MCP servers without a server restart.
214
+ const settings = loadSettings();
215
+ const userServerAllowedTools = userServerAllowedToolNames(userServers, useDocker);
216
+
217
+ const cliArgs = buildCliArgs({
218
+ systemPrompt: fullSystemPrompt,
219
+ activePlugins,
220
+ claudeSessionId,
221
+ mcpConfigPath: hasMcp ? mcpPaths.argPath : undefined,
222
+ extraAllowedTools: [...settings.extraAllowedTools, ...userServerAllowedTools],
223
+ });
224
+
225
+ // Don't persist raw sessionId into log sinks (esp. the retained
226
+ // file sink). A boolean presence flag is enough for operational
227
+ // debugging and avoids writing identifiers that route back to a
228
+ // specific session into long-lived log files.
229
+ log.info("agent", "spawning claude", {
230
+ roleId: role.id,
231
+ useDocker,
232
+ hasMcp,
233
+ resumed: Boolean(claudeSessionId),
234
+ hasSessionId: Boolean(sessionId),
235
+ });
236
+ const proc = spawnClaude(useDocker, workspacePath, cliArgs);
237
+
238
+ // stream-json input mode: stream the user turn as a single JSON
239
+ // line to stdin, then close the pipe so the CLI knows no further
240
+ // turns are coming. Writing before attaching the abort handler
241
+ // is fine — if the write fails because the process already died
242
+ // for other reasons, the `readAgentEvents` loop below surfaces it.
243
+ const messageLine = await buildUserMessageLine(message, attachments);
244
+ proc.stdin.write(messageLine);
245
+ proc.stdin.end();
246
+
247
+ // If an abort signal is provided, kill the process when it fires.
248
+ const onAbort = () => {
249
+ if (!proc.killed) proc.kill();
250
+ };
251
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
252
+
253
+ try {
254
+ yield* readAgentEvents(proc);
255
+ } finally {
256
+ abortSignal?.removeEventListener("abort", onAbort);
257
+ if (!proc.killed) proc.kill();
258
+ if (hasMcp) unlink(mcpPaths.hostPath).catch(() => {});
259
+ }
260
+ }