mulmoclaude 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/README.md +44 -0
  2. package/bin/mulmoclaude.js +202 -0
  3. package/bin/prepare-dist.js +93 -0
  4. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
  5. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
  6. package/client/assets/index-D8rhwXLq.js +4906 -0
  7. package/client/assets/index-KNLBjwuh.css +1 -0
  8. package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
  9. package/client/assets/material-icons-Dr0goTwe.woff +0 -0
  10. package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
  11. package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
  12. package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
  13. package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
  14. package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
  15. package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
  16. package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
  17. package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
  18. package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
  19. package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
  20. package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
  21. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
  22. package/client/index.html +28 -0
  23. package/package.json +66 -0
  24. package/server/agent/attachmentConverter.ts +270 -0
  25. package/server/agent/config.ts +414 -0
  26. package/server/agent/index.ts +260 -0
  27. package/server/agent/mcp-server.ts +412 -0
  28. package/server/agent/mcp-tools/index.ts +63 -0
  29. package/server/agent/mcp-tools/x.ts +188 -0
  30. package/server/agent/plugin-names.ts +75 -0
  31. package/server/agent/prompt.ts +349 -0
  32. package/server/agent/resumeFailover.ts +129 -0
  33. package/server/agent/sandboxMounts.ts +329 -0
  34. package/server/agent/stream.ts +194 -0
  35. package/server/api/auth/bearerAuth.ts +61 -0
  36. package/server/api/auth/token.ts +98 -0
  37. package/server/api/csrfGuard.ts +85 -0
  38. package/server/api/routes/agent.ts +478 -0
  39. package/server/api/routes/chart.ts +98 -0
  40. package/server/api/routes/chat-index.ts +46 -0
  41. package/server/api/routes/config.ts +258 -0
  42. package/server/api/routes/dispatchResponse.ts +79 -0
  43. package/server/api/routes/files.ts +812 -0
  44. package/server/api/routes/html.ts +101 -0
  45. package/server/api/routes/image.ts +169 -0
  46. package/server/api/routes/mulmo-script.ts +712 -0
  47. package/server/api/routes/mulmoScriptValidate.ts +101 -0
  48. package/server/api/routes/notifications.ts +69 -0
  49. package/server/api/routes/pdf.ts +163 -0
  50. package/server/api/routes/plugins.ts +276 -0
  51. package/server/api/routes/presentHtml.ts +48 -0
  52. package/server/api/routes/roles.ts +125 -0
  53. package/server/api/routes/scheduler.ts +153 -0
  54. package/server/api/routes/schedulerHandlers.ts +151 -0
  55. package/server/api/routes/schedulerTasks.ts +163 -0
  56. package/server/api/routes/sessions.ts +294 -0
  57. package/server/api/routes/sessionsCursor.ts +59 -0
  58. package/server/api/routes/skills.ts +195 -0
  59. package/server/api/routes/sources.ts +540 -0
  60. package/server/api/routes/todos.ts +263 -0
  61. package/server/api/routes/todosColumnsHandlers.ts +347 -0
  62. package/server/api/routes/todosHandlers.ts +274 -0
  63. package/server/api/routes/todosItemsHandlers.ts +386 -0
  64. package/server/api/routes/wiki/pageIndex.ts +53 -0
  65. package/server/api/routes/wiki.ts +363 -0
  66. package/server/api/sandboxStatus.ts +64 -0
  67. package/server/events/notifications.ts +160 -0
  68. package/server/events/pub-sub/index.ts +45 -0
  69. package/server/events/relay-client.ts +288 -0
  70. package/server/events/scheduler-adapter.ts +302 -0
  71. package/server/events/session-store/index.ts +492 -0
  72. package/server/events/task-manager/index.ts +181 -0
  73. package/server/index.ts +572 -0
  74. package/server/system/config.ts +243 -0
  75. package/server/system/credentials.ts +220 -0
  76. package/server/system/docker.ts +97 -0
  77. package/server/system/env.ts +109 -0
  78. package/server/system/logger/config.ts +112 -0
  79. package/server/system/logger/formatters.ts +40 -0
  80. package/server/system/logger/index.ts +53 -0
  81. package/server/system/logger/rotation.ts +37 -0
  82. package/server/system/logger/sinks.ts +101 -0
  83. package/server/system/logger/types.ts +29 -0
  84. package/server/utils/date.ts +57 -0
  85. package/server/utils/errors.ts +7 -0
  86. package/server/utils/fetch.ts +27 -0
  87. package/server/utils/files/atomic.ts +125 -0
  88. package/server/utils/files/html-io.ts +20 -0
  89. package/server/utils/files/image-store.ts +66 -0
  90. package/server/utils/files/index.ts +45 -0
  91. package/server/utils/files/journal-io.ts +213 -0
  92. package/server/utils/files/json.ts +69 -0
  93. package/server/utils/files/markdown-store.ts +33 -0
  94. package/server/utils/files/naming.ts +50 -0
  95. package/server/utils/files/reference-dirs-io.ts +45 -0
  96. package/server/utils/files/roles-io.ts +45 -0
  97. package/server/utils/files/safe.ts +106 -0
  98. package/server/utils/files/scheduler-io.ts +20 -0
  99. package/server/utils/files/scheduler-overrides-io.ts +64 -0
  100. package/server/utils/files/session-io.ts +136 -0
  101. package/server/utils/files/spreadsheet-store.ts +63 -0
  102. package/server/utils/files/todos-io.ts +29 -0
  103. package/server/utils/files/user-tasks-io.ts +25 -0
  104. package/server/utils/files/workspace-io.ts +221 -0
  105. package/server/utils/gemini.ts +59 -0
  106. package/server/utils/gitignore.ts +69 -0
  107. package/server/utils/http.ts +15 -0
  108. package/server/utils/httpError.ts +61 -0
  109. package/server/utils/id.ts +16 -0
  110. package/server/utils/json.ts +83 -0
  111. package/server/utils/logBackgroundError.ts +22 -0
  112. package/server/utils/markdown.ts +82 -0
  113. package/server/utils/request.ts +29 -0
  114. package/server/utils/slug.ts +50 -0
  115. package/server/utils/spawn.ts +62 -0
  116. package/server/utils/time.ts +34 -0
  117. package/server/utils/types.ts +47 -0
  118. package/server/workspace/chat-index/index.ts +153 -0
  119. package/server/workspace/chat-index/indexer.ts +209 -0
  120. package/server/workspace/chat-index/paths.ts +34 -0
  121. package/server/workspace/chat-index/summarizer.ts +247 -0
  122. package/server/workspace/chat-index/types.ts +38 -0
  123. package/server/workspace/custom-dirs.ts +220 -0
  124. package/server/workspace/helps/business.md +104 -0
  125. package/server/workspace/helps/github.md +23 -0
  126. package/server/workspace/helps/index.md +60 -0
  127. package/server/workspace/helps/mulmoscript.md +249 -0
  128. package/server/workspace/helps/sandbox.md +90 -0
  129. package/server/workspace/helps/spreadsheet.md +43 -0
  130. package/server/workspace/helps/telegram.md +135 -0
  131. package/server/workspace/helps/wiki.md +131 -0
  132. package/server/workspace/journal/archivist.ts +386 -0
  133. package/server/workspace/journal/dailyPass.ts +743 -0
  134. package/server/workspace/journal/diff.ts +71 -0
  135. package/server/workspace/journal/index.ts +185 -0
  136. package/server/workspace/journal/indexFile.ts +136 -0
  137. package/server/workspace/journal/linkRewrite.ts +4 -0
  138. package/server/workspace/journal/memoryExtractor.ts +130 -0
  139. package/server/workspace/journal/optimizationPass.ts +160 -0
  140. package/server/workspace/journal/paths.ts +76 -0
  141. package/server/workspace/journal/state.ts +125 -0
  142. package/server/workspace/paths.ts +158 -0
  143. package/server/workspace/reference-dirs.ts +252 -0
  144. package/server/workspace/roles.ts +37 -0
  145. package/server/workspace/skills/discovery.ts +125 -0
  146. package/server/workspace/skills/index.ts +10 -0
  147. package/server/workspace/skills/parser.ts +144 -0
  148. package/server/workspace/skills/paths.ts +41 -0
  149. package/server/workspace/skills/scheduler.ts +149 -0
  150. package/server/workspace/skills/types.ts +30 -0
  151. package/server/workspace/skills/user-tasks.ts +257 -0
  152. package/server/workspace/skills/writer.ts +189 -0
  153. package/server/workspace/sources/arxivDiscovery.ts +182 -0
  154. package/server/workspace/sources/classifier.ts +268 -0
  155. package/server/workspace/sources/fetchers/arxiv.ts +170 -0
  156. package/server/workspace/sources/fetchers/github.ts +106 -0
  157. package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
  158. package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
  159. package/server/workspace/sources/fetchers/index.ts +71 -0
  160. package/server/workspace/sources/fetchers/registerAll.ts +15 -0
  161. package/server/workspace/sources/fetchers/rss.ts +141 -0
  162. package/server/workspace/sources/fetchers/rssParser.ts +295 -0
  163. package/server/workspace/sources/httpFetcher.ts +230 -0
  164. package/server/workspace/sources/interests.ts +120 -0
  165. package/server/workspace/sources/paths.ts +110 -0
  166. package/server/workspace/sources/pipeline/dedup.ts +60 -0
  167. package/server/workspace/sources/pipeline/fetch.ts +136 -0
  168. package/server/workspace/sources/pipeline/index.ts +249 -0
  169. package/server/workspace/sources/pipeline/notify.ts +72 -0
  170. package/server/workspace/sources/pipeline/plan.ts +66 -0
  171. package/server/workspace/sources/pipeline/summarize.ts +189 -0
  172. package/server/workspace/sources/pipeline/write.ts +185 -0
  173. package/server/workspace/sources/rateLimiter.ts +148 -0
  174. package/server/workspace/sources/registry.ts +326 -0
  175. package/server/workspace/sources/robots.ts +271 -0
  176. package/server/workspace/sources/sourceState.ts +135 -0
  177. package/server/workspace/sources/taxonomy.ts +74 -0
  178. package/server/workspace/sources/types.ts +144 -0
  179. package/server/workspace/sources/urls.ts +112 -0
  180. package/server/workspace/tool-trace/classify.ts +114 -0
  181. package/server/workspace/tool-trace/index.ts +250 -0
  182. package/server/workspace/tool-trace/writeSearch.ts +98 -0
  183. package/server/workspace/wiki-backlinks/index.ts +107 -0
  184. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
  185. package/server/workspace/workspace.ts +66 -0
  186. package/src/App.vue +720 -0
  187. package/src/assets/mulmo_bw.png +0 -0
  188. package/src/components/CanvasViewToggle.vue +27 -0
  189. package/src/components/ChatAttachmentPreview.vue +45 -0
  190. package/src/components/ChatImagePreview.vue +17 -0
  191. package/src/components/ChatInput.vue +208 -0
  192. package/src/components/FileContentHeader.vue +49 -0
  193. package/src/components/FileContentRenderer.vue +162 -0
  194. package/src/components/FileTree.vue +115 -0
  195. package/src/components/FileTreePane.vue +85 -0
  196. package/src/components/FilesView.vue +206 -0
  197. package/src/components/LockStatusPopup.vue +111 -0
  198. package/src/components/NotificationBell.vue +131 -0
  199. package/src/components/NotificationToast.vue +72 -0
  200. package/src/components/PluginLauncher.vue +138 -0
  201. package/src/components/RightSidebar.vue +113 -0
  202. package/src/components/RoleSelector.vue +64 -0
  203. package/src/components/SessionHistoryPanel.vue +176 -0
  204. package/src/components/SessionTabBar.vue +81 -0
  205. package/src/components/SettingsMcpTab.vue +350 -0
  206. package/src/components/SettingsModal.vue +275 -0
  207. package/src/components/SettingsReferenceDirsTab.vue +173 -0
  208. package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
  209. package/src/components/SidebarHeader.vue +69 -0
  210. package/src/components/StackView.vue +360 -0
  211. package/src/components/SuggestionsPanel.vue +65 -0
  212. package/src/components/TodoExplorer.vue +358 -0
  213. package/src/components/ToolResultsPanel.vue +77 -0
  214. package/src/components/todo/TodoAddDialog.vue +131 -0
  215. package/src/components/todo/TodoEditDialog.vue +47 -0
  216. package/src/components/todo/TodoEditPanel.vue +113 -0
  217. package/src/components/todo/TodoKanbanView.vue +249 -0
  218. package/src/components/todo/TodoListView.vue +79 -0
  219. package/src/components/todo/TodoTableView.vue +177 -0
  220. package/src/composables/useActiveSession.ts +40 -0
  221. package/src/composables/useAppApi.ts +45 -0
  222. package/src/composables/useCanvasViewMode.ts +121 -0
  223. package/src/composables/useChatScroll.ts +47 -0
  224. package/src/composables/useClickOutside.ts +26 -0
  225. package/src/composables/useClipboardCopy.ts +44 -0
  226. package/src/composables/useContentDisplay.ts +52 -0
  227. package/src/composables/useDebugBeat.ts +23 -0
  228. package/src/composables/useDynamicFavicon.ts +115 -0
  229. package/src/composables/useEventListeners.ts +42 -0
  230. package/src/composables/useExpandedDirs.ts +64 -0
  231. package/src/composables/useFaviconState.ts +30 -0
  232. package/src/composables/useFileSelection.ts +115 -0
  233. package/src/composables/useFileSortMode.ts +24 -0
  234. package/src/composables/useFileTree.ts +85 -0
  235. package/src/composables/useFreshPluginData.ts +89 -0
  236. package/src/composables/useHealth.ts +38 -0
  237. package/src/composables/useImeAwareEnter.ts +57 -0
  238. package/src/composables/useKeyNavigation.ts +60 -0
  239. package/src/composables/useMarkdownLinkHandler.ts +46 -0
  240. package/src/composables/useMarkdownMode.ts +17 -0
  241. package/src/composables/useMcpTools.ts +71 -0
  242. package/src/composables/useMergedSessions.ts +27 -0
  243. package/src/composables/useNotifications.ts +90 -0
  244. package/src/composables/usePdfDownload.ts +60 -0
  245. package/src/composables/usePendingCalls.ts +77 -0
  246. package/src/composables/usePubSub.ts +85 -0
  247. package/src/composables/useRightSidebar.ts +23 -0
  248. package/src/composables/useRoles.ts +34 -0
  249. package/src/composables/useSandboxStatus.ts +67 -0
  250. package/src/composables/useSelectedResult.ts +49 -0
  251. package/src/composables/useSessionDerived.ts +51 -0
  252. package/src/composables/useSessionHistory.ts +81 -0
  253. package/src/composables/useSessionSync.ts +57 -0
  254. package/src/composables/useViewLayout.ts +55 -0
  255. package/src/config/apiRoutes.ts +173 -0
  256. package/src/config/pubsubChannels.ts +45 -0
  257. package/src/config/roles.ts +335 -0
  258. package/src/config/schedulerActions.ts +25 -0
  259. package/src/config/toolNames.ts +71 -0
  260. package/src/config/workspacePaths.ts +24 -0
  261. package/src/index.css +107 -0
  262. package/src/main.ts +25 -0
  263. package/src/plugins/canvas/Preview.vue +13 -0
  264. package/src/plugins/canvas/View.vue +333 -0
  265. package/src/plugins/canvas/definition.ts +38 -0
  266. package/src/plugins/canvas/index.ts +36 -0
  267. package/src/plugins/chart/Preview.vue +49 -0
  268. package/src/plugins/chart/View.vue +143 -0
  269. package/src/plugins/chart/definition.ts +58 -0
  270. package/src/plugins/chart/index.ts +52 -0
  271. package/src/plugins/editImage/Preview.vue +13 -0
  272. package/src/plugins/editImage/View.vue +13 -0
  273. package/src/plugins/editImage/definition.ts +27 -0
  274. package/src/plugins/editImage/index.ts +36 -0
  275. package/src/plugins/generateImage/Preview.vue +13 -0
  276. package/src/plugins/generateImage/View.vue +33 -0
  277. package/src/plugins/generateImage/definition.ts +32 -0
  278. package/src/plugins/generateImage/index.ts +56 -0
  279. package/src/plugins/manageRoles/Preview.vue +49 -0
  280. package/src/plugins/manageRoles/View.vue +525 -0
  281. package/src/plugins/manageRoles/definition.ts +43 -0
  282. package/src/plugins/manageRoles/index.ts +47 -0
  283. package/src/plugins/manageSkills/Preview.vue +21 -0
  284. package/src/plugins/manageSkills/View.vue +321 -0
  285. package/src/plugins/manageSkills/definition.ts +49 -0
  286. package/src/plugins/manageSkills/index.ts +49 -0
  287. package/src/plugins/manageSource/Preview.vue +33 -0
  288. package/src/plugins/manageSource/View.vue +697 -0
  289. package/src/plugins/manageSource/definition.ts +63 -0
  290. package/src/plugins/manageSource/index.ts +66 -0
  291. package/src/plugins/markdown/Preview.vue +77 -0
  292. package/src/plugins/markdown/View.vue +476 -0
  293. package/src/plugins/markdown/definition.ts +50 -0
  294. package/src/plugins/markdown/index.ts +36 -0
  295. package/src/plugins/presentHtml/Preview.vue +25 -0
  296. package/src/plugins/presentHtml/View.vue +52 -0
  297. package/src/plugins/presentHtml/definition.ts +27 -0
  298. package/src/plugins/presentHtml/helpers.ts +72 -0
  299. package/src/plugins/presentHtml/index.ts +41 -0
  300. package/src/plugins/presentMulmoScript/Preview.vue +23 -0
  301. package/src/plugins/presentMulmoScript/View.vue +1166 -0
  302. package/src/plugins/presentMulmoScript/definition.ts +95 -0
  303. package/src/plugins/presentMulmoScript/helpers.ts +162 -0
  304. package/src/plugins/presentMulmoScript/index.ts +40 -0
  305. package/src/plugins/scheduler/Preview.vue +67 -0
  306. package/src/plugins/scheduler/TasksTab.vue +205 -0
  307. package/src/plugins/scheduler/View.vue +565 -0
  308. package/src/plugins/scheduler/definition.ts +57 -0
  309. package/src/plugins/scheduler/index.ts +45 -0
  310. package/src/plugins/scheduler/viewModes.ts +26 -0
  311. package/src/plugins/spreadsheet/Preview.vue +29 -0
  312. package/src/plugins/spreadsheet/View.vue +997 -0
  313. package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
  314. package/src/plugins/spreadsheet/definition.ts +121 -0
  315. package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
  316. package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
  317. package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
  318. package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
  319. package/src/plugins/spreadsheet/engine/engine.ts +176 -0
  320. package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
  321. package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
  322. package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
  323. package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
  324. package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
  325. package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
  326. package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
  327. package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
  328. package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
  329. package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
  330. package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
  331. package/src/plugins/spreadsheet/engine/index.ts +27 -0
  332. package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
  333. package/src/plugins/spreadsheet/engine/parser.ts +143 -0
  334. package/src/plugins/spreadsheet/engine/registry.ts +150 -0
  335. package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
  336. package/src/plugins/spreadsheet/engine/types.ts +64 -0
  337. package/src/plugins/spreadsheet/index.ts +36 -0
  338. package/src/plugins/textResponse/Preview.vue +94 -0
  339. package/src/plugins/textResponse/View.vue +503 -0
  340. package/src/plugins/textResponse/definition.ts +34 -0
  341. package/src/plugins/textResponse/index.ts +27 -0
  342. package/src/plugins/textResponse/plugin.ts +29 -0
  343. package/src/plugins/textResponse/samples.ts +97 -0
  344. package/src/plugins/textResponse/types.ts +11 -0
  345. package/src/plugins/todo/Preview.vue +63 -0
  346. package/src/plugins/todo/View.vue +364 -0
  347. package/src/plugins/todo/composables/useTodos.ts +177 -0
  348. package/src/plugins/todo/definition.ts +45 -0
  349. package/src/plugins/todo/index.ts +61 -0
  350. package/src/plugins/todo/labels.ts +163 -0
  351. package/src/plugins/todo/priority.ts +98 -0
  352. package/src/plugins/todo/viewModes.ts +19 -0
  353. package/src/plugins/ui-image/ImagePreview.vue +23 -0
  354. package/src/plugins/ui-image/ImageView.vue +34 -0
  355. package/src/plugins/ui-image/index.ts +3 -0
  356. package/src/plugins/ui-image/types.ts +4 -0
  357. package/src/plugins/wiki/Preview.vue +65 -0
  358. package/src/plugins/wiki/View.vue +342 -0
  359. package/src/plugins/wiki/definition.ts +25 -0
  360. package/src/plugins/wiki/helpers.ts +59 -0
  361. package/src/plugins/wiki/index.ts +52 -0
  362. package/src/router/guards.ts +61 -0
  363. package/src/router/index.ts +50 -0
  364. package/src/tools/index.ts +52 -0
  365. package/src/tools/types.ts +27 -0
  366. package/src/types/events.ts +16 -0
  367. package/src/types/fileTree.ts +13 -0
  368. package/src/types/notification.ts +67 -0
  369. package/src/types/session.ts +116 -0
  370. package/src/types/sse.ts +90 -0
  371. package/src/types/toolCallHistory.ts +13 -0
  372. package/src/utils/agent/eventDispatch.ts +74 -0
  373. package/src/utils/agent/request.ts +55 -0
  374. package/src/utils/agent/toolCalls.ts +62 -0
  375. package/src/utils/api.ts +218 -0
  376. package/src/utils/canvas/viewMode.ts +46 -0
  377. package/src/utils/dom/authTokenMeta.ts +20 -0
  378. package/src/utils/dom/clickOutside.ts +11 -0
  379. package/src/utils/dom/externalLink.ts +57 -0
  380. package/src/utils/dom/scrollable.ts +24 -0
  381. package/src/utils/errors.ts +11 -0
  382. package/src/utils/files/expandedDirs.ts +25 -0
  383. package/src/utils/files/filename.ts +12 -0
  384. package/src/utils/files/sortChildren.ts +20 -0
  385. package/src/utils/filesPreview/schedulerPreview.ts +38 -0
  386. package/src/utils/filesPreview/todoPreview.ts +40 -0
  387. package/src/utils/format/date.ts +85 -0
  388. package/src/utils/format/frontmatter.ts +80 -0
  389. package/src/utils/format/jsonSyntax.ts +109 -0
  390. package/src/utils/html/previewCsp.ts +65 -0
  391. package/src/utils/image/resolve.ts +8 -0
  392. package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
  393. package/src/utils/markdown/extractFirstH1.ts +39 -0
  394. package/src/utils/notification/dispatch.ts +22 -0
  395. package/src/utils/path/relativeLink.ts +130 -0
  396. package/src/utils/role/icon.ts +20 -0
  397. package/src/utils/role/merge.ts +10 -0
  398. package/src/utils/role/plugins.ts +12 -0
  399. package/src/utils/session/mergeSessions.ts +103 -0
  400. package/src/utils/session/seedRoleDefault.ts +35 -0
  401. package/src/utils/session/sessionEntries.ts +121 -0
  402. package/src/utils/session/sessionFactory.ts +22 -0
  403. package/src/utils/session/sessionHelpers.ts +99 -0
  404. package/src/utils/tools/dedup.ts +17 -0
  405. package/src/utils/tools/mcp.ts +33 -0
  406. package/src/utils/tools/pendingCalls.ts +16 -0
  407. package/src/utils/tools/result.ts +40 -0
  408. package/src/utils/types.ts +44 -0
@@ -0,0 +1,218 @@
1
+ // Single source of truth for Vue → MulmoClaude server HTTP calls.
2
+ //
3
+ // Before this module existed there were 56 scattered `fetch("/api/...")`
4
+ // calls across 29 files, each doing its own JSON serialization, its own
5
+ // `!res.ok` check, and its own ad-hoc error extraction. This made any
6
+ // cross-cutting concern — auth headers, error formatting, retry policy,
7
+ // logging — impossible to add without touching every call site.
8
+ //
9
+ // All HTTP traffic from the Vue app should now go through one of:
10
+ //
11
+ // apiGet<T>(path, query?)
12
+ // apiPost<T>(path, body?)
13
+ // apiPut<T>(path, body?)
14
+ // apiDelete<T>(path, body?)
15
+ // apiCall<T>(path, opts) ← generic, for methods not above
16
+ // apiFetchRaw(path, opts) ← when you need the raw Response
17
+ // (binary, streaming, etc.)
18
+ //
19
+ // Return type is a discriminated union `ApiResult<T>`:
20
+ //
21
+ // { ok: true, data: T }
22
+ // { ok: false, error: string, status: number }
23
+ //
24
+ // Callers pattern-match on `result.ok` — no more mixing try/catch with
25
+ // `!res.ok` branches. Network errors and HTTP errors surface through the
26
+ // same `{ ok: false }` shape.
27
+ //
28
+ // Future extension hooks (see #272 for auth token):
29
+ // - setAuthToken() populates a module-level token used by every call
30
+ // - interceptors could go here for logging, retry, metrics
31
+
32
+ import { errorMessage } from "./errors";
33
+ import { hasStringProp } from "./types";
34
+
35
+ // ── Auth token (populated by bootstrap; consumed by every call) ─────
36
+
37
+ let authToken: string | null = null;
38
+
39
+ /**
40
+ * Set the bearer token used on every API call. Call once during app
41
+ * bootstrap, typically after reading a `<meta>` tag or window global
42
+ * populated by the server.
43
+ */
44
+ export function setAuthToken(token: string | null): void {
45
+ authToken = token;
46
+ }
47
+
48
+ // ── Types ────────────────────────────────────────────────────────────
49
+
50
+ export type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string; status: number };
51
+
52
+ export type ApiQuery = Record<string, string | number | boolean | undefined>;
53
+
54
+ export interface ApiOptions {
55
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
56
+ /** JSON-serialized into the request body. Omit for GET/DELETE. */
57
+ body?: unknown;
58
+ /** Appended as a query string. `undefined` values are dropped. */
59
+ query?: ApiQuery;
60
+ /** AbortSignal — pass through to fetch. */
61
+ signal?: AbortSignal;
62
+ /**
63
+ * Extra headers. Content-Type is set automatically for JSON bodies;
64
+ * Authorization is injected from `authToken`.
65
+ */
66
+ headers?: Record<string, string>;
67
+ }
68
+
69
+ // Use Parameters<typeof fetch> rather than global DOM lib types so
70
+ // this module doesn't depend on DOM lib being in the ESLint globals.
71
+ type FetchInit = Parameters<typeof fetch>[1];
72
+ type FetchBody = NonNullable<FetchInit>["body"];
73
+
74
+ // ── Internals ────────────────────────────────────────────────────────
75
+
76
+ function buildQueryString(query: ApiQuery | undefined): string {
77
+ if (!query) return "";
78
+ const parts: string[] = [];
79
+ for (const [key, value] of Object.entries(query)) {
80
+ if (value === undefined) continue;
81
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
82
+ }
83
+ return parts.length === 0 ? "" : `?${parts.join("&")}`;
84
+ }
85
+
86
+ function buildHeaders(opts: { headers?: Record<string, string> }, hasBody: boolean): Record<string, string> {
87
+ const headers: Record<string, string> = { ...(opts.headers ?? {}) };
88
+ if (hasBody && headers["Content-Type"] === undefined) {
89
+ headers["Content-Type"] = "application/json";
90
+ }
91
+ if (authToken && headers["Authorization"] === undefined) {
92
+ headers["Authorization"] = `Bearer ${authToken}`;
93
+ }
94
+ return headers;
95
+ }
96
+
97
+ async function extractError(res: Response): Promise<{ error: string; status: number }> {
98
+ const status = res.status;
99
+ // Try to parse a `{ error: string }` body first — that's the server's
100
+ // standard error shape. `in` narrowing lets us read `body.error`
101
+ // without any type assertion.
102
+ try {
103
+ const body: unknown = await res.clone().json();
104
+ if (hasStringProp(body, "error")) {
105
+ return { error: body.error, status };
106
+ }
107
+ } catch {
108
+ // Body wasn't JSON — fall through.
109
+ }
110
+ return {
111
+ error: res.statusText || `Request failed (${status})`,
112
+ status,
113
+ };
114
+ }
115
+
116
+ // ── Core call ───────────────────────────────────────────────────────
117
+
118
+ /**
119
+ * Generic HTTP call. Returns a discriminated union on success vs
120
+ * failure. Network errors are caught and surfaced as
121
+ * `{ ok: false, status: 0 }`. Assumes JSON response bodies (all
122
+ * MulmoClaude `/api/*` endpoints return JSON on success); use
123
+ * `apiFetchRaw` for binary / streaming / non-JSON responses.
124
+ */
125
+ export async function apiCall<T = unknown>(path: string, opts: ApiOptions = {}): Promise<ApiResult<T>> {
126
+ const method = opts.method ?? "GET";
127
+ const hasBody = opts.body !== undefined;
128
+ const url = `${path}${buildQueryString(opts.query)}`;
129
+
130
+ const init: FetchInit = {
131
+ method,
132
+ headers: buildHeaders(opts, hasBody),
133
+ signal: opts.signal,
134
+ };
135
+ if (hasBody) {
136
+ init.body = JSON.stringify(opts.body);
137
+ }
138
+
139
+ let res: Response;
140
+ try {
141
+ res = await fetch(url, init);
142
+ } catch (err) {
143
+ return {
144
+ ok: false,
145
+ error: errorMessage(err),
146
+ status: 0,
147
+ };
148
+ }
149
+
150
+ if (!res.ok) {
151
+ const { error, status } = await extractError(res);
152
+ return { ok: false, error, status };
153
+ }
154
+
155
+ // `res.json()` returns `Promise<any>`, which is assignable to T
156
+ // without a cast.
157
+ try {
158
+ const data: T = await res.json();
159
+ return { ok: true, data };
160
+ } catch (err) {
161
+ return {
162
+ ok: false,
163
+ error: `Invalid JSON response: ${errorMessage(err)}`,
164
+ status: res.status,
165
+ };
166
+ }
167
+ }
168
+
169
+ // ── Convenience verbs ───────────────────────────────────────────────
170
+
171
+ export function apiGet<T = unknown>(path: string, query?: ApiQuery, extra: Omit<ApiOptions, "method" | "body" | "query"> = {}): Promise<ApiResult<T>> {
172
+ return apiCall<T>(path, { ...extra, method: "GET", query });
173
+ }
174
+
175
+ export function apiPost<T = unknown>(path: string, body?: unknown, extra: Omit<ApiOptions, "method" | "body"> = {}): Promise<ApiResult<T>> {
176
+ return apiCall<T>(path, { ...extra, method: "POST", body });
177
+ }
178
+
179
+ export function apiPut<T = unknown>(path: string, body?: unknown, extra: Omit<ApiOptions, "method" | "body"> = {}): Promise<ApiResult<T>> {
180
+ return apiCall<T>(path, { ...extra, method: "PUT", body });
181
+ }
182
+
183
+ export function apiPatch<T = unknown>(path: string, body?: unknown, extra: Omit<ApiOptions, "method" | "body"> = {}): Promise<ApiResult<T>> {
184
+ return apiCall<T>(path, { ...extra, method: "PATCH", body });
185
+ }
186
+
187
+ export function apiDelete<T = unknown>(path: string, body?: unknown, extra: Omit<ApiOptions, "method" | "body"> = {}): Promise<ApiResult<T>> {
188
+ return apiCall<T>(path, { ...extra, method: "DELETE", body });
189
+ }
190
+
191
+ // ── Raw Response escape hatch ───────────────────────────────────────
192
+
193
+ export interface RawOptions {
194
+ method?: string;
195
+ /** Accepts any value fetch accepts (string / Blob / FormData / …). */
196
+ body?: FetchBody;
197
+ headers?: Record<string, string>;
198
+ signal?: AbortSignal;
199
+ query?: ApiQuery;
200
+ }
201
+
202
+ /**
203
+ * Escape hatch for endpoints returning binary / streaming / non-JSON
204
+ * bodies (PDF download, audio blob, SSE, etc.). Auth header is still
205
+ * applied; other handling is the caller's responsibility.
206
+ *
207
+ * Throws on network errors. Does NOT check `res.ok`.
208
+ */
209
+ export async function apiFetchRaw(path: string, opts: RawOptions = {}): Promise<Response> {
210
+ const url = `${path}${buildQueryString(opts.query)}`;
211
+ const init: FetchInit = {
212
+ method: opts.method ?? "GET",
213
+ headers: buildHeaders(opts, false),
214
+ body: opts.body,
215
+ signal: opts.signal,
216
+ };
217
+ return fetch(url, init);
218
+ }
@@ -0,0 +1,46 @@
1
+ // Pure helpers for the canvas view mode.
2
+ // The type also lives here, so test files and composables can
3
+ // import it without pulling in a .vue file.
4
+ //
5
+ // To add a new view mode, add it to CANVAS_VIEW below in shortcut order.
6
+ // Everything else (type, set, parser, shortcut) derives automatically.
7
+
8
+ export const CANVAS_VIEW = {
9
+ single: "single",
10
+ stack: "stack",
11
+ files: "files",
12
+ todos: "todos",
13
+ scheduler: "scheduler",
14
+ wiki: "wiki",
15
+ skills: "skills",
16
+ roles: "roles",
17
+ } as const;
18
+
19
+ const VIEW_MODES = Object.values(CANVAS_VIEW);
20
+
21
+ export type CanvasViewMode = (typeof CANVAS_VIEW)[keyof typeof CANVAS_VIEW];
22
+
23
+ export function isCanvasViewMode(value: string): value is CanvasViewMode {
24
+ return (VIEW_MODES as readonly string[]).includes(value);
25
+ }
26
+
27
+ /** All valid view mode values — single source of truth for guards and parsers. */
28
+ export const VALID_VIEW_MODES: ReadonlySet<string> = new Set(VIEW_MODES);
29
+
30
+ export const VIEW_MODE_STORAGE_KEY = "canvas_view_mode";
31
+
32
+ // Parse a value pulled out of localStorage. Anything other than the
33
+ // known modes — including null — falls back to "single".
34
+ export function parseStoredViewMode(stored: string | null): CanvasViewMode {
35
+ if (typeof stored === "string" && isCanvasViewMode(stored)) {
36
+ return stored;
37
+ }
38
+ return CANVAS_VIEW.single;
39
+ }
40
+
41
+ // Map a Cmd/Ctrl + N keyboard shortcut digit to its view mode.
42
+ // Shortcut keys are 1-indexed into the VIEW_MODES array.
43
+ export function viewModeForShortcutKey(key: string): CanvasViewMode | null {
44
+ const index = Number(key) - 1;
45
+ return VIEW_MODES[index] ?? null;
46
+ }
@@ -0,0 +1,20 @@
1
+ // Read the bearer auth token that the server embeds into the
2
+ // `<meta name="mulmoclaude-auth" content="…">` tag of index.html (#272).
3
+ // Isolated in this tiny DOM-scoped module so it lives under
4
+ // `src/utils/dom/` where ESLint already configures browser globals —
5
+ // avoids promoting `src/main.ts` into the browser-globals override.
6
+ //
7
+ // Returns `null` when:
8
+ // - the meta tag is missing (placeholder never injected — shouldn't
9
+ // happen in production but guards against it)
10
+ // - the content attribute is empty (server embedded empty = no token
11
+ // available; every subsequent API call will 401, which is the
12
+ // correct dev-time signal)
13
+
14
+ export function readAuthTokenFromMeta(): string | null {
15
+ const meta = document.querySelector('meta[name="mulmoclaude-auth"]');
16
+ if (meta === null) return null;
17
+ const content = meta.getAttribute("content");
18
+ if (content === null || content === "") return null;
19
+ return content;
20
+ }
@@ -0,0 +1,11 @@
1
+ // Pure helper for "did this click happen outside both the trigger
2
+ // button and the popup body?" — used by every dismiss-on-outside
3
+ // popup. Lifted out so the boolean rule can be unit-tested without
4
+ // a real DOM.
5
+
6
+ export function isClickOutside(target: Node | null, buttonEl: HTMLElement | null, popupEl: HTMLElement | null): boolean {
7
+ if (!target) return false;
8
+ const insideButton = buttonEl?.contains(target) ?? false;
9
+ const insidePopup = popupEl?.contains(target) ?? false;
10
+ return !insideButton && !insidePopup;
11
+ }
@@ -0,0 +1,57 @@
1
+ // Click handler for rendered markdown / HTML bodies that opens
2
+ // external (cross-origin) http(s) links in a new tab instead of
3
+ // navigating the SPA away from itself.
4
+ //
5
+ // Split into a pure predicate (`isCrossOriginHttpUrl`) that's
6
+ // exhaustively unit-tested, and a thin DOM wrapper
7
+ // (`handleExternalLinkClick`) that reads the click event. Callers
8
+ // invoke the wrapper from their own `@click` handler and check the
9
+ // return value to decide whether to fall through to plugin-specific
10
+ // navigation.
11
+
12
+ // Pure predicate: is `href` an absolute http(s) URL pointing at an
13
+ // origin different from `currentOrigin`? Used by
14
+ // `handleExternalLinkClick` below, and directly by tests.
15
+ //
16
+ // Returns `false` for:
17
+ // - non-http schemes (mailto:, tel:, javascript:, file: …) — the
18
+ // browser's default behaviour is appropriate for those
19
+ // - same-origin URLs (including hash anchors resolved against the
20
+ // current page, which `anchor.href` normalises to a full URL)
21
+ // - malformed input that `URL` can't parse
22
+ export function isCrossOriginHttpUrl(href: string, currentOrigin: string): boolean {
23
+ if (!href.startsWith("http://") && !href.startsWith("https://")) {
24
+ return false;
25
+ }
26
+ try {
27
+ return new URL(href).origin !== currentOrigin;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ // DOM click handler. Invoke from a view's `@click` listener on a
34
+ // rendered-markdown container. If the event targets an external
35
+ // http(s) link, the default navigation is cancelled and the link
36
+ // opens in a new tab with `noopener,noreferrer`; returns `true` so
37
+ // the caller knows the click was consumed. Returns `false` for
38
+ // every other case (not an anchor, internal link, modifier-key
39
+ // click, non-left-button, …) so the caller can continue with its
40
+ // own plugin-specific click handling (e.g. wiki internal links).
41
+ export function handleExternalLinkClick(event: MouseEvent): boolean {
42
+ if (event.button !== 0) return false;
43
+ if (event.ctrlKey || event.metaKey || event.shiftKey) return false;
44
+ const target = event.target as HTMLElement | null;
45
+ if (!target) return false;
46
+ const anchor = target.closest("a");
47
+ if (!anchor) return false;
48
+ // `.href` (DOM property) is always a fully-resolved URL; contrast
49
+ // `getAttribute("href")` which returns the raw attribute string.
50
+ // Using the resolved form gives us reliable origin checks and
51
+ // normalises relative paths away.
52
+ const url = anchor.href;
53
+ if (!isCrossOriginHttpUrl(url, window.location.origin)) return false;
54
+ event.preventDefault();
55
+ window.open(url, "_blank", "noopener,noreferrer");
56
+ return true;
57
+ }
@@ -0,0 +1,24 @@
1
+ // Small DOM helpers shared across components.
2
+
3
+ // Walk a container's descendants and return the first one that
4
+ // has both more vertical content than its visible height AND a
5
+ // CSS overflow that allows scrolling. Used so canvas-level arrow
6
+ // keys can scroll whichever inner element actually owns the
7
+ // scrollbar (e.g. a plugin's view component).
8
+ //
9
+ // Pure in the "no Vue / no module state" sense — it does touch the
10
+ // DOM, so its tests use a synthetic element graph rather than the
11
+ // real DOM.
12
+ export function findScrollableChild(container: HTMLElement): HTMLElement | null {
13
+ const children = container.querySelectorAll("*");
14
+ for (const el of children) {
15
+ const html = el as HTMLElement;
16
+ if (html.scrollHeight > html.clientHeight) {
17
+ const style = getComputedStyle(html);
18
+ if (style.overflowY === "auto" || style.overflowY === "scroll" || style.overflow === "auto" || style.overflow === "scroll") {
19
+ return html;
20
+ }
21
+ }
22
+ }
23
+ return null;
24
+ }
@@ -0,0 +1,11 @@
1
+ // Shared error helpers for the Vue side. Mirrors the server-side
2
+ // `server/utils/errors.ts` so the same helper is available wherever
3
+ // we handle caught exceptions.
4
+ //
5
+ // Use `errorMessage(err)` instead of inlining
6
+ // `err instanceof Error ? err.message : String(err)` — searching for
7
+ // one canonical helper is easier than grepping for the inline form.
8
+
9
+ export function errorMessage(err: unknown): string {
10
+ return err instanceof Error ? err.message : String(err);
11
+ }
@@ -0,0 +1,25 @@
1
+ // Pure helpers for persisting the FileTree expand/collapse state.
2
+ // Kept Vue-free so the parsing rules are unit-testable in isolation.
3
+
4
+ export const EXPANDED_DIRS_STORAGE_KEY = "files_expanded_dirs";
5
+
6
+ // Default: only the workspace root ("") is expanded — matches the
7
+ // pre-persistence behavior of FileTree.vue, where nested dirs start
8
+ // collapsed so opening Files mode doesn't render the whole tree.
9
+ const DEFAULT_EXPANDED: ReadonlyArray<string> = [""];
10
+
11
+ export function parseStoredExpandedDirs(raw: string | null): Set<string> {
12
+ if (raw === null) return new Set(DEFAULT_EXPANDED);
13
+ try {
14
+ const parsed: unknown = JSON.parse(raw);
15
+ if (!Array.isArray(parsed)) return new Set(DEFAULT_EXPANDED);
16
+ const strings = parsed.filter((v): v is string => typeof v === "string");
17
+ return new Set(strings);
18
+ } catch {
19
+ return new Set(DEFAULT_EXPANDED);
20
+ }
21
+ }
22
+
23
+ export function serializeExpandedDirs(set: Set<string>): string {
24
+ return JSON.stringify([...set]);
25
+ }
@@ -0,0 +1,12 @@
1
+ // Strip filesystem-hostile chars from a string so it can safely be used
2
+ // as a browser download filename across Windows / macOS / Linux. Not a
3
+ // full slugifier — server-side slugification lives in
4
+ // `server/utils/slug.ts` and is applied before data hits the client.
5
+ // This helper is the last-line defensive escape for plugin views that
6
+ // build a download filename from arbitrary title text.
7
+ const UNSAFE_FILENAME_CHARS = /[/\\:*?"<>|]/g;
8
+
9
+ export function toSafeFilename(name: string, fallback = "download"): string {
10
+ const cleaned = name.replace(UNSAFE_FILENAME_CHARS, "_").trim();
11
+ return cleaned || fallback;
12
+ }
@@ -0,0 +1,20 @@
1
+ import type { TreeNode } from "../../types/fileTree";
2
+ import type { FileSortMode } from "../../composables/useFileSortMode";
3
+
4
+ // Sort tree children: directories always come before files; within
5
+ // each group, "name" is locale-aware alphabetical and "recent" is
6
+ // newest-first by modifiedMs (missing mtimes sort last, then tie-break
7
+ // on name so the order is deterministic).
8
+ export function sortChildren(children: readonly TreeNode[], mode: FileSortMode): TreeNode[] {
9
+ const copy = children.slice();
10
+ copy.sort((a, b) => {
11
+ if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
12
+ if (mode === "recent") {
13
+ const am = a.modifiedMs ?? -Infinity;
14
+ const bm = b.modifiedMs ?? -Infinity;
15
+ if (am !== bm) return bm - am;
16
+ }
17
+ return a.name.localeCompare(b.name);
18
+ });
19
+ return copy;
20
+ }
@@ -0,0 +1,38 @@
1
+ // Synthesize a ToolResultComplete<SchedulerData> from raw scheduler
2
+ // items.json content so FilesView can render it with the scheduler
3
+ // plugin's calendar view. Extracted from FilesView.vue (#507 step 8).
4
+
5
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
6
+ import type { SchedulerData, ScheduledItem } from "../../plugins/scheduler/index";
7
+ import { WORKSPACE_FILES } from "../../config/workspacePaths";
8
+ import { isRecord } from "../types";
9
+
10
+ function isScheduledItem(value: unknown): value is ScheduledItem {
11
+ if (!isRecord(value)) return false;
12
+ if (typeof value.id !== "string") return false;
13
+ if (typeof value.title !== "string") return false;
14
+ return true;
15
+ }
16
+
17
+ function isScheduledItemArray(value: unknown): value is ScheduledItem[] {
18
+ return Array.isArray(value) && value.every(isScheduledItem);
19
+ }
20
+
21
+ export function toSchedulerResult(selectedPath: string | null, rawText: string | null): ToolResultComplete<SchedulerData> | null {
22
+ if (selectedPath !== WORKSPACE_FILES.schedulerItems) return null;
23
+ if (rawText === null) return null;
24
+ let parsed: unknown;
25
+ try {
26
+ parsed = JSON.parse(rawText);
27
+ } catch {
28
+ return null;
29
+ }
30
+ if (!isScheduledItemArray(parsed)) return null;
31
+ return {
32
+ uuid: "files-scheduler-preview",
33
+ toolName: "manageScheduler",
34
+ message: WORKSPACE_FILES.schedulerItems,
35
+ title: "Scheduler",
36
+ data: { items: parsed },
37
+ };
38
+ }
@@ -0,0 +1,40 @@
1
+ // Synthesize a ToolResultComplete<TodoData> from raw todos.json
2
+ // content so FilesView can render it with the TodoExplorer.
3
+ // Extracted from FilesView.vue (#507 step 8).
4
+
5
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
6
+ import type { StatusColumn, TodoData, TodoItem } from "../../plugins/todo/index";
7
+ import { WORKSPACE_FILES } from "../../config/workspacePaths";
8
+ import { isRecord } from "../types";
9
+
10
+ function isTodoItem(value: unknown): value is TodoItem {
11
+ if (!isRecord(value)) return false;
12
+ if (typeof value["id"] !== "string" || typeof value["text"] !== "string") return false;
13
+ if (typeof value["completed"] !== "boolean") return false;
14
+ if (typeof value["createdAt"] !== "number") return false;
15
+ return true;
16
+ }
17
+
18
+ function isTodoItemArray(value: unknown): value is TodoItem[] {
19
+ return Array.isArray(value) && value.every(isTodoItem);
20
+ }
21
+
22
+ export function toTodoExplorerResult(selectedPath: string | null, rawText: string | null): ToolResultComplete<TodoData> | null {
23
+ if (selectedPath !== WORKSPACE_FILES.todosItems) return null;
24
+ if (rawText === null) return null;
25
+ let parsed: unknown;
26
+ try {
27
+ parsed = JSON.parse(rawText);
28
+ } catch {
29
+ return null;
30
+ }
31
+ const items: TodoItem[] = isTodoItemArray(parsed) ? parsed : [];
32
+ const columns: StatusColumn[] = [];
33
+ return {
34
+ uuid: "files-todo-preview",
35
+ toolName: "manageTodoList",
36
+ message: WORKSPACE_FILES.todosItems,
37
+ title: "Todo",
38
+ data: { items, columns },
39
+ };
40
+ }
@@ -0,0 +1,85 @@
1
+ // Pure date/time formatting helpers for the Vue frontend.
2
+ // All functions are locale-aware on purpose; tests assert
3
+ // structural properties only, not exact strings.
4
+
5
+ /** "Apr 11 06:32" — short month + day + 24h time. */
6
+ export function formatDate(iso: string): string {
7
+ const date = new Date(iso);
8
+ return (
9
+ date.toLocaleDateString(undefined, { month: "short", day: "numeric" }) + " " + date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
10
+ );
11
+ }
12
+
13
+ /** "Apr 11 06:32" — same format as formatDate but from epoch ms. */
14
+ export function formatDateTime(epochMs: number): string {
15
+ return new Date(epochMs).toLocaleString(undefined, {
16
+ month: "short",
17
+ day: "numeric",
18
+ hour: "2-digit",
19
+ minute: "2-digit",
20
+ });
21
+ }
22
+
23
+ /** "06:32:15" — locale time string from epoch ms. */
24
+ export function formatTime(epochMs: number): string {
25
+ return new Date(epochMs).toLocaleTimeString();
26
+ }
27
+
28
+ /** "06:32" — short HH:MM. Accepts Date, epoch ms, or ISO string. */
29
+ export function formatShortTime(value: Date | number | string): string {
30
+ try {
31
+ const date = value instanceof Date ? value : new Date(value);
32
+ return date.toLocaleTimeString([], {
33
+ hour: "2-digit",
34
+ minute: "2-digit",
35
+ });
36
+ } catch {
37
+ return String(value);
38
+ }
39
+ }
40
+
41
+ /** "Apr 11" — short month + day. Accepts Date, epoch ms, or ISO string. */
42
+ export function formatShortDate(value: Date | number | string): string {
43
+ const date = value instanceof Date ? value : new Date(value);
44
+ return date.toLocaleDateString(undefined, {
45
+ month: "short",
46
+ day: "numeric",
47
+ });
48
+ }
49
+
50
+ /** True when two Dates fall on the same calendar day. */
51
+ export function isSameDay(left: Date, right: Date): boolean {
52
+ return left.getFullYear() === right.getFullYear() && left.getMonth() === right.getMonth() && left.getDate() === right.getDate();
53
+ }
54
+
55
+ /** True when the given Date is today. */
56
+ export function isToday(date: Date): boolean {
57
+ return isSameDay(date, new Date());
58
+ }
59
+
60
+ /** "14:32" for today, "Apr 16 14:32" for past dates. Works with
61
+ * both epoch ms (number) and ISO strings. */
62
+ export function formatSmartTime(value: number | string): string {
63
+ const date = new Date(value);
64
+ const time = formatShortTime(date);
65
+ if (isToday(date)) return time;
66
+ return `${formatShortDate(date)} ${time}`;
67
+ }
68
+
69
+ const ONE_MINUTE = 60_000;
70
+ const ONE_HOUR = 3_600_000;
71
+ const ONE_DAY = 86_400_000;
72
+
73
+ /** "just now", "5m ago", "2h ago", "Apr 11" — relative time from ISO string. */
74
+ export function formatRelativeTime(iso: string): string {
75
+ try {
76
+ const date = new Date(iso);
77
+ const diffMs = Date.now() - date.getTime();
78
+ if (diffMs < ONE_MINUTE) return "just now";
79
+ if (diffMs < ONE_HOUR) return `${Math.floor(diffMs / ONE_MINUTE)}m ago`;
80
+ if (diffMs < ONE_DAY) return `${Math.floor(diffMs / ONE_HOUR)}h ago`;
81
+ return formatShortDate(date);
82
+ } catch {
83
+ return iso;
84
+ }
85
+ }