mulmoclaude 0.1.2 → 0.4.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 (251) hide show
  1. package/bin/mulmoclaude.js +7 -24
  2. package/client/assets/html2canvas-Cx501zZr-Cv5snK9D.js +5 -0
  3. package/client/assets/index-CubzmCVK.css +2 -0
  4. package/client/assets/{index-D8rhwXLq.js → index-DtcyExH9.js} +80 -61
  5. package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-DnizuhIY.js} +5 -5
  6. package/client/index.html +2 -4
  7. package/package.json +13 -13
  8. package/server/agent/attachmentConverter.ts +2 -2
  9. package/server/agent/config.ts +12 -12
  10. package/server/agent/index.ts +9 -3
  11. package/server/agent/mcp-server.ts +19 -19
  12. package/server/agent/mcp-tools/index.ts +6 -6
  13. package/server/agent/mcp-tools/x.ts +7 -6
  14. package/server/agent/prompt.ts +195 -29
  15. package/server/agent/resumeFailover.ts +5 -5
  16. package/server/agent/sandboxMounts.ts +10 -10
  17. package/server/agent/stream.ts +4 -4
  18. package/server/api/auth/bearerAuth.ts +3 -3
  19. package/server/api/auth/token.ts +2 -2
  20. package/server/api/routes/agent.ts +21 -3
  21. package/server/api/routes/config.ts +1 -1
  22. package/server/api/routes/files.ts +22 -21
  23. package/server/api/routes/html.ts +2 -2
  24. package/server/api/routes/image.ts +7 -7
  25. package/server/api/routes/mulmo-script.ts +33 -31
  26. package/server/api/routes/pdf.ts +2 -2
  27. package/server/api/routes/plugins.ts +16 -6
  28. package/server/api/routes/roles.ts +2 -2
  29. package/server/api/routes/scheduler.ts +14 -12
  30. package/server/api/routes/schedulerHandlers.ts +12 -12
  31. package/server/api/routes/schedulerTasks.ts +19 -17
  32. package/server/api/routes/sessions.ts +26 -26
  33. package/server/api/routes/sessionsCursor.ts +4 -4
  34. package/server/api/routes/skills.ts +5 -5
  35. package/server/api/routes/sources.ts +3 -3
  36. package/server/api/routes/todosColumnsHandlers.ts +30 -30
  37. package/server/api/routes/todosHandlers.ts +1 -1
  38. package/server/api/routes/todosItemsHandlers.ts +14 -14
  39. package/server/api/routes/wiki.ts +36 -22
  40. package/server/api/sandboxStatus.ts +1 -1
  41. package/server/events/notifications.ts +6 -6
  42. package/server/events/pub-sub/index.ts +3 -3
  43. package/server/events/relay-client.ts +17 -16
  44. package/server/events/scheduler-adapter.ts +20 -20
  45. package/server/events/session-store/index.ts +10 -10
  46. package/server/events/task-manager/index.ts +7 -7
  47. package/server/index.ts +59 -65
  48. package/server/system/config.ts +5 -5
  49. package/server/system/credentials.ts +7 -5
  50. package/server/system/env.ts +5 -5
  51. package/server/utils/date.ts +18 -18
  52. package/server/utils/files/atomic.ts +16 -16
  53. package/server/utils/files/html-io.ts +5 -5
  54. package/server/utils/files/image-store.ts +19 -8
  55. package/server/utils/files/journal-io.ts +4 -4
  56. package/server/utils/files/json.ts +5 -5
  57. package/server/utils/files/markdown-store.ts +4 -4
  58. package/server/utils/files/naming.ts +2 -2
  59. package/server/utils/files/reference-dirs-io.ts +3 -3
  60. package/server/utils/files/roles-io.ts +12 -12
  61. package/server/utils/files/safe.ts +14 -14
  62. package/server/utils/files/scheduler-io.ts +5 -5
  63. package/server/utils/files/scheduler-overrides-io.ts +2 -2
  64. package/server/utils/files/session-io.ts +35 -35
  65. package/server/utils/files/spreadsheet-store.ts +7 -7
  66. package/server/utils/files/todos-io.ts +9 -9
  67. package/server/utils/files/user-tasks-io.ts +5 -5
  68. package/server/utils/files/workspace-io.ts +12 -12
  69. package/server/utils/gemini.ts +2 -2
  70. package/server/utils/gitignore.ts +9 -9
  71. package/server/utils/json.ts +5 -5
  72. package/server/utils/logBackgroundError.ts +12 -3
  73. package/server/utils/markdown.ts +5 -5
  74. package/server/utils/port.d.mts +6 -0
  75. package/server/utils/port.mjs +48 -0
  76. package/server/utils/request.ts +12 -6
  77. package/server/utils/spawn.ts +1 -1
  78. package/server/utils/types.ts +2 -2
  79. package/server/workspace/chat-index/indexer.ts +15 -15
  80. package/server/workspace/chat-index/summarizer.ts +4 -4
  81. package/server/workspace/custom-dirs.ts +16 -16
  82. package/server/workspace/journal/archivist.ts +35 -35
  83. package/server/workspace/journal/dailyPass.ts +31 -28
  84. package/server/workspace/journal/diff.ts +2 -2
  85. package/server/workspace/journal/index.ts +4 -4
  86. package/server/workspace/journal/indexFile.ts +29 -25
  87. package/server/workspace/journal/optimizationPass.ts +2 -2
  88. package/server/workspace/journal/state.ts +6 -6
  89. package/server/workspace/paths.ts +3 -3
  90. package/server/workspace/reference-dirs.ts +20 -20
  91. package/server/workspace/roles.ts +6 -6
  92. package/server/workspace/skills/discovery.ts +4 -4
  93. package/server/workspace/skills/parser.ts +6 -6
  94. package/server/workspace/skills/scheduler.ts +3 -3
  95. package/server/workspace/skills/user-tasks.ts +34 -34
  96. package/server/workspace/skills/writer.ts +3 -3
  97. package/server/workspace/sources/arxivDiscovery.ts +10 -10
  98. package/server/workspace/sources/classifier.ts +7 -7
  99. package/server/workspace/sources/fetchers/arxiv.ts +7 -7
  100. package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
  101. package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
  102. package/server/workspace/sources/fetchers/rss.ts +5 -5
  103. package/server/workspace/sources/fetchers/rssParser.ts +4 -4
  104. package/server/workspace/sources/interests.ts +12 -12
  105. package/server/workspace/sources/paths.ts +6 -6
  106. package/server/workspace/sources/pipeline/fetch.ts +36 -13
  107. package/server/workspace/sources/pipeline/index.ts +8 -13
  108. package/server/workspace/sources/pipeline/notify.ts +3 -3
  109. package/server/workspace/sources/pipeline/plan.ts +15 -13
  110. package/server/workspace/sources/pipeline/write.ts +5 -5
  111. package/server/workspace/sources/rateLimiter.ts +1 -1
  112. package/server/workspace/sources/registry.ts +16 -16
  113. package/server/workspace/sources/robots.ts +14 -14
  114. package/server/workspace/sources/sourceState.ts +17 -10
  115. package/server/workspace/sources/types.ts +9 -0
  116. package/server/workspace/sources/urls.ts +1 -1
  117. package/server/workspace/tool-trace/classify.ts +4 -4
  118. package/server/workspace/tool-trace/index.ts +1 -1
  119. package/server/workspace/tool-trace/writeSearch.ts +26 -16
  120. package/server/workspace/wiki-backlinks/index.ts +8 -8
  121. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
  122. package/server/workspace/workspace.ts +7 -7
  123. package/src/App.vue +315 -141
  124. package/src/components/CanvasViewToggle.vue +10 -7
  125. package/src/components/ChatInput.vue +67 -33
  126. package/src/components/FileContentHeader.vue +7 -4
  127. package/src/components/FileContentRenderer.vue +20 -6
  128. package/src/components/FileTree.vue +6 -3
  129. package/src/components/FileTreePane.vue +11 -8
  130. package/src/components/FilesView.vue +5 -3
  131. package/src/components/LockStatusPopup.vue +17 -14
  132. package/src/components/NotificationBell.vue +14 -5
  133. package/src/components/NotificationToast.vue +6 -3
  134. package/src/components/PluginLauncher.vue +19 -56
  135. package/src/components/RightSidebar.vue +13 -10
  136. package/src/components/RoleSelector.vue +2 -2
  137. package/src/components/SessionHistoryPanel.vue +38 -34
  138. package/src/components/SessionTabBar.vue +8 -10
  139. package/src/components/SettingsMcpTab.vue +49 -36
  140. package/src/components/SettingsModal.vue +24 -22
  141. package/src/components/SettingsReferenceDirsTab.vue +39 -34
  142. package/src/components/SettingsWorkspaceDirsTab.vue +37 -27
  143. package/src/components/SidebarHeader.vue +25 -4
  144. package/src/components/StackView.vue +4 -1
  145. package/src/components/SuggestionsPanel.vue +7 -4
  146. package/src/components/TodoExplorer.vue +26 -15
  147. package/src/components/ToolResultsPanel.vue +27 -13
  148. package/src/components/todo/TodoAddDialog.vue +19 -14
  149. package/src/components/todo/TodoEditDialog.vue +7 -2
  150. package/src/components/todo/TodoEditPanel.vue +17 -12
  151. package/src/components/todo/TodoKanbanView.vue +10 -5
  152. package/src/components/todo/TodoListView.vue +10 -7
  153. package/src/components/todo/TodoTableView.vue +5 -2
  154. package/src/composables/useAppApi.ts +9 -0
  155. package/src/composables/useClickOutside.ts +2 -2
  156. package/src/composables/useDynamicFavicon.ts +172 -37
  157. package/src/composables/useEventListeners.ts +7 -8
  158. package/src/composables/useFaviconState.ts +13 -2
  159. package/src/composables/useFileSelection.ts +24 -6
  160. package/src/composables/useFreshPluginData.ts +3 -3
  161. package/src/composables/useKeyNavigation.ts +11 -11
  162. package/src/composables/useLayoutMode.ts +32 -0
  163. package/src/composables/useMcpTools.ts +2 -2
  164. package/src/composables/useNotifications.ts +3 -3
  165. package/src/composables/usePdfDownload.ts +4 -4
  166. package/src/composables/usePendingCalls.ts +1 -1
  167. package/src/composables/usePubSub.ts +10 -10
  168. package/src/composables/useRoles.ts +1 -1
  169. package/src/composables/useSandboxStatus.ts +1 -1
  170. package/src/composables/useSessionDerived.ts +3 -3
  171. package/src/composables/useSessionHistory.ts +7 -17
  172. package/src/composables/useSessionSync.ts +8 -8
  173. package/src/composables/useViewLayout.ts +20 -34
  174. package/src/config/roles.ts +2 -2
  175. package/src/lang/de.ts +536 -0
  176. package/src/lang/en.ts +558 -0
  177. package/src/lang/es.ts +543 -0
  178. package/src/lang/fr.ts +536 -0
  179. package/src/lang/ja.ts +536 -0
  180. package/src/lang/ko.ts +540 -0
  181. package/src/lang/pt-BR.ts +534 -0
  182. package/src/lang/zh.ts +537 -0
  183. package/src/lib/vue-i18n.ts +97 -0
  184. package/src/main.ts +2 -0
  185. package/src/plugins/canvas/View.vue +102 -186
  186. package/src/plugins/canvas/definition.ts +0 -8
  187. package/src/plugins/chart/Preview.vue +5 -5
  188. package/src/plugins/chart/View.vue +9 -4
  189. package/src/plugins/manageRoles/Preview.vue +4 -1
  190. package/src/plugins/manageRoles/View.vue +59 -43
  191. package/src/plugins/manageSkills/Preview.vue +8 -3
  192. package/src/plugins/manageSkills/View.vue +29 -25
  193. package/src/plugins/manageSource/Preview.vue +2 -2
  194. package/src/plugins/manageSource/View.vue +73 -52
  195. package/src/plugins/markdown/Preview.vue +1 -1
  196. package/src/plugins/markdown/View.vue +26 -36
  197. package/src/plugins/presentHtml/Preview.vue +1 -1
  198. package/src/plugins/presentHtml/View.vue +7 -4
  199. package/src/plugins/presentHtml/helpers.ts +8 -8
  200. package/src/plugins/presentMulmoScript/Preview.vue +1 -1
  201. package/src/plugins/presentMulmoScript/View.vue +40 -30
  202. package/src/plugins/presentMulmoScript/helpers.ts +1 -1
  203. package/src/plugins/scheduler/Preview.vue +13 -10
  204. package/src/plugins/scheduler/TasksTab.vue +57 -28
  205. package/src/plugins/scheduler/View.vue +28 -19
  206. package/src/plugins/scheduler/formatSchedule.ts +93 -0
  207. package/src/plugins/spreadsheet/Preview.vue +8 -3
  208. package/src/plugins/spreadsheet/View.vue +21 -12
  209. package/src/plugins/textResponse/Preview.vue +15 -58
  210. package/src/plugins/textResponse/View.vue +29 -9
  211. package/src/plugins/todo/Preview.vue +13 -8
  212. package/src/plugins/todo/View.vue +38 -24
  213. package/src/plugins/todo/composables/useTodos.ts +5 -5
  214. package/src/plugins/ui-image/ImagePreview.vue +6 -3
  215. package/src/plugins/ui-image/ImageView.vue +7 -4
  216. package/src/plugins/wiki/Preview.vue +10 -7
  217. package/src/plugins/wiki/View.vue +202 -81
  218. package/src/plugins/wiki/helpers.ts +4 -4
  219. package/src/plugins/wiki/route.ts +112 -0
  220. package/src/router/guards.ts +46 -28
  221. package/src/router/index.ts +41 -26
  222. package/src/types/session.ts +4 -3
  223. package/src/types/vue-i18n.d.ts +20 -0
  224. package/src/utils/agent/request.ts +22 -3
  225. package/src/utils/canvas/layoutMode.ts +26 -0
  226. package/src/utils/dom/scrollable.ts +2 -2
  227. package/src/utils/files/expandedDirs.ts +1 -1
  228. package/src/utils/files/sortChildren.ts +6 -6
  229. package/src/utils/format/frontmatter.ts +6 -6
  230. package/src/utils/image/cacheBust.ts +16 -0
  231. package/src/utils/image/resolve.ts +16 -0
  232. package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
  233. package/src/utils/markdown/extractFirstH1.ts +2 -2
  234. package/src/utils/path/relativeLink.ts +15 -15
  235. package/src/utils/path/workspaceLinkRouter.ts +81 -0
  236. package/src/utils/role/icon.ts +2 -2
  237. package/src/utils/role/merge.ts +2 -2
  238. package/src/utils/role/plugins.ts +1 -1
  239. package/src/utils/session/sessionFactory.ts +2 -2
  240. package/src/utils/session/sessionHelpers.ts +2 -2
  241. package/src/utils/tools/dedup.ts +4 -4
  242. package/src/utils/tools/result.ts +3 -3
  243. package/src/utils/types.ts +2 -2
  244. package/src/vite-env.d.ts +9 -0
  245. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
  246. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
  247. package/client/assets/index-KNLBjwuh.css +0 -1
  248. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
  249. package/src/composables/useCanvasViewMode.ts +0 -121
  250. package/src/utils/canvas/viewMode.ts +0 -46
  251. /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-D8rhwXLq.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-KNLBjwuh.css">
20
+ <script type="module" crossorigin src="/assets/index-DtcyExH9.js"></script>
21
+ <link rel="stylesheet" crossorigin href="/assets/index-CubzmCVK.css">
24
22
  </head>
25
23
  <body>
26
24
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmoclaude",
3
- "version": "0.1.2",
3
+ "version": "0.4.0",
4
4
  "description": "MulmoClaude — GUI-chat with Claude Code + long-term memory. One command to start.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,20 +18,20 @@
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",
27
+ "@mulmobridge/chat-service": "^0.1.1",
28
+ "@mulmobridge/client": "^0.1.1",
29
+ "@mulmobridge/protocol": "^0.1.3",
30
+ "@mulmocast/types": "^2.6.8",
32
31
  "@mulmochat-plugin/form": "0.5.0",
33
32
  "@mulmochat-plugin/quiz": "0.4.0",
34
33
  "@mulmochat-plugin/ui-image": "^0.3.0",
34
+ "@receptron/task-scheduler": "^0.1.0",
35
35
  "cors": "^2.8.6",
36
36
  "dotenv": "^17.4.2",
37
37
  "express": "^5.2.1",
@@ -40,15 +40,15 @@
40
40
  "ignore": "^7.0.5",
41
41
  "mammoth": "^1.12.0",
42
42
  "marked": "^18.0.2",
43
- "mulmocast": "^2.6.7",
44
- "puppeteer": "^24.41.0",
43
+ "mulmocast": "^2.6.8",
44
+ "puppeteer": "^24.42.0",
45
45
  "socket.io": "^4.8.3",
46
46
  "socket.io-client": "^4.8.3",
47
- "uuid": "^13.0.0",
47
+ "tsx": "^4.19.0",
48
+ "uuid": "^14.0.0",
48
49
  "ws": "^8.20.0",
49
50
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
50
- "zod": "^4.3.6",
51
- "tsx": "^4.19.0"
51
+ "zod": "^4.3.6"
52
52
  },
53
53
  "engines": {
54
54
  "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
 
@@ -14,10 +14,10 @@ export const CONTAINER_WORKSPACE_PATH = "/home/node/mulmoclaude";
14
14
 
15
15
  const BASE_ALLOWED_TOOLS = ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "WebFetch", "WebSearch"];
16
16
 
17
- const MCP_PLUGINS = new Set([...MCP_PLUGIN_NAMES, ...mcpTools.filter(isMcpToolEnabled).map((t) => t.definition.name)]);
17
+ const MCP_PLUGINS = new Set([...MCP_PLUGIN_NAMES, ...mcpTools.filter(isMcpToolEnabled).map((toolDef) => toolDef.definition.name)]);
18
18
 
19
19
  export function getActivePlugins(role: Role): string[] {
20
- return role.availablePlugins.filter((p) => MCP_PLUGINS.has(p));
20
+ return role.availablePlugins.filter((pluginName) => MCP_PLUGINS.has(pluginName));
21
21
  }
22
22
 
23
23
  export interface McpConfigParams {
@@ -71,12 +71,12 @@ function prepareUserStdioServer(spec: Extract<McpServerSpec, { type: "stdio" }>,
71
71
 
72
72
  export function prepareUserServers(userServers: Record<string, McpServerSpec>, useDocker: boolean, hostWorkspacePath: string): Record<string, McpServerSpec> {
73
73
  const out: Record<string, McpServerSpec> = {};
74
- for (const [id, spec] of Object.entries(userServers)) {
74
+ for (const [serverId, spec] of Object.entries(userServers)) {
75
75
  if (spec.enabled === false) continue;
76
76
  if (spec.type === "http") {
77
- out[id] = prepareUserHttpServer(spec, useDocker);
77
+ out[serverId] = prepareUserHttpServer(spec, useDocker);
78
78
  } else {
79
- out[id] = prepareUserStdioServer(spec, useDocker, hostWorkspacePath);
79
+ out[serverId] = prepareUserStdioServer(spec, useDocker, hostWorkspacePath);
80
80
  }
81
81
  }
82
82
  return out;
@@ -137,9 +137,9 @@ function buildMulmoclaudeServer(params: { chatSessionId: string; port: number; a
137
137
  // defence-in-depth.
138
138
  function excludeReservedKeys(servers: Record<string, McpServerSpec>): Record<string, McpServerSpec> {
139
139
  const out: Record<string, McpServerSpec> = {};
140
- for (const [id, spec] of Object.entries(servers)) {
141
- if (id === "mulmoclaude") continue;
142
- out[id] = spec;
140
+ for (const [serverId, spec] of Object.entries(servers)) {
141
+ if (serverId === "mulmoclaude") continue;
142
+ out[serverId] = spec;
143
143
  }
144
144
  return out;
145
145
  }
@@ -165,12 +165,12 @@ export function buildMcpConfig(params: McpConfigParams): object {
165
165
  // we're running natively (since the sandbox image is minimal in Docker).
166
166
  export function userServerAllowedToolNames(userServers: Record<string, McpServerSpec>, useDocker: boolean): string[] {
167
167
  const names: string[] = [];
168
- for (const [id, spec] of Object.entries(userServers)) {
168
+ for (const [serverId, spec] of Object.entries(userServers)) {
169
169
  if (spec.enabled === false) continue;
170
170
  // Stdio servers are dropped under Docker because the sandbox
171
171
  // image is too minimal to run most of them (see #162).
172
172
  if (spec.type === "stdio" && useDocker) continue;
173
- names.push(`mcp__${id}`);
173
+ names.push(`mcp__${serverId}`);
174
174
  }
175
175
  return names;
176
176
  }
@@ -188,7 +188,7 @@ export interface CliArgsParams {
188
188
  export function buildCliArgs(params: CliArgsParams): string[] {
189
189
  const { systemPrompt, activePlugins, claudeSessionId, mcpConfigPath, extraAllowedTools = [] } = params;
190
190
 
191
- const mcpToolNames = activePlugins.map((p) => `mcp__mulmoclaude__${p}`);
191
+ const mcpToolNames = activePlugins.map((pluginName) => `mcp__mulmoclaude__${pluginName}`);
192
192
  const allowedTools = [...BASE_ALLOWED_TOOLS, ...extraAllowedTools, ...mcpToolNames];
193
193
 
194
194
  // stream-json input mode: the user message is streamed through
@@ -351,7 +351,7 @@ export function buildDockerSpawnArgs(params: DockerSpawnArgsParams): string[] {
351
351
  sandboxAuthArgs = [],
352
352
  sshAgentForward = false,
353
353
  } = params;
354
- const toDockerPath = (p: string): string => p.replace(/\\/g, "/");
354
+ const toDockerPath = (hostPath: string): string => hostPath.replace(/\\/g, "/");
355
355
  const extraHosts: string[] = platform === "linux" ? ["--add-host", "host.docker.internal:host-gateway"] : [];
356
356
 
357
357
  return [
@@ -1,5 +1,6 @@
1
1
  import { spawn, type ChildProcessByStdio } from "child_process";
2
- import { mkdir, writeFile, unlink } from "fs/promises";
2
+ import { mkdir, unlink } from "fs/promises";
3
+ import { writeJsonAtomic } from "../utils/files/json.js";
3
4
  import { dirname } from "path";
4
5
  import type { Readable, Writable } from "stream";
5
6
  import { isDockerAvailable } from "../system/docker.js";
@@ -157,6 +158,7 @@ export async function* runAgent(
157
158
  claudeSessionId?: string,
158
159
  abortSignal?: AbortSignal,
159
160
  attachments?: Attachment[],
161
+ userTimezone?: string,
160
162
  ): AsyncGenerator<AgentEvent> {
161
163
  const activePlugins = getActivePlugins(role);
162
164
  const useDocker = await isDockerAvailable();
@@ -180,6 +182,7 @@ export async function* runAgent(
180
182
  role,
181
183
  workspacePath: useDocker ? CONTAINER_WORKSPACE_PATH : workspacePath,
182
184
  useDocker,
185
+ userTimezone,
183
186
  });
184
187
 
185
188
  // In debug mode (--debug), dump the full system prompt on the first
@@ -202,11 +205,14 @@ export async function* runAgent(
202
205
  chatSessionId: sessionId,
203
206
  port,
204
207
  activePlugins,
205
- roleIds: loadAllRoles().map((r) => r.id),
208
+ roleIds: loadAllRoles().map((loadedRole) => loadedRole.id),
206
209
  useDocker,
207
210
  userServers,
208
211
  });
209
- await writeFile(mcpPaths.hostPath, JSON.stringify(mcpConfig, null, 2));
212
+ // Write atomically so a partially-written file can't be picked
213
+ // up by a concurrent claude spawn (they share the --mcp-config
214
+ // path under the session dir).
215
+ await writeJsonAtomic(mcpPaths.hostPath, mcpConfig);
210
216
  }
211
217
 
212
218
  // Fresh read on every invocation so the Settings UI can change
@@ -30,7 +30,7 @@ interface JsonRpcMessage {
30
30
  params?: ToolCallParams;
31
31
  }
32
32
 
33
- const isJsonRpcMessage = (v: unknown): v is JsonRpcMessage => isRecord(v) && "method" in v;
33
+ const isJsonRpcMessage = (value: unknown): value is JsonRpcMessage => isRecord(value) && "method" in value;
34
34
 
35
35
  const SESSION_ID = env.mcpSessionId;
36
36
  const PORT = env.port;
@@ -80,12 +80,12 @@ function fromPackage(def: ToolDefinition, endpoint: string): ToolDef {
80
80
 
81
81
  // Pure MCP tools (no GUI) — auto-registered from server/mcp-tools/
82
82
  const mcpToolDefs: Record<string, ToolDef> = Object.fromEntries(
83
- mcpTools.filter(isMcpToolEnabled).map((t) => [
84
- t.definition.name,
83
+ mcpTools.filter(isMcpToolEnabled).map((toolDef) => [
84
+ toolDef.definition.name,
85
85
  {
86
- name: t.definition.name,
87
- description: t.definition.description,
88
- inputSchema: t.definition.inputSchema,
86
+ name: toolDef.definition.name,
87
+ description: toolDef.definition.description,
88
+ inputSchema: toolDef.definition.inputSchema,
89
89
  },
90
90
  ]),
91
91
  );
@@ -303,7 +303,7 @@ async function handleToolCall(name: string, args: Record<string, unknown>): Prom
303
303
  // Pure MCP tools — call via /api/mcp-tools/:tool, return text directly
304
304
  // (no frontend push). Opt out of postJson's HTTP error throw because
305
305
  // we want to surface the JSON error body to the caller as a string.
306
- const mcpTool = mcpTools.find((t) => t.definition.name === name);
306
+ const mcpTool = mcpTools.find((toolDef) => toolDef.definition.name === name);
307
307
  if (mcpTool) {
308
308
  const res = await postJson(`/api/mcp-tools/${name}`, args, {
309
309
  allowHttpError: true,
@@ -313,7 +313,7 @@ async function handleToolCall(name: string, args: Record<string, unknown>): Prom
313
313
  return typeof json.result === "string" ? json.result : JSON.stringify(json.result);
314
314
  }
315
315
 
316
- const tool = tools.find((t) => t.name === name);
316
+ const tool = tools.find((toolDef) => toolDef.name === name);
317
317
  if (!tool) throw new Error(`Unknown tool: ${name}`);
318
318
 
319
319
  const res = await postJson(tool.endpoint!, args);
@@ -347,12 +347,12 @@ process.stdin.on("data", (chunk: Buffer) => {
347
347
  }
348
348
  if (!isJsonRpcMessage(msg)) continue;
349
349
 
350
- const { id, method, params } = msg;
350
+ const { id: requestId, method, params } = msg;
351
351
 
352
352
  if (method === "initialize") {
353
353
  respond({
354
354
  jsonrpc: "2.0",
355
- id,
355
+ id: requestId,
356
356
  result: {
357
357
  protocolVersion: "2024-11-05",
358
358
  capabilities: { tools: {} },
@@ -362,12 +362,12 @@ process.stdin.on("data", (chunk: Buffer) => {
362
362
  } else if (method === "tools/list") {
363
363
  respond({
364
364
  jsonrpc: "2.0",
365
- id,
365
+ id: requestId,
366
366
  result: {
367
- tools: tools.map((t) => ({
368
- name: t.name,
369
- description: t.description,
370
- inputSchema: t.inputSchema,
367
+ tools: tools.map((toolDef) => ({
368
+ name: toolDef.name,
369
+ description: toolDef.description,
370
+ inputSchema: toolDef.inputSchema,
371
371
  })),
372
372
  },
373
373
  });
@@ -375,7 +375,7 @@ process.stdin.on("data", (chunk: Buffer) => {
375
375
  if (!params?.name) {
376
376
  respond({
377
377
  jsonrpc: "2.0",
378
- id,
378
+ id: requestId,
379
379
  error: {
380
380
  code: -32602,
381
381
  message: "Invalid params: tools/call requires params.name",
@@ -388,14 +388,14 @@ process.stdin.on("data", (chunk: Buffer) => {
388
388
  .then((text) => {
389
389
  respond({
390
390
  jsonrpc: "2.0",
391
- id,
391
+ id: requestId,
392
392
  result: { content: [{ type: "text", text }] },
393
393
  });
394
394
  })
395
395
  .catch((err: unknown) => {
396
396
  respond({
397
397
  jsonrpc: "2.0",
398
- id,
398
+ id: requestId,
399
399
  result: {
400
400
  content: [{ type: "text", text: String(err) }],
401
401
  isError: true,
@@ -403,7 +403,7 @@ process.stdin.on("data", (chunk: Buffer) => {
403
403
  });
404
404
  });
405
405
  } else if (method === "ping") {
406
- respond({ jsonrpc: "2.0", id, result: {} });
406
+ respond({ jsonrpc: "2.0", id: requestId, result: {} });
407
407
  }
408
408
  // notifications/initialized and other notifications: no response needed
409
409
  }
@@ -17,7 +17,7 @@ export interface McpTool {
17
17
 
18
18
  export const mcpTools: McpTool[] = [readXPost, searchX];
19
19
 
20
- const toolMap = new Map(mcpTools.map((t) => [t.definition.name, t]));
20
+ const toolMap = new Map(mcpTools.map((tool) => [tool.definition.name, tool]));
21
21
 
22
22
  export function isMcpToolEnabled(tool: McpTool): boolean {
23
23
  return (tool.requiredEnv ?? []).every((key) => !!process.env[key]);
@@ -34,11 +34,11 @@ interface McpToolParams {
34
34
  // GET /api/mcp-tools — returns { name, enabled, requiredEnv } for each tool (used by the role builder UI)
35
35
  mcpToolsRouter.get(API_ROUTES.mcpTools.list, (_req: Request, res: Response) => {
36
36
  res.json(
37
- mcpTools.map((t) => ({
38
- name: t.definition.name,
39
- enabled: isMcpToolEnabled(t),
40
- requiredEnv: t.requiredEnv ?? [],
41
- prompt: t.prompt,
37
+ mcpTools.map((tool) => ({
38
+ name: tool.definition.name,
39
+ enabled: isMcpToolEnabled(tool),
40
+ requiredEnv: tool.requiredEnv ?? [],
41
+ prompt: tool.prompt,
42
42
  })),
43
43
  );
44
44
  });
@@ -1,5 +1,6 @@
1
1
  import { errorMessage } from "../../utils/errors.js";
2
2
  import { safeResponseText } from "../../utils/http.js";
3
+ import { toUtcIsoDate } from "../../utils/date.js";
3
4
  import { env } from "../../system/env.js";
4
5
 
5
6
  const X_API_BASE = "https://api.twitter.com/2";
@@ -56,7 +57,7 @@ async function fetchX(path: string): Promise<XApiResponse> {
56
57
  }
57
58
 
58
59
  function formatTweet(tweet: XTweet, author?: XUser, url?: string): string {
59
- const date = tweet.created_at ? new Date(tweet.created_at).toISOString().split("T")[0] : "";
60
+ const date = tweet.created_at ? toUtcIsoDate(new Date(tweet.created_at)) : "";
60
61
  const dateSuffix = date ? ` · ${date}` : "";
61
62
  const byline = author ? `@${author.username} (${author.name})${dateSuffix}` : date;
62
63
  const metrics = tweet.public_metrics
@@ -64,7 +65,7 @@ function formatTweet(tweet: XTweet, author?: XUser, url?: string): string {
64
65
  : "";
65
66
  const link = url ?? "";
66
67
  return [byline, "", tweet.text, "", metrics, link]
67
- .filter((l) => l !== undefined)
68
+ .filter((line) => line !== undefined)
68
69
  .join("\n")
69
70
  .trimEnd();
70
71
  }
@@ -104,12 +105,12 @@ export const readXPost = {
104
105
  return errorMessage(err);
105
106
  }
106
107
 
107
- if (data.errors?.length) return `X API error: ${data.errors.map((e) => e.detail).join("; ")}`;
108
+ if (data.errors?.length) return `X API error: ${data.errors.map((err) => err.detail).join("; ")}`;
108
109
 
109
110
  const tweet = data.data as XTweet | undefined;
110
111
  if (!tweet) return "Tweet not found.";
111
112
 
112
- const author = data.includes?.users?.find((u) => u.id === tweet.author_id);
113
+ const author = data.includes?.users?.find((user) => user.id === tweet.author_id);
113
114
  const canonicalUrl = author ? `https://x.com/${author.username}/status/${tweet.id}` : undefined;
114
115
  return formatTweet(tweet, author, canonicalUrl);
115
116
  },
@@ -168,13 +169,13 @@ export const searchX = {
168
169
  return errorMessage(err);
169
170
  }
170
171
 
171
- if (data.errors?.length) return `X API error: ${data.errors.map((e) => e.detail).join("; ")}`;
172
+ if (data.errors?.length) return `X API error: ${data.errors.map((err) => err.detail).join("; ")}`;
172
173
 
173
174
  const tweets = Array.isArray(data.data) ? data.data : [];
174
175
  if (tweets.length === 0) return `No recent posts found for: "${query}"`;
175
176
 
176
177
  const users = data.includes?.users ?? [];
177
- const userMap = new Map(users.map((u) => [u.id, u]));
178
+ const userMap = new Map(users.map((user) => [user.id, user]));
178
179
 
179
180
  const lines: string[] = [`Search: "${query}" — ${tweets.length} result${tweets.length !== 1 ? "s" : ""}`, ""];
180
181
  tweets.forEach((tweet, i) => {