mulmoclaude 0.3.0 → 0.5.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 (312) hide show
  1. package/bin/mulmoclaude.js +7 -24
  2. package/client/assets/html2canvas-Cx501zZr-DiKaqnKs.js +5 -0
  3. package/client/assets/{index-eHWB79u5.js → index-C94GcmNa.js} +203 -198
  4. package/client/assets/index-CY-WpQUm.css +2 -0
  5. package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-5ipqh8Pe.js} +5 -5
  6. package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
  7. package/client/index.html +2 -4
  8. package/package.json +17 -15
  9. package/server/agent/attachmentConverter.ts +2 -2
  10. package/server/agent/backend/claude-code.ts +170 -0
  11. package/server/agent/backend/index.ts +14 -0
  12. package/server/agent/backend/types.ts +65 -0
  13. package/server/agent/index.ts +31 -159
  14. package/server/agent/mcp-server.ts +88 -10
  15. package/server/agent/mcp-tools/index.ts +8 -7
  16. package/server/agent/mcp-tools/notify.ts +76 -0
  17. package/server/agent/mcp-tools/x.ts +12 -2
  18. package/server/agent/plugin-names.ts +10 -4
  19. package/server/agent/prompt.ts +187 -26
  20. package/server/agent/resumeFailover.ts +5 -5
  21. package/server/agent/sandboxMounts.ts +3 -3
  22. package/server/api/auth/bearerAuth.ts +3 -3
  23. package/server/api/auth/token.ts +2 -2
  24. package/server/api/routes/agent.ts +99 -4
  25. package/server/api/routes/chart.ts +13 -0
  26. package/server/api/routes/chat-index.ts +2 -1
  27. package/server/api/routes/config.ts +35 -8
  28. package/server/api/routes/files.ts +75 -24
  29. package/server/api/routes/html.ts +15 -2
  30. package/server/api/routes/image.ts +75 -20
  31. package/server/api/routes/mulmo-script.ts +33 -31
  32. package/server/api/routes/news.ts +146 -0
  33. package/server/api/routes/notifications.ts +58 -2
  34. package/server/api/routes/pdf.ts +2 -2
  35. package/server/api/routes/plugins.ts +73 -91
  36. package/server/api/routes/presentHtml.ts +9 -0
  37. package/server/api/routes/roles.ts +12 -2
  38. package/server/api/routes/scheduler.ts +20 -11
  39. package/server/api/routes/schedulerTasks.ts +58 -21
  40. package/server/api/routes/sessions.ts +15 -4
  41. package/server/api/routes/sessionsCursor.ts +4 -4
  42. package/server/api/routes/skills.ts +26 -5
  43. package/server/api/routes/sources.ts +8 -7
  44. package/server/api/routes/todos.ts +30 -0
  45. package/server/api/routes/todosColumnsHandlers.ts +13 -27
  46. package/server/api/routes/todosHandlers.ts +1 -1
  47. package/server/api/routes/todosItemsHandlers.ts +14 -14
  48. package/server/api/routes/wiki/frontmatter.ts +86 -0
  49. package/server/api/routes/wiki.ts +335 -75
  50. package/server/api/sandboxStatus.ts +1 -1
  51. package/server/events/notifications.ts +32 -8
  52. package/server/events/pub-sub/index.ts +3 -3
  53. package/server/events/relay-client.ts +26 -16
  54. package/server/events/resolveRelayBridgeOptions.ts +125 -0
  55. package/server/index.ts +72 -49
  56. package/server/system/config.ts +5 -5
  57. package/server/system/credentials.ts +7 -5
  58. package/server/system/env.ts +15 -5
  59. package/server/system/macosNotify.ts +152 -0
  60. package/server/utils/errors.ts +11 -2
  61. package/server/utils/fetch.ts +54 -0
  62. package/server/utils/files/atomic.ts +18 -17
  63. package/server/utils/files/image-store.ts +19 -13
  64. package/server/utils/files/journal-io.ts +2 -2
  65. package/server/utils/files/json.ts +5 -5
  66. package/server/utils/files/markdown-image-fill.ts +131 -0
  67. package/server/utils/files/markdown-store.ts +22 -6
  68. package/server/utils/files/naming.ts +20 -10
  69. package/server/utils/files/reference-dirs-io.ts +3 -3
  70. package/server/utils/files/roles-io.ts +4 -4
  71. package/server/utils/files/safe.ts +14 -14
  72. package/server/utils/files/scheduler-overrides-io.ts +2 -2
  73. package/server/utils/files/spreadsheet-store.ts +15 -10
  74. package/server/utils/files/workspace-io.ts +12 -12
  75. package/server/utils/gemini.ts +30 -4
  76. package/server/utils/gitignore.ts +9 -9
  77. package/server/utils/id.ts +40 -8
  78. package/server/utils/json.ts +5 -5
  79. package/server/utils/logBackgroundError.ts +12 -3
  80. package/server/utils/logPreview.ts +24 -0
  81. package/server/utils/markdown.ts +5 -5
  82. package/server/utils/port.d.mts +6 -0
  83. package/server/utils/port.mjs +48 -0
  84. package/server/utils/promptMeta.ts +32 -0
  85. package/server/utils/request.ts +12 -6
  86. package/server/utils/slug.ts +65 -4
  87. package/server/utils/spawn.ts +1 -1
  88. package/server/utils/types.ts +2 -2
  89. package/server/workspace/chat-index/index.ts +1 -1
  90. package/server/workspace/chat-index/summarizer.ts +5 -5
  91. package/server/workspace/custom-dirs.ts +5 -5
  92. package/server/workspace/helps/gemini.md +57 -0
  93. package/server/workspace/helps/index.md +2 -1
  94. package/server/workspace/helps/sources.md +42 -0
  95. package/server/workspace/helps/wiki.md +40 -5
  96. package/server/workspace/journal/archivist-cli.ts +121 -0
  97. package/server/workspace/journal/{archivist.ts → archivist-schemas.ts} +12 -120
  98. package/server/workspace/journal/dailyPass.ts +78 -38
  99. package/server/workspace/journal/diff.ts +2 -2
  100. package/server/workspace/journal/index.ts +56 -5
  101. package/server/workspace/journal/memoryExtractor.ts +1 -1
  102. package/server/workspace/journal/optimizationPass.ts +4 -5
  103. package/server/workspace/journal/paths.ts +8 -24
  104. package/server/workspace/journal/state.ts +18 -8
  105. package/server/workspace/news/reader.ts +248 -0
  106. package/server/workspace/paths.ts +4 -3
  107. package/server/workspace/reference-dirs.ts +3 -3
  108. package/server/workspace/skills/parser.ts +6 -6
  109. package/server/workspace/skills/scheduler.ts +5 -4
  110. package/server/workspace/skills/user-tasks.ts +3 -2
  111. package/server/workspace/skills/writer.ts +3 -3
  112. package/server/workspace/sources/arxivDiscovery.ts +2 -2
  113. package/server/workspace/sources/classifier.ts +1 -1
  114. package/server/workspace/sources/fetchers/rss.ts +5 -5
  115. package/server/workspace/sources/fetchers/rssParser.ts +4 -4
  116. package/server/workspace/sources/interests.ts +3 -3
  117. package/server/workspace/sources/paths.ts +6 -6
  118. package/server/workspace/sources/pipeline/fetch.ts +59 -13
  119. package/server/workspace/sources/pipeline/index.ts +59 -7
  120. package/server/workspace/sources/pipeline/notify.ts +13 -5
  121. package/server/workspace/sources/pipeline/plan.ts +11 -9
  122. package/server/workspace/sources/pipeline/summarize.ts +1 -1
  123. package/server/workspace/sources/pipeline/write.ts +5 -5
  124. package/server/workspace/sources/rateLimiter.ts +1 -1
  125. package/server/workspace/sources/sourceState.ts +9 -4
  126. package/server/workspace/sources/types.ts +9 -0
  127. package/server/workspace/sources/urls.ts +1 -1
  128. package/server/workspace/tool-trace/classify.ts +4 -4
  129. package/server/workspace/workspace.ts +7 -7
  130. package/src/App.vue +477 -251
  131. package/src/components/CanvasViewToggle.vue +12 -10
  132. package/src/components/ChatInput.vue +112 -105
  133. package/src/components/FileContentHeader.vue +10 -7
  134. package/src/components/FileContentRenderer.vue +37 -10
  135. package/src/components/FileTree.vue +34 -4
  136. package/src/components/FileTreePane.vue +32 -27
  137. package/src/components/FilesView.vue +5 -3
  138. package/src/components/FilterChip.vue +22 -0
  139. package/src/components/LockStatusPopup.vue +19 -13
  140. package/src/components/NewsView.vue +252 -0
  141. package/src/components/NotificationBell.vue +35 -9
  142. package/src/components/NotificationToast.vue +4 -1
  143. package/src/components/PageChatComposer.vue +101 -0
  144. package/src/components/PluginLauncher.vue +36 -62
  145. package/src/components/RightSidebar.vue +13 -10
  146. package/src/components/RoleSelector.vue +3 -2
  147. package/src/components/SessionHeaderControls.vue +63 -0
  148. package/src/components/SessionHistoryExpandButton.vue +30 -0
  149. package/src/components/SessionHistoryPanel.vue +64 -93
  150. package/src/components/SessionHistoryToggleButton.vue +40 -0
  151. package/src/components/SessionRoleIcon.vue +72 -0
  152. package/src/components/SessionSidebar.vue +96 -0
  153. package/src/components/SessionTabBar.vue +44 -51
  154. package/src/components/SettingsMcpTab.vue +361 -52
  155. package/src/components/SettingsModal.vue +203 -72
  156. package/src/components/SettingsReferenceDirsTab.vue +72 -51
  157. package/src/components/SettingsWorkspaceDirsTab.vue +74 -51
  158. package/src/components/SidebarHeader.vue +50 -16
  159. package/src/components/SourcesManager.vue +900 -0
  160. package/src/components/SourcesView.vue +45 -0
  161. package/src/components/StackView.vue +84 -48
  162. package/src/components/SuggestionsPanel.vue +25 -36
  163. package/src/components/SystemFileBanner.vue +106 -0
  164. package/src/components/ThinkingIndicator.vue +41 -0
  165. package/src/components/TodoExplorer.vue +72 -22
  166. package/src/components/todo/TodoAddDialog.vue +17 -12
  167. package/src/components/todo/TodoEditDialog.vue +7 -2
  168. package/src/components/todo/TodoEditPanel.vue +15 -10
  169. package/src/components/todo/TodoKanbanView.vue +16 -6
  170. package/src/components/todo/TodoListView.vue +14 -3
  171. package/src/components/todo/TodoTableView.vue +36 -5
  172. package/src/composables/favicon/conditions.ts +76 -0
  173. package/src/composables/favicon/resolveColor.ts +93 -0
  174. package/src/composables/favicon/types.ts +61 -0
  175. package/src/composables/useAppApi.ts +23 -0
  176. package/src/composables/useChatScroll.ts +5 -5
  177. package/src/composables/useCurrentRole.ts +32 -0
  178. package/src/composables/useDynamicFavicon.ts +174 -58
  179. package/src/composables/useEventListeners.ts +7 -12
  180. package/src/composables/useFaviconState.ts +93 -12
  181. package/src/composables/useFileSelection.ts +25 -6
  182. package/src/composables/useHealth.ts +76 -7
  183. package/src/composables/useLayoutMode.ts +27 -0
  184. package/src/composables/useNewsItems.ts +38 -0
  185. package/src/composables/useNewsReadState.ts +75 -0
  186. package/src/composables/useNotifications.ts +76 -13
  187. package/src/composables/usePendingCalls.ts +11 -1
  188. package/src/composables/useRoles.ts +6 -10
  189. package/src/composables/useRunElapsed.ts +80 -0
  190. package/src/composables/useSessionDerived.ts +21 -5
  191. package/src/composables/useSessionHistory.ts +7 -17
  192. package/src/composables/useSidePanelVisible.ts +25 -0
  193. package/src/composables/useViewLayout.ts +16 -37
  194. package/src/config/apiRoutes.ts +19 -6
  195. package/src/config/historyFilters.ts +30 -0
  196. package/src/config/mcpCatalog.ts +285 -0
  197. package/src/config/mcpTypes.ts +26 -0
  198. package/src/config/roles.ts +19 -51
  199. package/src/config/systemFileDescriptors.ts +170 -0
  200. package/src/config/toolNames.ts +6 -1
  201. package/src/config/workspacePaths.ts +1 -0
  202. package/src/index.css +14 -0
  203. package/src/lang/de.ts +706 -0
  204. package/src/lang/en.ts +726 -0
  205. package/src/lang/es.ts +712 -0
  206. package/src/lang/fr.ts +704 -0
  207. package/src/lang/ja.ts +707 -0
  208. package/src/lang/ko.ts +709 -0
  209. package/src/lang/pt-BR.ts +702 -0
  210. package/src/lang/zh.ts +705 -0
  211. package/src/lib/vue-i18n.ts +97 -0
  212. package/src/main.ts +3 -0
  213. package/src/plugins/canvas/View.vue +104 -186
  214. package/src/plugins/canvas/definition.ts +0 -8
  215. package/src/plugins/canvas/index.ts +3 -2
  216. package/src/plugins/chart/Preview.vue +1 -1
  217. package/src/plugins/chart/View.vue +9 -4
  218. package/src/plugins/chart/index.ts +3 -2
  219. package/src/plugins/editImage/index.ts +3 -2
  220. package/src/plugins/generateImage/index.ts +3 -2
  221. package/src/plugins/manageRoles/Preview.vue +4 -1
  222. package/src/plugins/manageRoles/View.vue +67 -46
  223. package/src/plugins/manageRoles/index.ts +3 -2
  224. package/src/plugins/manageSkills/Preview.vue +8 -3
  225. package/src/plugins/manageSkills/View.vue +39 -34
  226. package/src/plugins/manageSkills/index.ts +3 -2
  227. package/src/plugins/manageSource/Preview.vue +1 -1
  228. package/src/plugins/manageSource/View.vue +3 -687
  229. package/src/plugins/manageSource/index.ts +3 -2
  230. package/src/plugins/markdown/Preview.vue +1 -1
  231. package/src/plugins/markdown/View.vue +164 -73
  232. package/src/plugins/markdown/definition.ts +6 -4
  233. package/src/plugins/markdown/index.ts +3 -2
  234. package/src/plugins/presentForm/Preview.vue +99 -0
  235. package/src/plugins/presentForm/View.vue +675 -0
  236. package/src/plugins/presentForm/definition.ts +127 -0
  237. package/src/plugins/presentForm/index.ts +18 -0
  238. package/src/plugins/presentForm/plugin.ts +94 -0
  239. package/src/plugins/presentForm/types.ts +109 -0
  240. package/src/plugins/presentHtml/Preview.vue +1 -1
  241. package/src/plugins/presentHtml/View.vue +7 -4
  242. package/src/plugins/presentHtml/index.ts +3 -2
  243. package/src/plugins/presentMulmoScript/Preview.vue +1 -1
  244. package/src/plugins/presentMulmoScript/View.vue +36 -26
  245. package/src/plugins/presentMulmoScript/index.ts +3 -2
  246. package/src/plugins/scheduler/AutomationsPreview.vue +37 -0
  247. package/src/plugins/scheduler/AutomationsView.vue +23 -0
  248. package/src/plugins/scheduler/CalendarView.vue +23 -0
  249. package/src/plugins/scheduler/LegacySchedulerView.vue +32 -0
  250. package/src/plugins/scheduler/Preview.vue +7 -4
  251. package/src/plugins/scheduler/TasksTab.vue +119 -28
  252. package/src/plugins/scheduler/View.vue +75 -32
  253. package/src/plugins/scheduler/automationsDefinition.ts +58 -0
  254. package/src/plugins/scheduler/calendarDefinition.ts +46 -0
  255. package/src/plugins/scheduler/formatSchedule.ts +93 -0
  256. package/src/plugins/scheduler/index.ts +68 -14
  257. package/src/plugins/scheduler/legacyShape.ts +34 -0
  258. package/src/plugins/spreadsheet/Preview.vue +9 -5
  259. package/src/plugins/spreadsheet/View.vue +43 -57
  260. package/src/plugins/spreadsheet/engine/responseDecoder.ts +2 -1
  261. package/src/plugins/spreadsheet/index.ts +3 -2
  262. package/src/plugins/textResponse/Preview.vue +15 -58
  263. package/src/plugins/textResponse/View.vue +42 -45
  264. package/src/plugins/textResponse/utils.ts +25 -0
  265. package/src/plugins/todo/Preview.vue +11 -6
  266. package/src/plugins/todo/View.vue +27 -13
  267. package/src/plugins/todo/composables/useTodos.ts +3 -1
  268. package/src/plugins/todo/index.ts +3 -2
  269. package/src/plugins/ui-image/ImagePreview.vue +6 -3
  270. package/src/plugins/ui-image/ImageView.vue +7 -4
  271. package/src/plugins/wiki/Preview.vue +5 -2
  272. package/src/plugins/wiki/View.vue +539 -92
  273. package/src/plugins/wiki/index.ts +5 -2
  274. package/src/plugins/wiki/route.ts +121 -0
  275. package/src/router/guards.ts +43 -24
  276. package/src/router/index.ts +53 -26
  277. package/src/router/pageRoutes.ts +23 -0
  278. package/src/tools/index.ts +12 -5
  279. package/src/tools/legacyPluginNames.ts +13 -0
  280. package/src/types/notification.ts +31 -6
  281. package/src/types/vue-i18n.d.ts +20 -0
  282. package/src/utils/agent/eventDispatch.ts +3 -6
  283. package/src/utils/agent/formatElapsed.ts +37 -0
  284. package/src/utils/agent/request.ts +22 -1
  285. package/src/utils/canvas/layoutMode.ts +26 -0
  286. package/src/utils/canvas/sidePanelVisible.ts +19 -0
  287. package/src/utils/dom/scrollIntoViewByTestId.ts +38 -0
  288. package/src/utils/errors.ts +9 -2
  289. package/src/utils/files/filename.ts +24 -0
  290. package/src/utils/filesPreview/schedulerPreview.ts +9 -3
  291. package/src/utils/id.ts +18 -0
  292. package/src/utils/image/cacheBust.ts +16 -0
  293. package/src/utils/image/resolve.ts +16 -0
  294. package/src/utils/markdown/taskList.ts +175 -0
  295. package/src/utils/mcp/interpolateSpec.ts +97 -0
  296. package/src/utils/notification/dispatch.ts +51 -15
  297. package/src/utils/path/workspaceLinkRouter.ts +99 -0
  298. package/src/utils/session/mergeSessions.ts +5 -0
  299. package/src/utils/sources/filter.ts +69 -0
  300. package/src/vite-env.d.ts +9 -0
  301. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
  302. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
  303. package/client/assets/index-Bm70FDU2.css +0 -1
  304. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
  305. package/server/workspace/journal/linkRewrite.ts +0 -4
  306. package/src/components/ToolResultsPanel.vue +0 -77
  307. package/src/composables/useCanvasViewMode.ts +0 -121
  308. package/src/plugins/scheduler/definition.ts +0 -57
  309. package/src/utils/canvas/viewMode.ts +0 -46
  310. package/src/utils/role/plugins.ts +0 -12
  311. package/src/utils/session/seedRoleDefault.ts +0 -35
  312. /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
package/client/index.html CHANGED
@@ -17,10 +17,8 @@
17
17
  <title>MulmoClaude</title>
18
18
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='30' height='30' x='1' y='1' rx='6' fill='%236B7280'/><text x='16' y='17' text-anchor='middle' dominant-baseline='central' font-family='sans-serif' font-weight='bold' font-size='20' fill='white'>M</text></svg>" />
19
19
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
20
- <script type="module" crossorigin src="/assets/index-eHWB79u5.js"></script>
21
- <link rel="modulepreload" crossorigin href="/assets/chunk-vKJrgz-R-C_I3GbVV.js">
22
- <link rel="modulepreload" crossorigin href="/assets/typeof-DBp4T-Ny-BC0P-2DM.js">
23
- <link rel="stylesheet" crossorigin href="/assets/index-Bm70FDU2.css">
20
+ <script type="module" crossorigin src="/assets/index-C94GcmNa.js"></script>
21
+ <link rel="stylesheet" crossorigin href="/assets/index-CY-WpQUm.css">
24
22
  </head>
25
23
  <body>
26
24
  <div id="app"></div>
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "mulmoclaude",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "MulmoClaude — GUI-chat with Claude Code + long-term memory. One command to start.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mulmoclaude": "bin/mulmoclaude.js"
8
8
  },
9
9
  "scripts": {
10
- "prepublishOnly": "node bin/prepare-dist.js",
10
+ "prepack": "node bin/prepare-dist.js",
11
11
  "typecheck": "echo \"mulmoclaude: no typecheck (launcher is plain JS; server and src are dist artifacts)\"",
12
12
  "test": "echo no tests yet"
13
13
  },
@@ -18,20 +18,19 @@
18
18
  "src/"
19
19
  ],
20
20
  "dependencies": {
21
- "@mulmobridge/chat-service": "^0.1.1",
22
- "@mulmobridge/client": "^0.1.1",
23
- "@mulmobridge/protocol": "^0.1.3",
24
- "@receptron/task-scheduler": "^0.1.0",
25
21
  "@google/genai": "^1.50.1",
26
- "@mulmocast/types": "^2.6.7",
27
- "@gui-chat-plugin/mindmap": "^0.4.0",
28
- "@gui-chat-plugin/present3d": "^0.1.0",
29
22
  "@gui-chat-plugin/browse": "^0.2.0",
30
23
  "@gui-chat-plugin/camera": "^0.4.0",
24
+ "@gui-chat-plugin/mindmap": "^0.4.0",
25
+ "@gui-chat-plugin/present3d": "^0.1.0",
31
26
  "@gui-chat-plugin/weather": "^0.1.0",
32
- "@mulmochat-plugin/form": "0.5.0",
27
+ "@mulmobridge/chat-service": "^0.1.2",
28
+ "@mulmobridge/client": "^0.1.4",
29
+ "@mulmobridge/protocol": "^0.1.4",
30
+ "@mulmocast/types": "^2.6.8",
33
31
  "@mulmochat-plugin/quiz": "0.4.0",
34
32
  "@mulmochat-plugin/ui-image": "^0.3.0",
33
+ "@receptron/task-scheduler": "^0.1.0",
35
34
  "cors": "^2.8.6",
36
35
  "dotenv": "^17.4.2",
37
36
  "express": "^5.2.1",
@@ -40,15 +39,18 @@
40
39
  "ignore": "^7.0.5",
41
40
  "mammoth": "^1.12.0",
42
41
  "marked": "^18.0.2",
43
- "mulmocast": "^2.6.7",
44
- "puppeteer": "^24.41.0",
42
+ "mulmocast": "^2.6.8",
43
+ "puppeteer": "^24.42.0",
45
44
  "socket.io": "^4.8.3",
46
45
  "socket.io-client": "^4.8.3",
47
- "uuid": "^13.0.0",
46
+ "tsx": "^4.19.0",
47
+ "uuid": "^14.0.0",
48
48
  "ws": "^8.20.0",
49
49
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
50
- "zod": "^4.3.6",
51
- "tsx": "^4.19.0"
50
+ "zod": "^4.3.6"
51
+ },
52
+ "optionalDependencies": {
53
+ "node-pty": "^1.1.0"
52
54
  },
53
55
  "engines": {
54
56
  "node": ">=20"
@@ -17,7 +17,7 @@ import * as XLSX from "xlsx";
17
17
  import { execFile } from "child_process";
18
18
  import { mkdtemp, readFile, writeFile, rm } from "fs/promises";
19
19
  import path from "path";
20
- import os from "os";
20
+ import { tmpdir } from "os";
21
21
  import { promisify } from "util";
22
22
 
23
23
  const execFileAsync = promisify(execFile);
@@ -116,7 +116,7 @@ async function tryDockerLibreOffice(): Promise<boolean> {
116
116
  }
117
117
 
118
118
  async function convertPptxToPdf(data: string): Promise<Buffer | null> {
119
- const tmpDir = await mkdtemp(path.join(os.tmpdir(), "pptx-"));
119
+ const tmpDir = await mkdtemp(path.join(tmpdir(), "pptx-"));
120
120
  const inputPath = path.join(tmpDir, "input.pptx");
121
121
  const outputPath = path.join(tmpDir, "input.pdf");
122
122
 
@@ -0,0 +1,170 @@
1
+ // Claude Code backend: spawns the `claude` CLI as a subprocess (or
2
+ // inside the mulmoclaude-sandbox Docker image) and translates its
3
+ // stream-json output into portable AgentEvents.
4
+ //
5
+ // This file is the single seam between the orchestrator in
6
+ // server/agent/index.ts (which is backend-agnostic) and the Claude
7
+ // CLI specifics. Pure helpers it depends on (CLI arg construction,
8
+ // Docker arg construction, stream parsing) stay in their existing
9
+ // home so the existing test suite under test/agent/ keeps working
10
+ // unchanged.
11
+
12
+ import { spawn, type ChildProcessByStdio } from "child_process";
13
+ import type { Readable, Writable } from "stream";
14
+ import { buildCliArgs, buildDockerSpawnArgs, buildUserMessageLine } from "../config.js";
15
+ import { resolveSandboxAuth } from "../sandboxMounts.js";
16
+ import { getCachedReferenceDirs, referenceDirMountArgs } from "../../workspace/reference-dirs.js";
17
+ import { createStreamParser, type AgentEvent, type RawStreamEvent } from "../stream.js";
18
+ import { log } from "../../system/logger/index.js";
19
+ import { EVENT_TYPES } from "../../../src/types/events.js";
20
+ import { env } from "../../system/env.js";
21
+ import type { AgentInput, LLMBackend } from "./types.js";
22
+
23
+ type ClaudeProc = ChildProcessByStdio<Writable, Readable, Readable>;
24
+
25
+ function spawnClaude(useDocker: boolean, workspacePath: string, cliArgs: string[]): ClaudeProc {
26
+ if (!useDocker) {
27
+ return spawn("claude", cliArgs, {
28
+ cwd: workspacePath,
29
+ stdio: ["pipe", "pipe", "pipe"],
30
+ });
31
+ }
32
+ const sandboxAuth = resolveSandboxAuth({
33
+ sshAgentForward: env.sandboxSshAgentForward,
34
+ sshAllowedHosts: env.sandboxSshAllowedHosts,
35
+ configMountNames: env.sandboxMountConfigs,
36
+ sshAuthSock: process.env.SSH_AUTH_SOCK,
37
+ });
38
+ const refDirArgs = referenceDirMountArgs(getCachedReferenceDirs());
39
+ const dockerArgs = buildDockerSpawnArgs({
40
+ workspacePath,
41
+ cliArgs,
42
+ uid: process.getuid?.() ?? 1000,
43
+ gid: process.getgid?.() ?? 1000,
44
+ platform: process.platform,
45
+ sandboxAuthArgs: [...sandboxAuth.args, ...refDirArgs],
46
+ sshAgentForward: env.sandboxSshAgentForward,
47
+ });
48
+ return spawn("docker", dockerArgs, { stdio: ["pipe", "pipe", "pipe"] });
49
+ }
50
+
51
+ // Track MCP tool usage to detect silent MCP server failures.
52
+ // If ToolSearch was called but no mcp__* tool was ever invoked,
53
+ // the MCP server likely crashed on startup (e.g. module resolution
54
+ // failure inside Docker). See #430.
55
+ function createMcpTracker() {
56
+ let toolSearchCalled = false;
57
+ let mcpToolCalled = false;
58
+ return {
59
+ track(event: AgentEvent) {
60
+ if (event.type !== EVENT_TYPES.toolCall) return;
61
+ if (event.toolName === "ToolSearch") toolSearchCalled = true;
62
+ if (event.toolName.startsWith("mcp__")) mcpToolCalled = true;
63
+ },
64
+ logIfSuspicious() {
65
+ if (toolSearchCalled && !mcpToolCalled) {
66
+ log.warn(
67
+ "agent",
68
+ "ToolSearch was used but no MCP tool was called — the MCP server may have crashed. " +
69
+ "Check Docker volume mounts and package.json exports. " +
70
+ "Run: npx tsx --test test/agent/test_mcp_docker_smoke.ts",
71
+ );
72
+ }
73
+ },
74
+ };
75
+ }
76
+
77
+ async function* readAgentEvents(proc: ClaudeProc): AsyncGenerator<AgentEvent> {
78
+ let stderrOutput = "";
79
+ let stderrBuffer = "";
80
+ proc.stderr.on("data", (chunk: Buffer) => {
81
+ const text = chunk.toString();
82
+ stderrOutput += text;
83
+ stderrBuffer += text;
84
+ const lines = stderrBuffer.split("\n");
85
+ stderrBuffer = lines.pop() ?? "";
86
+ for (const line of lines) {
87
+ if (line.trim()) log.error("agent-stderr", line);
88
+ }
89
+ });
90
+
91
+ // Stateful parser tracks whether text was already streamed via
92
+ // assistant content blocks so the final `result` event's duplicate
93
+ // text is suppressed. See createStreamParser() in stream.ts.
94
+ const parser = createStreamParser();
95
+
96
+ const mcpTracker = createMcpTracker();
97
+
98
+ let buffer = "";
99
+ for await (const chunk of proc.stdout) {
100
+ buffer += (chunk as Buffer).toString();
101
+ const lines = buffer.split("\n");
102
+ buffer = lines.pop() ?? "";
103
+
104
+ for (const line of lines) {
105
+ if (!line.trim()) continue;
106
+ let event: RawStreamEvent;
107
+ try {
108
+ event = JSON.parse(line);
109
+ } catch {
110
+ continue;
111
+ }
112
+ for (const agentEvent of parser.parse(event)) {
113
+ mcpTracker.track(agentEvent);
114
+ yield agentEvent;
115
+ }
116
+ }
117
+ }
118
+
119
+ const exitCode = await new Promise<number>((resolve) => proc.on("close", resolve));
120
+
121
+ if (stderrBuffer.trim()) log.error("agent-stderr", stderrBuffer);
122
+ log.info("agent", "claude exited", { exitCode });
123
+ mcpTracker.logIfSuspicious();
124
+
125
+ if (exitCode !== 0) {
126
+ yield {
127
+ type: EVENT_TYPES.error,
128
+ message: stderrOutput || `claude exited with code ${exitCode}`,
129
+ };
130
+ }
131
+ }
132
+
133
+ async function* runClaudeAgent(input: AgentInput): AsyncGenerator<AgentEvent> {
134
+ const cliArgs = buildCliArgs({
135
+ systemPrompt: input.systemPrompt,
136
+ activePlugins: input.activePlugins,
137
+ claudeSessionId: input.sessionToken,
138
+ mcpConfigPath: input.mcpConfigPath,
139
+ extraAllowedTools: input.extraAllowedTools,
140
+ });
141
+
142
+ const proc = spawnClaude(input.useDocker, input.workspacePath, cliArgs);
143
+
144
+ // stream-json input mode: stream the user turn as a single JSON
145
+ // line to stdin, then close the pipe so the CLI knows no further
146
+ // turns are coming. Writing before attaching the abort handler is
147
+ // fine — if the write fails because the process already died for
148
+ // other reasons, the readAgentEvents loop below surfaces it.
149
+ const messageLine = await buildUserMessageLine(input.message, input.attachments);
150
+ proc.stdin.write(messageLine);
151
+ proc.stdin.end();
152
+
153
+ const onAbort = () => {
154
+ if (!proc.killed) proc.kill();
155
+ };
156
+ input.abortSignal?.addEventListener("abort", onAbort, { once: true });
157
+
158
+ try {
159
+ yield* readAgentEvents(proc);
160
+ } finally {
161
+ input.abortSignal?.removeEventListener("abort", onAbort);
162
+ if (!proc.killed) proc.kill();
163
+ }
164
+ }
165
+
166
+ export const claudeCodeBackend: LLMBackend = {
167
+ id: "claude-code",
168
+ capabilities: { sessionResume: true, mcp: true },
169
+ runAgent: runClaudeAgent,
170
+ };
@@ -0,0 +1,14 @@
1
+ // Backend factory. Today there is only ClaudeCodeBackend; future
2
+ // backends (OpenAI, Ollama native, Gemini) are selected here based on
3
+ // env / settings. Callers go through getActiveBackend() rather than
4
+ // importing a concrete adapter so adding a backend doesn't require
5
+ // touching every call site.
6
+
7
+ import { claudeCodeBackend } from "./claude-code.js";
8
+ import type { LLMBackend } from "./types.js";
9
+
10
+ export type { AgentInput, BackendCapabilities, LLMBackend } from "./types.js";
11
+
12
+ export function getActiveBackend(): LLMBackend {
13
+ return claudeCodeBackend;
14
+ }
@@ -0,0 +1,65 @@
1
+ // LLM backend abstraction. Today the only implementation is
2
+ // ClaudeCodeBackend (server/agent/backend/claude-code.ts), which spawns
3
+ // the `claude` CLI as a subprocess. The interface exists so future
4
+ // backends (OpenAI, Ollama native, Gemini, etc.) can plug in here
5
+ // without the orchestrator in server/agent/index.ts knowing which one
6
+ // is active.
7
+ //
8
+ // See plans/refactor-llm-backend-abstraction.md for the broader plan.
9
+
10
+ import type { Attachment } from "@mulmobridge/protocol";
11
+ import type { Role } from "../../../src/config/roles.js";
12
+ import type { AgentEvent } from "../stream.js";
13
+
14
+ /** Inputs the orchestrator passes to a backend for one user turn.
15
+ * The orchestrator owns role expansion, system prompt building, and
16
+ * MCP config writing. The backend owns the LLM call itself plus
17
+ * translation of provider-specific stream events into AgentEvent. */
18
+ export interface AgentInput {
19
+ systemPrompt: string;
20
+ message: string;
21
+ role: Role;
22
+ workspacePath: string;
23
+ sessionId: string;
24
+ port: number;
25
+ /** Opaque, backend-specific resume token. For Claude this is the
26
+ * CLI's session id passed to --resume; other backends may
27
+ * interpret it differently or ignore it entirely
28
+ * (capabilities.sessionResume === false). */
29
+ sessionToken?: string;
30
+ attachments?: Attachment[];
31
+ /** Active MCP plugin names (the subset of role.availablePlugins
32
+ * that is actually registered as an MCP plugin). The orchestrator
33
+ * has already filtered these — backends should not re-derive. */
34
+ activePlugins: string[];
35
+ /** When set, the path the backend should hand to its MCP loader.
36
+ * Pre-resolved for host-vs-container by the orchestrator. */
37
+ mcpConfigPath?: string;
38
+ /** Extra allowed-tool names from settings + user MCP servers. */
39
+ extraAllowedTools: string[];
40
+ /** When fired, the backend must terminate any in-flight
41
+ * subprocess / connection. */
42
+ abortSignal?: AbortSignal;
43
+ userTimezone?: string;
44
+ /** Whether the orchestrator detected a usable Docker sandbox.
45
+ * Backends that don't sandbox can ignore. */
46
+ useDocker: boolean;
47
+ }
48
+
49
+ export interface BackendCapabilities {
50
+ /** Can the backend resume a prior conversation by an opaque token?
51
+ * Claude: yes (--resume <id>). OpenAI / Ollama: no — the
52
+ * orchestrator must replay transcript instead. */
53
+ sessionResume: boolean;
54
+ /** Does the backend speak MCP natively? Claude: yes. Others:
55
+ * emulate or skip. Today only Claude consumes activePlugins /
56
+ * mcpConfigPath. */
57
+ mcp: boolean;
58
+ }
59
+
60
+ export interface LLMBackend {
61
+ readonly id: string;
62
+ readonly capabilities: BackendCapabilities;
63
+ /** Run one user turn. Yields portable AgentEvents. */
64
+ runAgent(input: AgentInput): AsyncIterable<AgentEvent>;
65
+ }
@@ -1,141 +1,17 @@
1
- import { spawn, type ChildProcessByStdio } from "child_process";
2
- import { mkdir, writeFile, unlink } from "fs/promises";
1
+ import { mkdir, unlink } from "fs/promises";
2
+ import { writeJsonAtomic } from "../utils/files/json.js";
3
3
  import { dirname } from "path";
4
- import type { Readable, Writable } from "stream";
5
4
  import { isDockerAvailable } from "../system/docker.js";
6
5
  import { refreshCredentials } from "../system/credentials.js";
7
6
  import { loadMcpConfig, loadSettings } from "../system/config.js";
8
7
  import type { Role } from "../../src/config/roles.js";
9
8
  import { loadAllRoles } from "../workspace/roles.js";
10
9
  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";
10
+ import { CONTAINER_WORKSPACE_PATH, buildMcpConfig, getActivePlugins, prepareUserServers, resolveMcpConfigPaths, userServerAllowedToolNames } from "./config.js";
22
11
  import type { Attachment } from "@mulmobridge/protocol";
23
- import { createStreamParser, type AgentEvent, type RawStreamEvent } from "./stream.js";
12
+ import type { AgentEvent } from "./stream.js";
24
13
  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
- }
14
+ import { getActiveBackend } from "./backend/index.js";
139
15
 
140
16
  export interface RunAgentOptions {
141
17
  message: string;
@@ -157,6 +33,7 @@ export async function* runAgent(
157
33
  claudeSessionId?: string,
158
34
  abortSignal?: AbortSignal,
159
35
  attachments?: Attachment[],
36
+ userTimezone?: string,
160
37
  ): AsyncGenerator<AgentEvent> {
161
38
  const activePlugins = getActivePlugins(role);
162
39
  const useDocker = await isDockerAvailable();
@@ -180,6 +57,7 @@ export async function* runAgent(
180
57
  role,
181
58
  workspacePath: useDocker ? CONTAINER_WORKSPACE_PATH : workspacePath,
182
59
  useDocker,
60
+ userTimezone,
183
61
  });
184
62
 
185
63
  // In debug mode (--debug), dump the full system prompt on the first
@@ -202,11 +80,14 @@ export async function* runAgent(
202
80
  chatSessionId: sessionId,
203
81
  port,
204
82
  activePlugins,
205
- roleIds: loadAllRoles().map((r) => r.id),
83
+ roleIds: loadAllRoles().map((loadedRole) => loadedRole.id),
206
84
  useDocker,
207
85
  userServers,
208
86
  });
209
- await writeFile(mcpPaths.hostPath, JSON.stringify(mcpConfig, null, 2));
87
+ // Write atomically so a partially-written file can't be picked
88
+ // up by a concurrent claude spawn (they share the --mcp-config
89
+ // path under the session dir).
90
+ await writeJsonAtomic(mcpPaths.hostPath, mcpConfig);
210
91
  }
211
92
 
212
93
  // Fresh read on every invocation so the Settings UI can change
@@ -214,47 +95,38 @@ export async function* runAgent(
214
95
  const settings = loadSettings();
215
96
  const userServerAllowedTools = userServerAllowedToolNames(userServers, useDocker);
216
97
 
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
98
  // Don't persist raw sessionId into log sinks (esp. the retained
226
99
  // file sink). A boolean presence flag is enough for operational
227
100
  // debugging and avoids writing identifiers that route back to a
228
101
  // specific session into long-lived log files.
229
- log.info("agent", "spawning claude", {
102
+ const backend = getActiveBackend();
103
+ log.info("agent", "spawning agent", {
104
+ backend: backend.id,
230
105
  roleId: role.id,
231
106
  useDocker,
232
107
  hasMcp,
233
108
  resumed: Boolean(claudeSessionId),
234
109
  hasSessionId: Boolean(sessionId),
235
110
  });
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
111
 
253
112
  try {
254
- yield* readAgentEvents(proc);
113
+ yield* backend.runAgent({
114
+ systemPrompt: fullSystemPrompt,
115
+ message,
116
+ role,
117
+ workspacePath,
118
+ sessionId,
119
+ port,
120
+ sessionToken: claudeSessionId,
121
+ attachments,
122
+ activePlugins,
123
+ mcpConfigPath: hasMcp ? mcpPaths.argPath : undefined,
124
+ extraAllowedTools: [...settings.extraAllowedTools, ...userServerAllowedTools],
125
+ abortSignal,
126
+ userTimezone,
127
+ useDocker,
128
+ });
255
129
  } finally {
256
- abortSignal?.removeEventListener("abort", onAbort);
257
- if (!proc.killed) proc.kill();
258
130
  if (hasMcp) unlink(mcpPaths.hostPath).catch(() => {});
259
131
  }
260
132
  }