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
@@ -77,12 +77,12 @@ function parseTranscriptEntries(jsonlContent: string): TranscriptEntry[] {
77
77
  continue;
78
78
  }
79
79
  if (!isRecord(entry)) continue;
80
- const o = entry;
81
- if (o.type !== EVENT_TYPES.text) continue;
82
- if (o.source !== "user" && o.source !== "assistant") continue;
83
- const message = o.message;
80
+ const record = entry;
81
+ if (record.type !== EVENT_TYPES.text) continue;
82
+ if (record.source !== "user" && record.source !== "assistant") continue;
83
+ const message = record.message;
84
84
  if (typeof message !== "string" || message.length === 0) continue;
85
- out.push({ source: o.source, text: message });
85
+ out.push({ source: record.source, text: message });
86
86
  }
87
87
  return out;
88
88
  }
@@ -15,7 +15,7 @@
15
15
  // See docs/sandbox-credentials.md for the user-facing contract.
16
16
 
17
17
  import path from "node:path";
18
- import fs from "node:fs";
18
+ import { existsSync, statSync } from "node:fs";
19
19
  import { execFileSync } from "node:child_process";
20
20
  import { homedir } from "node:os";
21
21
  import { log } from "../system/logger/index.js";
@@ -105,7 +105,7 @@ export function resolveMountNames(names: readonly string[], allowed: Record<stri
105
105
 
106
106
  function hostPathExists(spec: SandboxMountSpec): boolean {
107
107
  try {
108
- const stat = fs.statSync(spec.hostPath);
108
+ const stat = statSync(spec.hostPath);
109
109
  return spec.kind === "dir" ? stat.isDirectory() : stat.isFile();
110
110
  } catch {
111
111
  return false;
@@ -181,7 +181,7 @@ export function sshAgentForwardArgs(
181
181
  skippedReason: "SSH_AUTH_SOCK not set on host",
182
182
  };
183
183
  }
184
- if (!fs.existsSync(sshAuthSock)) {
184
+ if (!existsSync(sshAuthSock)) {
185
185
  return {
186
186
  args: [],
187
187
  skippedReason: `SSH_AUTH_SOCK=${sshAuthSock} not found on host`,
@@ -1,8 +1,8 @@
1
1
  import { timingSafeEqual } from "crypto";
2
2
 
3
- function safeEqual(a: string, b: string): boolean {
4
- if (a.length !== b.length) return false;
5
- return timingSafeEqual(Buffer.from(a), Buffer.from(b));
3
+ function safeEqual(left: string, right: string): boolean {
4
+ if (left.length !== right.length) return false;
5
+ return timingSafeEqual(Buffer.from(left), Buffer.from(right));
6
6
  }
7
7
 
8
8
  // Bearer token middleware (#272). Reject any `/api/*` request whose
@@ -21,7 +21,7 @@
21
21
  // setting it once on both sides survives restarts.
22
22
 
23
23
  import { randomBytes } from "crypto";
24
- import fs from "fs";
24
+ import { promises } from "fs";
25
25
  import { writeFileAtomic } from "../../utils/files/index.js";
26
26
  import { log } from "../../system/logger/index.js";
27
27
  import { isNonEmptyString } from "../../utils/types.js";
@@ -83,7 +83,7 @@ function resolveToken(override: string | undefined): string {
83
83
  */
84
84
  export async function deleteTokenFile(tokenPath: string = WORKSPACE_PATHS.sessionToken): Promise<void> {
85
85
  try {
86
- await fs.promises.unlink(tokenPath);
86
+ await promises.unlink(tokenPath);
87
87
  } catch {
88
88
  /* already gone — nothing to do */
89
89
  }
@@ -27,7 +27,18 @@ import { logBackgroundError } from "../../utils/logBackgroundError.js";
27
27
  import { createArgsCache, recordToolEvent } from "../../workspace/tool-trace/index.js";
28
28
  import { API_ROUTES } from "../../../src/config/apiRoutes.js";
29
29
  import { EVENT_TYPES } from "../../../src/types/events.js";
30
- import { isSessionOrigin } from "../../../src/types/session.js";
30
+ import { isSessionOrigin, type SessionOrigin } from "../../../src/types/session.js";
31
+ // Imports kept commented (instead of deleted) alongside the
32
+ // publishNotification call below — see the duplicate-notification
33
+ // comment near `endRun()` in `runAgentInBackground` for context.
34
+ // `SESSION_ORIGINS` is dragged into this same commented block
35
+ // because every remaining live reference to it lived inside the
36
+ // commented helper / call site; once those went, leaving the value
37
+ // import un-commented would trip the unused-import lint rule.
38
+ // (by snakajima)
39
+ // import { SESSION_ORIGINS } from "../../../src/types/session.js";
40
+ // import { NOTIFICATION_KINDS } from "../../../src/types/notification.js";
41
+ // import { publishNotification } from "../../events/notifications.js";
31
42
  import { env } from "../../system/env.js";
32
43
  import type { Attachment } from "@mulmobridge/protocol";
33
44
  import { parseDataUrl } from "@mulmobridge/client";
@@ -105,12 +116,23 @@ export interface StartChatParams {
105
116
  /** Where this session originates (#486). Accepts string for
106
117
  * cross-package compatibility (chat-service passes string). */
107
118
  origin?: string;
119
+ /** IANA timezone the user's browser resolved (e.g. "Asia/Tokyo").
120
+ * Validated server-side before it reaches the system prompt — an
121
+ * invalid or missing value falls back to server-local time. */
122
+ userTimezone?: string;
123
+ /** Flat primitive bag forwarded from the bridge handshake, string
124
+ * / number / boolean values only (see plans/feat-bridge-options-
125
+ * passthrough.md). The session-level `defaultRole` override is
126
+ * already applied upstream in chat-service; MulmoClaude doesn't
127
+ * read any other keys today. Accepted here so the typing matches
128
+ * `StartChatFn` exported by chat-service. */
129
+ bridgeOptions?: Readonly<Record<string, string | number | boolean>>;
108
130
  }
109
131
 
110
132
  export type StartChatResult = { kind: "started"; chatSessionId: string } | { kind: "error"; error: string; status?: number };
111
133
 
112
134
  export async function startChat(params: StartChatParams): Promise<StartChatResult> {
113
- const { message, roleId, chatSessionId, selectedImageData, attachments } = params;
135
+ const { message, roleId, chatSessionId, selectedImageData, attachments, userTimezone } = params;
114
136
 
115
137
  if (!message || !roleId || !chatSessionId) {
116
138
  return {
@@ -203,6 +225,8 @@ export async function startChat(params: StartChatParams): Promise<StartChatResul
203
225
  requestStartedAt,
204
226
  toolArgsCache: createArgsCache(),
205
227
  attachments: mergeAttachments(selectedImageData, attachments),
228
+ userTimezone,
229
+ origin: validOrigin,
206
230
  });
207
231
 
208
232
  return { kind: "started", chatSessionId };
@@ -239,6 +263,7 @@ interface AgentBody {
239
263
  roleId: string;
240
264
  chatSessionId: string;
241
265
  selectedImageData?: string;
266
+ userTimezone?: string;
242
267
  }
243
268
 
244
269
  interface ErrorResponse {
@@ -271,6 +296,12 @@ interface BackgroundRunParams {
271
296
  requestStartedAt: number;
272
297
  toolArgsCache: ReturnType<typeof createArgsCache>;
273
298
  attachments: Attachment[] | undefined;
299
+ userTimezone: string | undefined;
300
+ // Where this run was triggered from. Used to decide whether to
301
+ // fire a completion notification: human-initiated runs don't (the
302
+ // user is right there in the UI), but scheduler / bridge / skill
303
+ // runs do (the user is probably away from the keyboard).
304
+ origin: SessionOrigin | undefined;
274
305
  }
275
306
 
276
307
  // Per-event side-effect context passed to `handleAgentEvent`.
@@ -357,8 +388,30 @@ async function flushTextAccumulator(ctx: EventContext): Promise<void> {
357
388
  );
358
389
  }
359
390
 
391
+ // Helper kept commented (instead of deleted) alongside the
392
+ // publishNotification call below — see the duplicate-notification
393
+ // comment near `endRun()` in `runAgentInBackground` for context.
394
+ // (by snakajima)
395
+ //
396
+ // // Build the title used for the agent-completion notification on
397
+ // // non-human runs. Surfaces both the role name and the trigger so
398
+ // // the user can read it in passing on a phone lock screen.
399
+ // function completionNotificationTitle(roleName: string, origin: SessionOrigin): string {
400
+ // switch (origin) {
401
+ // case SESSION_ORIGINS.scheduler:
402
+ // return `✅ ${roleName} (scheduler) finished`;
403
+ // case SESSION_ORIGINS.skill:
404
+ // return `✅ ${roleName} (skill) finished`;
405
+ // case SESSION_ORIGINS.bridge:
406
+ // return `✅ ${roleName} reply ready`;
407
+ // default:
408
+ // return `✅ ${roleName} finished`;
409
+ // }
410
+ // }
411
+
360
412
  async function runAgentInBackground(params: BackgroundRunParams): Promise<void> {
361
- const { decoratedMessage, role, chatSessionId, claudeSessionId, abortSignal, resultsFilePath, requestStartedAt, toolArgsCache, attachments } = params;
413
+ const { decoratedMessage, role, chatSessionId, claudeSessionId, abortSignal, resultsFilePath, requestStartedAt, toolArgsCache, attachments, userTimezone } =
414
+ params;
362
415
 
363
416
  const eventCtx: EventContext = {
364
417
  chatSessionId,
@@ -378,7 +431,17 @@ async function runAgentInBackground(params: BackgroundRunParams): Promise<void>
378
431
  try {
379
432
  while (true) {
380
433
  let staleSessionDetected = false;
381
- for await (const event of runAgent(currentMessage, role, workspacePath, chatSessionId, PORT, currentClaudeSessionId, abortSignal, attachments)) {
434
+ for await (const event of runAgent(
435
+ currentMessage,
436
+ role,
437
+ workspacePath,
438
+ chatSessionId,
439
+ PORT,
440
+ currentClaudeSessionId,
441
+ abortSignal,
442
+ attachments,
443
+ userTimezone,
444
+ )) {
382
445
  if (failoverAttemptsRemaining > 0 && event.type === EVENT_TYPES.error && typeof event.message === "string" && isStaleSessionError(event.message)) {
383
446
  // Swallow the error — we're about to recover. `break`
384
447
  // abandons the current generator; since the event is only
@@ -431,6 +494,38 @@ async function runAgentInBackground(params: BackgroundRunParams): Promise<void>
431
494
  });
432
495
  } finally {
433
496
  endRun(chatSessionId);
497
+ // Commented out: this would create a duplicate notification.
498
+ //
499
+ // `endRun(chatSessionId)` above flips `session.hasUnread = true`
500
+ // for every chat-session turn completion regardless of origin,
501
+ // which already lights up the red unread-count badge on the
502
+ // Session History Panel toggle button (driven by `hasUnread` →
503
+ // `useSessionDerived.unreadCount` →
504
+ // `SessionHistoryToggleButton.vue`). Firing
505
+ // `publishNotification` here adds a *second* red badge — on the
506
+ // notification bell — for the exact same event, in the same
507
+ // chrome row. Two indicators, one event = noise.
508
+ //
509
+ // The duplicate occurs whenever a chat session receives a new
510
+ // message, which is exactly what every code path through this
511
+ // `finally` represents. The initiator of the turn (human, bridge
512
+ // user, scheduled job, skill chain, another agent) does not
513
+ // change this — both badges flip together.
514
+ //
515
+ // Other `publishNotification` call sites (news pipeline, `notify`
516
+ // MCP tool, scheduled-test endpoint) do not post a chat-session
517
+ // message at the same time, so they are not duplicates and
518
+ // remain enabled.
519
+ //
520
+ // (by snakajima)
521
+ //
522
+ // if (params.origin && params.origin !== SESSION_ORIGINS.human) {
523
+ // publishNotification({
524
+ // kind: NOTIFICATION_KINDS.agent,
525
+ // title: completionNotificationTitle(params.role.name, params.origin),
526
+ // sessionId: chatSessionId,
527
+ // });
528
+ // }
434
529
  // Fire-and-forget: journal + chat-index post-processing
435
530
  maybeRunJournal({ activeSessionIds: getActiveSessionIds() }).catch(logBackgroundError("journal"));
436
531
  maybeIndexSession({
@@ -4,6 +4,8 @@ import { writeWorkspaceText } from "../../utils/files/workspace-io.js";
4
4
  import { buildArtifactPath } from "../../utils/files/naming.js";
5
5
  import { errorMessage } from "../../utils/errors.js";
6
6
  import { badRequest, serverError } from "../../utils/httpError.js";
7
+ import { log } from "../../system/logger/index.js";
8
+ import { previewSnippet } from "../../utils/logPreview.js";
7
9
  import { isRecord } from "../../utils/types.js";
8
10
  import { API_ROUTES } from "../../../src/config/apiRoutes.js";
9
11
 
@@ -68,13 +70,22 @@ function isValidChartEntry(value: unknown): value is ChartEntry {
68
70
 
69
71
  router.post(API_ROUTES.chart.present, async (req: Request<object, unknown, PresentChartBody>, res: Response<PresentChartResponse>) => {
70
72
  const { document, title } = req.body;
73
+ log.info("chart", "present: start", {
74
+ titlePreview: typeof title === "string" ? previewSnippet(title) : undefined,
75
+ chartCount:
76
+ typeof document === "object" && document !== null && Array.isArray((document as { charts?: unknown[] }).charts)
77
+ ? (document as { charts: unknown[] }).charts.length
78
+ : undefined,
79
+ });
71
80
 
72
81
  if (!isValidChartDocument(document)) {
82
+ log.warn("chart", "present: invalid document shape");
73
83
  badRequest(res, "document must be { charts: [{ option: {...}, title?, type? }, ...] } with at least one entry");
74
84
  return;
75
85
  }
76
86
 
77
87
  if (title !== undefined && typeof title !== "string") {
88
+ log.warn("chart", "present: title must be string");
78
89
  badRequest(res, "title must be a string when provided");
79
90
  return;
80
91
  }
@@ -83,6 +94,7 @@ router.post(API_ROUTES.chart.present, async (req: Request<object, unknown, Prese
83
94
  const baseLabel = title ?? document.title ?? "chart";
84
95
  const filePath = buildArtifactPath(WORKSPACE_DIRS.charts, baseLabel, ".chart.json", "chart");
85
96
  await writeWorkspaceText(filePath, `${JSON.stringify(document, null, 2)}\n`);
97
+ log.info("chart", "present: ok", { filePath, chartCount: document.charts.length });
86
98
  res.json({
87
99
  message: `Saved chart document to ${filePath}`,
88
100
  instructions:
@@ -91,6 +103,7 @@ router.post(API_ROUTES.chart.present, async (req: Request<object, unknown, Prese
91
103
  data: { document, title, filePath },
92
104
  });
93
105
  } catch (err) {
106
+ log.error("chart", "present: threw", { error: errorMessage(err) });
94
107
  serverError(res, errorMessage(err));
95
108
  }
96
109
  });
@@ -13,6 +13,7 @@ import { Router, Request, Response } from "express";
13
13
  import { backfillAllSessions } from "../../workspace/chat-index/index.js";
14
14
  import { log } from "../../system/logger/index.js";
15
15
  import { serverError } from "../../utils/httpError.js";
16
+ import { errorMessage } from "../../utils/errors.js";
16
17
  import { API_ROUTES } from "../../../src/config/apiRoutes.js";
17
18
 
18
19
  interface RebuildResponse {
@@ -39,7 +40,7 @@ router.post(API_ROUTES.chatIndex.rebuild, async (_req: Request, res: Response<Re
39
40
  res.json(result);
40
41
  } catch (err) {
41
42
  log.warn("chat-index", "rebuild failed", { error: String(err) });
42
- serverError(res, err instanceof Error ? err.message : "unknown error");
43
+ serverError(res, errorMessage(err, "unknown error"));
43
44
  }
44
45
  });
45
46
 
@@ -12,8 +12,10 @@ import {
12
12
  type McpServerEntry,
13
13
  } from "../../system/config.js";
14
14
  import { badRequest, serverError } from "../../utils/httpError.js";
15
+ import { errorMessage } from "../../utils/errors.js";
15
16
  import { isRecord } from "../../utils/types.js";
16
17
  import { API_ROUTES } from "../../../src/config/apiRoutes.js";
18
+ import { log } from "../../system/logger/index.js";
17
19
  import { loadCustomDirs, saveCustomDirs, ensureCustomDirs, validateCustomDirs, type CustomDirEntry } from "../../workspace/custom-dirs.js";
18
20
  import { loadReferenceDirs, saveReferenceDirs, validateReferenceDirs, type ReferenceDirEntry } from "../../workspace/reference-dirs.js";
19
21
 
@@ -43,7 +45,7 @@ function isMcpPutBody(value: unknown): value is { servers: McpServerEntry[] } {
43
45
  if (!Array.isArray(value.servers)) return false;
44
46
  // Full shape validation happens inside fromMcpEntries (throws on
45
47
  // anything malformed). Here we just confirm the envelope.
46
- return value.servers.every((e) => isRecord(e) && "id" in e && "spec" in e);
48
+ return value.servers.every((entry) => isRecord(entry) && "id" in entry && "spec" in entry);
47
49
  }
48
50
 
49
51
  // Parse an MCP payload through `fromMcpEntries` (which does the full
@@ -53,7 +55,7 @@ function parseMcpPayloadOrFail(res: ConfigRes, servers: McpServerEntry[]): McpCo
53
55
  try {
54
56
  return fromMcpEntries(servers);
55
57
  } catch (err) {
56
- badRequest(res, err instanceof Error ? err.message : "invalid mcp entries");
58
+ badRequest(res, errorMessage(err, "invalid mcp entries"));
57
59
  return null;
58
60
  }
59
61
  }
@@ -66,7 +68,8 @@ function runSaveOrFail(res: ConfigRes, save: () => void, fallback: string): bool
66
68
  save();
67
69
  return true;
68
70
  } catch (err) {
69
- serverError(res, err instanceof Error ? err.message : fallback);
71
+ log.error("config", `save failed: ${fallback}`, { error: errorMessage(err) });
72
+ serverError(res, errorMessage(err, fallback));
70
73
  return false;
71
74
  }
72
75
  }
@@ -94,7 +97,9 @@ function isPutConfigBody(value: unknown): value is PutConfigBody {
94
97
 
95
98
  router.put(API_ROUTES.config.base, (req: Request<unknown, unknown, PutConfigBody>, res: ConfigRes) => {
96
99
  const body = req.body;
100
+ log.info("config", "PUT base: start");
97
101
  if (!isPutConfigBody(body)) {
102
+ log.warn("config", "PUT base: invalid payload");
98
103
  badRequest(res, "Invalid config payload");
99
104
  return;
100
105
  }
@@ -113,29 +118,35 @@ router.put(API_ROUTES.config.base, (req: Request<unknown, unknown, PutConfigBody
113
118
  // is already on the wire.
114
119
  try {
115
120
  saveSettings(previousSettings);
116
- } catch {
117
- /* swallow original error already sent */
121
+ } catch (err) {
122
+ log.error("config", "PUT base: rollback also failed", { error: errorMessage(err) });
118
123
  }
119
124
  return;
120
125
  }
126
+ log.info("config", "PUT base: ok");
121
127
  res.json(buildFullResponse());
122
128
  });
123
129
 
124
130
  router.put(API_ROUTES.config.settings, (req: Request<unknown, unknown, AppSettings>, res: ConfigRes) => {
125
131
  const body = req.body;
132
+ log.info("config", "PUT settings: start");
126
133
  if (!isAppSettings(body)) {
134
+ log.warn("config", "PUT settings: invalid payload");
127
135
  badRequest(res, "Invalid AppSettings payload");
128
136
  return;
129
137
  }
130
138
  if (!runSaveOrFail(res, () => saveSettings(body), "saveSettings failed")) {
131
139
  return;
132
140
  }
141
+ log.info("config", "PUT settings: ok");
133
142
  res.json(buildFullResponse());
134
143
  });
135
144
 
136
145
  router.put(API_ROUTES.config.mcp, (req: Request<unknown, unknown, { servers: McpServerEntry[] }>, res: ConfigRes) => {
137
146
  const body = req.body;
147
+ log.info("config", "PUT mcp: start", { servers: Array.isArray(body?.servers) ? body.servers.length : undefined });
138
148
  if (!isMcpPutBody(body)) {
149
+ log.warn("config", "PUT mcp: invalid envelope");
139
150
  badRequest(res, "Invalid mcp payload envelope");
140
151
  return;
141
152
  }
@@ -146,6 +157,7 @@ router.put(API_ROUTES.config.mcp, (req: Request<unknown, unknown, { servers: Mcp
146
157
  if (!runSaveOrFail(res, () => saveMcpConfig(cfg), "saveMcpConfig failed")) {
147
158
  return;
148
159
  }
160
+ log.info("config", "PUT mcp: ok", { servers: body.servers.length });
149
161
  res.json(buildFullResponse());
150
162
  });
151
163
 
@@ -159,21 +171,26 @@ router.put(
159
171
  API_ROUTES.config.workspaceDirs,
160
172
  (req: Request<unknown, unknown, { dirs: unknown }>, res: Response<{ dirs: CustomDirEntry[] } | ConfigErrorResponse>) => {
161
173
  const body = req.body;
174
+ log.info("config", "PUT workspace-dirs: start");
162
175
  if (!isRecord(body) || !("dirs" in body)) {
176
+ log.warn("config", "PUT workspace-dirs: invalid envelope");
163
177
  badRequest(res, "expected { dirs: [...] }");
164
178
  return;
165
179
  }
166
180
  const result = validateCustomDirs(body.dirs);
167
181
  if ("error" in result) {
182
+ log.warn("config", "PUT workspace-dirs: validation failed", { error: result.error });
168
183
  badRequest(res, result.error);
169
184
  return;
170
185
  }
171
186
  try {
172
187
  saveCustomDirs(result.entries);
173
188
  ensureCustomDirs(result.entries);
189
+ log.info("config", "PUT workspace-dirs: ok", { dirs: result.entries.length });
174
190
  res.json({ dirs: result.entries });
175
191
  } catch (err) {
176
- serverError(res, err instanceof Error ? err.message : "save failed");
192
+ log.error("config", "PUT workspace-dirs: threw", { error: errorMessage(err) });
193
+ serverError(res, errorMessage(err, "save failed"));
177
194
  }
178
195
  },
179
196
  );
@@ -188,20 +205,25 @@ router.put(
188
205
  API_ROUTES.config.referenceDirs,
189
206
  (req: Request<unknown, unknown, { dirs: unknown }>, res: Response<{ dirs: ReferenceDirEntry[] } | ConfigErrorResponse>) => {
190
207
  const body = req.body;
208
+ log.info("config", "PUT reference-dirs: start");
191
209
  if (!isRecord(body) || !("dirs" in body)) {
210
+ log.warn("config", "PUT reference-dirs: invalid envelope");
192
211
  badRequest(res, "expected { dirs: [...] }");
193
212
  return;
194
213
  }
195
214
  const result = validateReferenceDirs(body.dirs);
196
215
  if ("error" in result) {
216
+ log.warn("config", "PUT reference-dirs: validation failed", { error: result.error });
197
217
  badRequest(res, result.error);
198
218
  return;
199
219
  }
200
220
  try {
201
221
  saveReferenceDirs(result.entries);
222
+ log.info("config", "PUT reference-dirs: ok", { dirs: result.entries.length });
202
223
  res.json({ dirs: result.entries });
203
224
  } catch (err) {
204
- serverError(res, err instanceof Error ? err.message : "save failed");
225
+ log.error("config", "PUT reference-dirs: threw", { error: errorMessage(err) });
226
+ serverError(res, errorMessage(err, "save failed"));
205
227
  }
206
228
  },
207
229
  );
@@ -220,12 +242,15 @@ router.put(
220
242
  API_ROUTES.config.schedulerOverrides,
221
243
  async (req: Request<unknown, unknown, { overrides: unknown }>, res: Response<{ overrides: ScheduleOverrides } | ConfigErrorResponse>) => {
222
244
  const body = req.body;
245
+ log.info("config", "PUT scheduler-overrides: start");
223
246
  if (!isRecord(body) || !("overrides" in body)) {
247
+ log.warn("config", "PUT scheduler-overrides: invalid envelope");
224
248
  badRequest(res, "expected { overrides: { ... } }");
225
249
  return;
226
250
  }
227
251
  const raw = body.overrides;
228
252
  if (!isRecord(raw)) {
253
+ log.warn("config", "PUT scheduler-overrides: overrides not an object");
229
254
  badRequest(res, "overrides must be an object");
230
255
  return;
231
256
  }
@@ -248,9 +273,11 @@ router.put(
248
273
  }
249
274
  }
250
275
 
276
+ log.info("config", "PUT scheduler-overrides: ok", { tasks: Object.keys(overrides).length });
251
277
  res.json({ overrides: loadSchedulerOverrides() });
252
278
  } catch (err) {
253
- serverError(res, err instanceof Error ? err.message : "save failed");
279
+ log.error("config", "PUT scheduler-overrides: threw", { error: errorMessage(err) });
280
+ serverError(res, errorMessage(err, "save failed"));
254
281
  }
255
282
  },
256
283
  );