mulmoclaude 0.5.2 → 0.6.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 (486) hide show
  1. package/Dockerfile.sandbox +100 -0
  2. package/README.md +17 -4
  3. package/bin/mulmoclaude.js +46 -15
  4. package/bin/prepare-dist.js +18 -2
  5. package/client/assets/chunk-CernVdwh.js +1 -0
  6. package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
  7. package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
  8. package/client/assets/index-BbgSjFQ8.js +4968 -0
  9. package/client/assets/index-ECD0lgIv.css +2 -0
  10. package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
  11. package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
  12. package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
  13. package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
  14. package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
  15. package/client/assets/vue-1e_vz2LW.js +1 -0
  16. package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
  17. package/client/index.html +33 -2
  18. package/package.json +20 -18
  19. package/sandbox-entrypoint.sh +106 -0
  20. package/server/accounting/accountNormalize.ts +32 -0
  21. package/server/accounting/defaultAccounts.ts +87 -0
  22. package/server/accounting/eventPublisher.ts +51 -0
  23. package/server/accounting/journal.ts +252 -0
  24. package/server/accounting/openingBalances.ts +114 -0
  25. package/server/accounting/report.ts +237 -0
  26. package/server/accounting/service.ts +718 -0
  27. package/server/accounting/snapshotCache.ts +333 -0
  28. package/server/accounting/timeSeries.ts +265 -0
  29. package/server/accounting/types.ts +148 -0
  30. package/server/agent/activeTools.ts +128 -0
  31. package/server/agent/attachmentConverter.ts +10 -5
  32. package/server/agent/backend/claude-code.ts +8 -2
  33. package/server/agent/backend/types.ts +1 -1
  34. package/server/agent/config.ts +101 -31
  35. package/server/agent/index.ts +45 -33
  36. package/server/agent/mcp-server.ts +146 -69
  37. package/server/agent/mcp-tools/index.ts +1 -5
  38. package/server/agent/mcp-tools/notify.ts +2 -22
  39. package/server/agent/mcp-tools/x.ts +0 -4
  40. package/server/agent/mcpHealth.ts +168 -0
  41. package/server/agent/plugin-names.ts +20 -77
  42. package/server/agent/prompt.ts +259 -51
  43. package/server/agent/resumeFailover.ts +1 -1
  44. package/server/agent/stream.ts +0 -1
  45. package/server/api/auth/bearerAuth.ts +5 -5
  46. package/server/api/csrfGuard.ts +1 -1
  47. package/server/api/routes/accounting.ts +366 -0
  48. package/server/api/routes/agent.ts +509 -46
  49. package/server/api/routes/attachment.ts +104 -0
  50. package/server/api/routes/chart.ts +2 -1
  51. package/server/api/routes/config.ts +12 -12
  52. package/server/api/routes/files.ts +105 -48
  53. package/server/api/routes/image.ts +70 -25
  54. package/server/api/routes/journal.ts +35 -0
  55. package/server/api/routes/mulmo-script.ts +358 -118
  56. package/server/api/routes/mulmoScriptValidate.ts +1 -1
  57. package/server/api/routes/news.ts +1 -1
  58. package/server/api/routes/notifications.ts +92 -22
  59. package/server/api/routes/notifier.ts +98 -0
  60. package/server/api/routes/pdf.ts +188 -48
  61. package/server/api/routes/plugins.ts +34 -14
  62. package/server/api/routes/presentHtml.ts +58 -3
  63. package/server/api/routes/roles.ts +1 -8
  64. package/server/api/routes/runtime-plugin.ts +224 -0
  65. package/server/api/routes/scheduler.ts +7 -5
  66. package/server/api/routes/schedulerHandlers.ts +1 -1
  67. package/server/api/routes/schedulerTasks.ts +8 -7
  68. package/server/api/routes/sessions.ts +234 -121
  69. package/server/api/routes/skills.ts +56 -51
  70. package/server/api/routes/sources.ts +52 -45
  71. package/server/api/routes/translation.ts +44 -0
  72. package/server/api/routes/wiki/frontmatter.ts +13 -65
  73. package/server/api/routes/wiki/history.ts +261 -0
  74. package/server/api/routes/wiki/pageIndex.ts +1 -1
  75. package/server/api/routes/wiki.ts +50 -26
  76. package/server/events/file-change.ts +83 -0
  77. package/server/events/notifications.ts +247 -91
  78. package/server/events/pub-sub/index.ts +1 -1
  79. package/server/events/relay-client.ts +5 -5
  80. package/server/events/scheduler-adapter.ts +2 -2
  81. package/server/events/session-store/index.ts +110 -22
  82. package/server/events/task-manager/index.ts +10 -9
  83. package/server/index.ts +509 -33
  84. package/server/notifier/engine.ts +419 -0
  85. package/server/notifier/legacy-adapters.ts +76 -0
  86. package/server/notifier/runtime-api.ts +74 -0
  87. package/server/notifier/store.ts +70 -0
  88. package/server/notifier/types.ts +121 -0
  89. package/server/plugins/dev-loader.ts +171 -0
  90. package/server/plugins/dev-watcher.ts +150 -0
  91. package/server/plugins/diagnostics.ts +188 -0
  92. package/server/plugins/preset-list.ts +52 -0
  93. package/server/plugins/preset-loader.ts +112 -0
  94. package/server/plugins/runtime-chat-api.ts +38 -0
  95. package/server/plugins/runtime-loader.ts +430 -0
  96. package/server/plugins/runtime-registry.ts +112 -0
  97. package/server/plugins/runtime-tasks-api.ts +50 -0
  98. package/server/plugins/runtime.ts +378 -0
  99. package/server/services/translation/cache.ts +72 -0
  100. package/server/services/translation/index.ts +106 -0
  101. package/server/services/translation/llm.ts +140 -0
  102. package/server/services/translation/types.ts +35 -0
  103. package/server/system/credentials.ts +13 -2
  104. package/server/system/env.ts +6 -1
  105. package/server/system/logger/formatters.ts +46 -4
  106. package/server/system/logger/index.ts +4 -4
  107. package/server/system/logger/sinks.ts +26 -5
  108. package/server/system/logger/types.ts +2 -2
  109. package/server/utils/dev-plugin-args.d.mts +11 -0
  110. package/server/utils/dev-plugin-args.mjs +43 -0
  111. package/server/utils/errors.ts +13 -4
  112. package/server/utils/files/accounting-io.ts +295 -0
  113. package/server/utils/files/atomic.ts +17 -49
  114. package/server/utils/files/attachment-store.ts +182 -0
  115. package/server/utils/files/html-io.ts +1 -7
  116. package/server/utils/files/html-store.ts +19 -0
  117. package/server/utils/files/image-store.ts +20 -22
  118. package/server/utils/files/index.ts +5 -15
  119. package/server/utils/files/journal-io.ts +7 -35
  120. package/server/utils/files/json.ts +2 -29
  121. package/server/utils/files/markdown-image-fill.ts +6 -37
  122. package/server/utils/files/markdown-store.ts +6 -21
  123. package/server/utils/files/naming.ts +3 -39
  124. package/server/utils/files/plugins-io.ts +100 -0
  125. package/server/utils/files/reference-dirs-io.ts +1 -9
  126. package/server/utils/files/roles-io.ts +2 -10
  127. package/server/utils/files/safe.ts +17 -19
  128. package/server/utils/files/scheduler-io.ts +1 -7
  129. package/server/utils/files/scheduler-overrides-io.ts +3 -12
  130. package/server/utils/files/session-io.ts +21 -30
  131. package/server/utils/files/spreadsheet-store.ts +9 -22
  132. package/server/utils/files/translation-io.ts +46 -0
  133. package/server/utils/files/user-tasks-io.ts +1 -7
  134. package/server/utils/files/workspace-io.ts +3 -79
  135. package/server/utils/gemini.ts +33 -11
  136. package/server/utils/html/htmlArtifactSplicer.ts +41 -0
  137. package/server/utils/markdown/frontmatter.ts +112 -0
  138. package/server/utils/regex.ts +56 -0
  139. package/server/utils/router.ts +41 -0
  140. package/server/utils/slug.ts +5 -3
  141. package/server/utils/time.ts +12 -0
  142. package/server/workspace/chat-index/indexer.ts +15 -2
  143. package/server/workspace/chat-index/summarizer.ts +1 -1
  144. package/server/workspace/custom-dirs.ts +1 -1
  145. package/server/workspace/helps/gemini.md +1 -1
  146. package/server/workspace/helps/guide.md +61 -0
  147. package/server/workspace/helps/index.md +4 -0
  148. package/server/workspace/helps/presenthtml.md +80 -0
  149. package/server/workspace/helps/sandbox.md +7 -0
  150. package/server/workspace/helps/storyteller.md +101 -0
  151. package/server/workspace/helps/telegram.md +1 -0
  152. package/server/workspace/helps/wiki.md +9 -7
  153. package/server/workspace/journal/archivist-cli.ts +7 -33
  154. package/server/workspace/journal/archivist-schemas.ts +5 -43
  155. package/server/workspace/journal/dailyPass.ts +34 -187
  156. package/server/workspace/journal/diff.ts +3 -28
  157. package/server/workspace/journal/index.ts +10 -81
  158. package/server/workspace/journal/indexFile.ts +3 -24
  159. package/server/workspace/journal/latestDaily.ts +51 -0
  160. package/server/workspace/journal/memoryExtractor.ts +4 -20
  161. package/server/workspace/journal/optimizationPass.ts +4 -21
  162. package/server/workspace/journal/paths.ts +4 -23
  163. package/server/workspace/journal/state.ts +6 -29
  164. package/server/workspace/memory/io.ts +213 -0
  165. package/server/workspace/memory/llm-classifier.ts +158 -0
  166. package/server/workspace/memory/migrate.ts +263 -0
  167. package/server/workspace/memory/run.ts +84 -0
  168. package/server/workspace/memory/topic-cluster.ts +218 -0
  169. package/server/workspace/memory/topic-detect.ts +67 -0
  170. package/server/workspace/memory/topic-index-hook.ts +128 -0
  171. package/server/workspace/memory/topic-io.ts +180 -0
  172. package/server/workspace/memory/topic-migrate.ts +248 -0
  173. package/server/workspace/memory/topic-run.ts +172 -0
  174. package/server/workspace/memory/topic-swap.ts +135 -0
  175. package/server/workspace/memory/topic-types.ts +142 -0
  176. package/server/workspace/memory/types.ts +83 -0
  177. package/server/workspace/news/reader.ts +4 -5
  178. package/server/workspace/paths.ts +124 -47
  179. package/server/workspace/roles.ts +2 -11
  180. package/server/workspace/skills/parser.ts +38 -55
  181. package/server/workspace/skills/user-tasks.ts +1 -2
  182. package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
  183. package/server/workspace/skills-preset.ts +196 -0
  184. package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
  185. package/server/workspace/sources/fetchers/index.ts +1 -1
  186. package/server/workspace/sources/fetchers/rssParser.ts +1 -1
  187. package/server/workspace/sources/pipeline/index.ts +2 -2
  188. package/server/workspace/sources/pipeline/notify.ts +3 -3
  189. package/server/workspace/sources/pipeline/write.ts +2 -2
  190. package/server/workspace/sources/registry.ts +39 -61
  191. package/server/workspace/sources/robots.ts +1 -1
  192. package/server/workspace/tool-trace/classify.ts +2 -1
  193. package/server/workspace/tool-trace/index.ts +1 -1
  194. package/server/workspace/tool-trace/writeSearch.ts +6 -1
  195. package/server/workspace/wiki-backlinks/index.ts +19 -7
  196. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
  197. package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
  198. package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
  199. package/server/workspace/wiki-history/provision.ts +181 -0
  200. package/server/workspace/wiki-pages/io.ts +217 -0
  201. package/server/workspace/wiki-pages/snapshot.ts +380 -0
  202. package/server/workspace/workspace.ts +75 -13
  203. package/src/App.vue +115 -40
  204. package/src/_runtime/protocol-vue.ts +21 -0
  205. package/src/_runtime/vue.ts +22 -0
  206. package/src/components/ChatInput.vue +14 -10
  207. package/src/components/CopyChatButton.vue +76 -0
  208. package/src/components/FileContentRenderer.vue +67 -14
  209. package/src/components/FileTree.vue +2 -2
  210. package/src/components/FilesView.vue +17 -1
  211. package/src/components/NewsView.vue +16 -2
  212. package/src/components/NotificationBell.vue +320 -93
  213. package/src/components/PageChatComposer.vue +5 -4
  214. package/src/components/PluginLauncher.vue +42 -6
  215. package/src/components/PluginScopedRoot.vue +87 -0
  216. package/src/components/RoleSelector.vue +12 -1
  217. package/src/components/RolesView.vue +562 -0
  218. package/src/components/SentAttachmentChip.vue +102 -0
  219. package/src/components/SessionHistoryPanel.vue +109 -20
  220. package/src/components/SessionRoleIcon.vue +7 -4
  221. package/src/components/SessionSidebar.vue +20 -7
  222. package/src/components/SessionTabBar.vue +1 -1
  223. package/src/components/SettingsMcpTab.vue +4 -4
  224. package/src/components/SettingsModal.vue +2 -0
  225. package/src/components/SidebarHeader.vue +16 -5
  226. package/src/components/SourcesManager.vue +23 -9
  227. package/src/components/SourcesView.vue +1 -1
  228. package/src/components/StackView.vue +102 -6
  229. package/src/components/SuggestionsPanel.vue +105 -16
  230. package/src/components/SystemFileBanner.vue +1 -1
  231. package/src/components/TodoExplorer.vue +4 -5
  232. package/src/components/todo/TodoAddDialog.vue +2 -3
  233. package/src/components/todo/TodoEditDialog.vue +1 -2
  234. package/src/components/todo/TodoEditPanel.vue +2 -3
  235. package/src/components/todo/TodoKanbanView.vue +8 -5
  236. package/src/components/todo/TodoListView.vue +3 -5
  237. package/src/components/todo/TodoTableView.vue +7 -5
  238. package/src/composables/useAccountingChannel.ts +58 -0
  239. package/src/composables/useActiveSession.ts +4 -25
  240. package/src/composables/useAppApi.ts +6 -44
  241. package/src/composables/useClipboardCopy.ts +3 -20
  242. package/src/composables/useContentDisplay.ts +33 -2
  243. package/src/composables/useDevPluginReload.ts +23 -0
  244. package/src/composables/useDynamicFavicon.ts +5 -31
  245. package/src/composables/useEventListeners.ts +0 -20
  246. package/src/composables/useExpandedDirs.ts +4 -15
  247. package/src/composables/useFaviconState.ts +12 -46
  248. package/src/composables/useFileChange.ts +53 -0
  249. package/src/composables/useFreshPluginData.ts +6 -43
  250. package/src/composables/useHealth.ts +14 -43
  251. package/src/composables/useImageErrorRepair.ts +104 -0
  252. package/src/composables/useLatestDaily.ts +40 -0
  253. package/src/composables/useMarkdownDoc.ts +39 -0
  254. package/src/composables/useMarkdownLinkHandler.ts +1 -1
  255. package/src/composables/useMcpTools.ts +3 -16
  256. package/src/composables/useNotifications.ts +138 -112
  257. package/src/composables/usePdfDownload.ts +17 -3
  258. package/src/composables/usePendingCalls.ts +8 -26
  259. package/src/composables/usePluginErrorBoundary.ts +68 -0
  260. package/src/composables/usePubSub.ts +9 -17
  261. package/src/composables/useRunElapsed.ts +5 -22
  262. package/src/composables/useSandboxStatus.ts +4 -20
  263. package/src/composables/useSessionDerived.ts +7 -15
  264. package/src/composables/useSessionHistory.ts +70 -29
  265. package/src/composables/useSessionSync.ts +25 -3
  266. package/src/composables/useSkillsList.ts +59 -0
  267. package/src/composables/useTranslatedQueries.ts +109 -0
  268. package/src/config/apiRoutes.ts +181 -80
  269. package/src/config/historyFilters.ts +5 -3
  270. package/src/config/hostEvents.ts +17 -0
  271. package/src/config/mcpCatalog.ts +277 -5
  272. package/src/config/pubsubChannels.ts +134 -12
  273. package/src/config/roles.ts +212 -147
  274. package/src/config/systemFileDescriptors.ts +5 -5
  275. package/src/config/toolNames.ts +52 -30
  276. package/src/config/workspacePaths.ts +26 -2
  277. package/src/lang/de.ts +483 -27
  278. package/src/lang/en.ts +448 -27
  279. package/src/lang/es.ts +474 -27
  280. package/src/lang/fr.ts +476 -27
  281. package/src/lang/ja.ts +465 -27
  282. package/src/lang/ko.ts +466 -27
  283. package/src/lang/pt-BR.ts +473 -27
  284. package/src/lang/zh.ts +463 -27
  285. package/src/lib/vue-i18n.ts +1 -1
  286. package/src/lib/wiki-page/slug.ts +66 -0
  287. package/src/main.ts +85 -0
  288. package/src/plugins/_extras.ts +58 -0
  289. package/src/plugins/_generated/metas.ts +42 -0
  290. package/src/plugins/_generated/registrations.ts +44 -0
  291. package/src/plugins/_generated/server-bindings.ts +47 -0
  292. package/src/plugins/accounting/Preview.vue +106 -0
  293. package/src/plugins/accounting/View.vue +632 -0
  294. package/src/plugins/accounting/actions.ts +34 -0
  295. package/src/plugins/accounting/api.ts +301 -0
  296. package/src/plugins/accounting/components/AccountEditor.vue +250 -0
  297. package/src/plugins/accounting/components/AccountRow.vue +50 -0
  298. package/src/plugins/accounting/components/AccountsList.vue +102 -0
  299. package/src/plugins/accounting/components/AccountsModal.vue +300 -0
  300. package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
  301. package/src/plugins/accounting/components/BookSettings.vue +284 -0
  302. package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
  303. package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
  304. package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
  305. package/src/plugins/accounting/components/JournalList.vue +553 -0
  306. package/src/plugins/accounting/components/Ledger.vue +206 -0
  307. package/src/plugins/accounting/components/NewBookForm.vue +211 -0
  308. package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
  309. package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
  310. package/src/plugins/accounting/components/accountDraft.ts +13 -0
  311. package/src/plugins/accounting/components/accountNumbering.ts +103 -0
  312. package/src/plugins/accounting/components/accountValidation.ts +75 -0
  313. package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
  314. package/src/plugins/accounting/countries.ts +158 -0
  315. package/src/plugins/accounting/currencies.ts +64 -0
  316. package/src/plugins/accounting/dates.ts +51 -0
  317. package/src/plugins/accounting/definition.ts +199 -0
  318. package/src/plugins/accounting/fiscalYear.ts +136 -0
  319. package/src/plugins/accounting/index.ts +49 -0
  320. package/src/plugins/accounting/meta.ts +91 -0
  321. package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
  322. package/src/plugins/api.ts +125 -0
  323. package/src/plugins/canvas/View.vue +38 -28
  324. package/src/plugins/canvas/definition.ts +10 -8
  325. package/src/plugins/canvas/index.ts +15 -8
  326. package/src/plugins/canvas/meta.ts +12 -0
  327. package/src/plugins/chart/Preview.vue +1 -1
  328. package/src/plugins/chart/View.vue +2 -2
  329. package/src/plugins/chart/definition.ts +12 -2
  330. package/src/plugins/chart/index.ts +15 -7
  331. package/src/plugins/chart/meta.ts +18 -0
  332. package/src/plugins/editImages/definition.ts +44 -0
  333. package/src/plugins/editImages/index.ts +43 -0
  334. package/src/plugins/editImages/meta.ts +5 -0
  335. package/src/plugins/generateImage/View.vue +3 -1
  336. package/src/plugins/generateImage/definition.ts +2 -0
  337. package/src/plugins/generateImage/index.ts +13 -5
  338. package/src/plugins/generateImage/meta.ts +5 -0
  339. package/src/plugins/index.ts +35 -0
  340. package/src/plugins/manageRoles/Preview.vue +7 -4
  341. package/src/plugins/manageRoles/View.vue +12 -8
  342. package/src/plugins/manageRoles/definition.ts +6 -0
  343. package/src/plugins/manageRoles/index.ts +7 -6
  344. package/src/plugins/manageSkills/View.vue +11 -7
  345. package/src/plugins/manageSkills/definition.ts +4 -1
  346. package/src/plugins/manageSkills/index.ts +14 -7
  347. package/src/plugins/manageSkills/meta.ts +21 -0
  348. package/src/plugins/manageSource/definition.ts +4 -1
  349. package/src/plugins/manageSource/index.ts +15 -7
  350. package/src/plugins/manageSource/meta.ts +21 -0
  351. package/src/plugins/markdown/Preview.vue +10 -8
  352. package/src/plugins/markdown/View.vue +84 -17
  353. package/src/plugins/markdown/definition.ts +7 -1
  354. package/src/plugins/markdown/index.ts +15 -8
  355. package/src/plugins/markdown/meta.ts +16 -0
  356. package/src/plugins/meta-types.ts +97 -0
  357. package/src/plugins/metas.ts +224 -0
  358. package/src/plugins/presentForm/Preview.vue +4 -15
  359. package/src/plugins/presentForm/View.vue +35 -78
  360. package/src/plugins/presentForm/definition.ts +7 -6
  361. package/src/plugins/presentForm/index.ts +12 -5
  362. package/src/plugins/presentForm/meta.ts +11 -0
  363. package/src/plugins/presentForm/plugin.ts +8 -9
  364. package/src/plugins/presentForm/types.ts +0 -24
  365. package/src/plugins/presentHtml/Preview.vue +1 -8
  366. package/src/plugins/presentHtml/View.vue +401 -30
  367. package/src/plugins/presentHtml/definition.ts +8 -5
  368. package/src/plugins/presentHtml/index.ts +15 -8
  369. package/src/plugins/presentHtml/meta.ts +14 -0
  370. package/src/plugins/presentMulmoScript/View.vue +327 -107
  371. package/src/plugins/presentMulmoScript/definition.ts +34 -7
  372. package/src/plugins/presentMulmoScript/helpers.ts +4 -5
  373. package/src/plugins/presentMulmoScript/index.ts +20 -7
  374. package/src/plugins/presentMulmoScript/meta.ts +52 -0
  375. package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
  376. package/src/plugins/scheduler/Preview.vue +5 -2
  377. package/src/plugins/scheduler/TasksTab.vue +16 -36
  378. package/src/plugins/scheduler/View.vue +22 -54
  379. package/src/plugins/scheduler/automationsDefinition.ts +14 -9
  380. package/src/plugins/scheduler/automationsMeta.ts +5 -0
  381. package/src/plugins/scheduler/calendarDefinition.ts +4 -7
  382. package/src/plugins/scheduler/calendarMeta.ts +28 -0
  383. package/src/plugins/scheduler/formatSchedule.ts +6 -24
  384. package/src/plugins/scheduler/index.ts +26 -52
  385. package/src/plugins/scope.ts +57 -0
  386. package/src/plugins/server-bindings-types.ts +38 -0
  387. package/src/plugins/server.ts +32 -0
  388. package/src/plugins/skill/Preview.vue +25 -0
  389. package/src/plugins/skill/View.vue +125 -0
  390. package/src/plugins/skill/definition.ts +23 -0
  391. package/src/plugins/skill/index.ts +36 -0
  392. package/src/plugins/skill/plugin.ts +31 -0
  393. package/src/plugins/skill/types.ts +21 -0
  394. package/src/plugins/spreadsheet/Preview.vue +1 -3
  395. package/src/plugins/spreadsheet/View.vue +29 -49
  396. package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
  397. package/src/plugins/spreadsheet/definition.ts +5 -2
  398. package/src/plugins/spreadsheet/index.ts +15 -8
  399. package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
  400. package/src/plugins/spreadsheet/meta.ts +14 -0
  401. package/src/plugins/textResponse/Preview.vue +9 -1
  402. package/src/plugins/textResponse/View.vue +59 -8
  403. package/src/plugins/textResponse/index.ts +11 -3
  404. package/src/plugins/textResponse/plugin.ts +8 -10
  405. package/src/plugins/textResponse/types.ts +28 -0
  406. package/src/plugins/wiki/Preview.vue +6 -4
  407. package/src/plugins/wiki/View.vue +463 -254
  408. package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
  409. package/src/plugins/wiki/helpers.ts +17 -0
  410. package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
  411. package/src/plugins/wiki/history/HistoryTab.vue +167 -0
  412. package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
  413. package/src/plugins/wiki/history/api.ts +52 -0
  414. package/src/plugins/wiki/history/diff.ts +145 -0
  415. package/src/plugins/wiki/index.ts +42 -32
  416. package/src/plugins/wiki/meta.ts +10 -0
  417. package/src/plugins/wiki/pageEditLoader.ts +53 -0
  418. package/src/plugins/wiki/route.ts +8 -0
  419. package/src/router/guards.ts +2 -1
  420. package/src/router/index.ts +19 -0
  421. package/src/router/pageRoutes.ts +1 -0
  422. package/src/tools/index.ts +50 -51
  423. package/src/tools/runtimeLoader.ts +141 -0
  424. package/src/tools/types.ts +44 -1
  425. package/src/types/notification.ts +23 -0
  426. package/src/types/pastedFile.ts +10 -0
  427. package/src/types/session.ts +61 -3
  428. package/src/types/sse.ts +21 -6
  429. package/src/utils/agent/eventDispatch.ts +12 -9
  430. package/src/utils/agent/pastedAttachment.ts +35 -0
  431. package/src/utils/agent/request.ts +32 -3
  432. package/src/utils/agent/toolCalls.ts +7 -1
  433. package/src/utils/api.ts +1 -1
  434. package/src/utils/chat/exportMarkdown.ts +243 -0
  435. package/src/utils/errors.ts +10 -2
  436. package/src/utils/files/expandedDirs.ts +1 -1
  437. package/src/utils/filesPreview/todoPreview.ts +13 -2
  438. package/src/utils/format/date.ts +1 -3
  439. package/src/utils/format/jsonSyntax.ts +5 -0
  440. package/src/utils/html/iframeHeightReporterScript.ts +62 -0
  441. package/src/utils/html/previewCsp.ts +29 -2
  442. package/src/utils/image/htmlSrcAttrs.ts +122 -0
  443. package/src/utils/image/imageRepairInlineScript.ts +115 -0
  444. package/src/utils/image/resolve.ts +17 -3
  445. package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
  446. package/src/utils/markdown/frontmatter.ts +125 -0
  447. package/src/utils/markdown/taskList.ts +7 -2
  448. package/src/utils/plugin/runtime.ts +132 -0
  449. package/src/utils/session/mergeSessions.ts +40 -37
  450. package/src/utils/session/sessionEntries.ts +74 -18
  451. package/src/utils/session/sessionHelpers.ts +54 -10
  452. package/src/utils/tools/result.ts +76 -14
  453. package/src/vite-env.d.ts +6 -0
  454. package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
  455. package/client/assets/index-CY-WpQUm.css +0 -2
  456. package/client/assets/index-DbTz2Mfs.js +0 -4911
  457. package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
  458. package/server/api/routes/html.ts +0 -114
  459. package/server/api/routes/todos.ts +0 -293
  460. package/server/api/routes/todosColumnsHandlers.ts +0 -333
  461. package/server/api/routes/todosHandlers.ts +0 -274
  462. package/server/api/routes/todosItemsHandlers.ts +0 -386
  463. package/server/utils/files/todos-io.ts +0 -29
  464. package/src/components/NotificationToast.vue +0 -75
  465. package/src/plugins/editImage/definition.ts +0 -27
  466. package/src/plugins/editImage/index.ts +0 -37
  467. package/src/plugins/presentHtml/helpers.ts +0 -72
  468. package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
  469. package/src/plugins/scheduler/legacyShape.ts +0 -34
  470. package/src/plugins/todo/Preview.vue +0 -68
  471. package/src/plugins/todo/View.vue +0 -378
  472. package/src/plugins/todo/composables/useTodos.ts +0 -179
  473. package/src/plugins/todo/definition.ts +0 -45
  474. package/src/plugins/todo/index.ts +0 -62
  475. package/src/plugins/todo/labels.ts +0 -163
  476. package/src/plugins/todo/priority.ts +0 -98
  477. package/src/plugins/todo/viewModes.ts +0 -19
  478. package/src/plugins/wiki/definition.ts +0 -25
  479. package/src/tools/legacyPluginNames.ts +0 -13
  480. package/src/utils/format/frontmatter.ts +0 -80
  481. package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
  482. package/src/utils/notification/dispatch.ts +0 -58
  483. /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
  484. /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
  485. /package/src/plugins/{editImage → editImages}/View.vue +0 -0
  486. /package/src/{config/schedulerActions.ts → plugins/scheduler/actions.ts} +0 -0
@@ -1,12 +1,6 @@
1
- // Domain I/O: custom roles
2
- // config/roles/<id>.json
3
- //
4
- // Optional `root` for test DI.
5
-
6
1
  import path from "node:path";
7
2
  import { mkdirSync, statSync, unlinkSync } from "node:fs";
8
- import { WORKSPACE_DIRS } from "../../workspace/paths.js";
9
- import { workspacePath } from "../../workspace/paths.js";
3
+ import { WORKSPACE_DIRS, workspacePath } from "../../workspace/paths.js";
10
4
  import { writeFileAtomicSync } from "./atomic.js";
11
5
  import { isEnoent } from "./safe.js";
12
6
 
@@ -16,7 +10,6 @@ function roleFilePath(roleId: string, workspaceRoot?: string): string {
16
10
  return path.join(root(workspaceRoot), WORKSPACE_DIRS.roles, `${roleId}.json`);
17
11
  }
18
12
 
19
- /** Check if a custom role file exists. */
20
13
  export function roleExists(roleId: string, workspaceRoot?: string): boolean {
21
14
  try {
22
15
  statSync(roleFilePath(roleId, workspaceRoot));
@@ -26,7 +19,7 @@ export function roleExists(roleId: string, workspaceRoot?: string): boolean {
26
19
  }
27
20
  }
28
21
 
29
- /** Delete a custom role file. Returns false if not found. */
22
+ // Returns false if not found.
30
23
  export function deleteRole(roleId: string, workspaceRoot?: string): boolean {
31
24
  try {
32
25
  unlinkSync(roleFilePath(roleId, workspaceRoot));
@@ -37,7 +30,6 @@ export function deleteRole(roleId: string, workspaceRoot?: string): boolean {
37
30
  }
38
31
  }
39
32
 
40
- /** Save (create or overwrite) a custom role file atomically. */
41
33
  export function saveRole(roleId: string, data: unknown, workspaceRoot?: string): void {
42
34
  const dir = path.join(root(workspaceRoot), WORKSPACE_DIRS.roles);
43
35
  mkdirSync(dir, { recursive: true });
@@ -1,22 +1,14 @@
1
- // Safe filesystem wrappers that swallow ENOENT / EACCES so callers
2
- // can do `if (result === null)` instead of try/catch boilerplate.
3
- //
4
- // `resolveWithinRoot` is the realpath-based path-traversal check
5
- // that underpins every endpoint serving files out of the workspace.
6
- //
7
- // Moved from server/utils/fs.ts (issue #366 Phase 1). The old
8
- // file re-exports these for backwards compat.
1
+ // Wrappers that swallow ENOENT/EACCES so callers branch on `result === null` instead of try/catch.
2
+ // resolveWithinRoot is the realpath-based traversal check used by every endpoint serving workspace files.
9
3
 
10
4
  import { Dirent, Stats, promises, readFileSync, readdirSync, realpathSync, statSync } from "fs";
11
5
  import path from "path";
12
6
  import { isErrorWithCode } from "../types.js";
13
7
 
14
- /** Check if an error is ENOENT (file/dir not found). */
15
8
  export function isEnoent(err: unknown): boolean {
16
9
  return isErrorWithCode(err) && err.code === "ENOENT";
17
10
  }
18
11
 
19
- /** Read a binary file by absolute path. Null on ENOENT. */
20
12
  export function readBinarySafeSync(absPath: string): Buffer | null {
21
13
  try {
22
14
  return readFileSync(absPath);
@@ -25,7 +17,6 @@ export function readBinarySafeSync(absPath: string): Buffer | null {
25
17
  }
26
18
  }
27
19
 
28
- /** Read a text file by absolute path (async). Null on ENOENT. */
29
20
  export async function readTextSafe(absPath: string): Promise<string | null> {
30
21
  try {
31
22
  return await promises.readFile(absPath, "utf-8");
@@ -34,7 +25,6 @@ export async function readTextSafe(absPath: string): Promise<string | null> {
34
25
  }
35
26
  }
36
27
 
37
- /** Read a text file by absolute path (sync). Null on ENOENT. */
38
28
  export function readTextSafeSync(absPath: string): string | null {
39
29
  try {
40
30
  return readFileSync(absPath, "utf-8");
@@ -83,13 +73,21 @@ export async function readTextOrNull(file: string): Promise<string | null> {
83
73
  }
84
74
  }
85
75
 
86
- /**
87
- * Resolve a relative path against a root, ensuring the result stays
88
- * inside the root after symlink resolution. Returns null on traversal
89
- * or if either path doesn't exist on disk.
90
- *
91
- * `rootReal` MUST already be a realpath.
92
- */
76
+ // True if any segment of `relPath` (split on either `/` or `\`)
77
+ // starts with a dot the same policy `express.static({ dotfiles:
78
+ // "deny" })` applies. Splits on both separators because
79
+ // `decodeURIComponent` of `%5C` produces a literal `\`, and on
80
+ // Windows `path.normalize` (used downstream by `resolveWithinRoot`)
81
+ // treats `\` as a separator. Without the dual split, a request like
82
+ // `/dir%5C.hidden.html` decodes to `dir\.hidden.html` → splits on
83
+ // `/` as one segment `dir\.hidden.html` (no leading dot) → bypasses
84
+ // the guard on Windows even though `path.normalize` later resolves
85
+ // it to `dir/.hidden.html`. (Codex review on PR #1082.)
86
+ export function containsDotfileSegment(relPath: string): boolean {
87
+ return relPath.split(/[/\\]/).some((segment) => segment.startsWith("."));
88
+ }
89
+
90
+ // `rootReal` MUST already be a realpath. Returns null on traversal or if either path doesn't exist on disk.
93
91
  export function resolveWithinRoot(rootReal: string, relPath: string): string | null {
94
92
  const normalized = path.normalize(relPath || "");
95
93
  const resolved = path.resolve(rootReal, normalized);
@@ -1,10 +1,4 @@
1
- // Domain I/O: scheduler items
2
- // data/scheduler/items.json
3
- //
4
- // Sync API. Optional `root` for test DI.
5
-
6
- import { WORKSPACE_FILES } from "../../workspace/paths.js";
7
- import { workspacePath } from "../../workspace/paths.js";
1
+ import { WORKSPACE_FILES, workspacePath } from "../../workspace/paths.js";
8
2
  import { resolvePath } from "./workspace-io.js";
9
3
  import { loadJsonFile } from "./json.js";
10
4
  import { writeFileAtomicSync } from "./atomic.js";
@@ -1,9 +1,3 @@
1
- // Domain I/O for scheduler override config.
2
- //
3
- // Reads/writes config/scheduler/overrides.json. Each key is a
4
- // system task ID (e.g. "system:journal"), value overrides the
5
- // default schedule.
6
-
7
1
  import { mkdirSync } from "fs";
8
2
  import path from "path";
9
3
  import { workspacePath } from "../../workspace/paths.js";
@@ -14,15 +8,14 @@ import { log } from "../../system/logger/index.js";
14
8
  import { isRecord } from "../types.js";
15
9
 
16
10
  export interface ScheduleOverride {
17
- /** Override interval in milliseconds (for interval-type schedules). */
18
11
  intervalMs?: number;
19
- /** Override time "HH:MM" in UTC (for daily-type schedules). */
12
+ // "HH:MM" in UTC for daily schedules.
20
13
  time?: string;
21
14
  }
22
15
 
23
16
  export type ScheduleOverrides = Record<string, ScheduleOverride>;
24
17
 
25
- /** Strict HH:MM validation — rejects 99:99 etc. */
18
+ // Strict HH:MM — rejects 99:99 etc.
26
19
  export const UTC_HH_MM_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
27
20
 
28
21
  function isScheduleOverride(value: unknown): value is ScheduleOverride {
@@ -30,7 +23,6 @@ function isScheduleOverride(value: unknown): value is ScheduleOverride {
30
23
  const obj = value;
31
24
  const hasInterval = "intervalMs" in obj && typeof obj.intervalMs === "number" && obj.intervalMs > 0;
32
25
  const hasTime = "time" in obj && typeof obj.time === "string" && UTC_HH_MM_RE.test(obj.time);
33
- // At least one valid field required
34
26
  return hasInterval || hasTime;
35
27
  }
36
28
 
@@ -38,7 +30,7 @@ function overridesPath(root?: string): string {
38
30
  return path.join(root ?? workspacePath, WORKSPACE_FILES.schedulerOverrides);
39
31
  }
40
32
 
41
- /** Load schedule overrides. Filters out invalid entries with a warning. */
33
+ // Filters out invalid entries with a warning.
42
34
  export function loadSchedulerOverrides(root?: string): ScheduleOverrides {
43
35
  const raw = loadJsonFile<unknown>(overridesPath(root), {});
44
36
  if (!isRecord(raw)) {
@@ -56,7 +48,6 @@ export function loadSchedulerOverrides(root?: string): ScheduleOverrides {
56
48
  return result;
57
49
  }
58
50
 
59
- /** Save schedule overrides atomically. Creates directory if needed. */
60
51
  export function saveSchedulerOverrides(overrides: ScheduleOverrides, root?: string): void {
61
52
  const filePath = overridesPath(root);
62
53
  mkdirSync(path.dirname(filePath), { recursive: true });
@@ -1,19 +1,12 @@
1
- // Domain I/O: chat sessions
2
- // conversations/chat/<id>.json — metadata
3
- // conversations/chat/<id>.jsonl — event log
4
- //
5
- // All functions take optional `root` for test DI.
6
-
7
- import { appendFile } from "fs/promises";
1
+ import { appendFile, rm } from "fs/promises";
8
2
  import path from "node:path";
9
- import { WORKSPACE_DIRS } from "../../workspace/paths.js";
10
- import { workspacePath } from "../../workspace/paths.js";
3
+ import { WORKSPACE_DIRS, workspacePath } from "../../workspace/paths.js";
11
4
  import { readTextUnder, writeTextUnder, resolvePath, ensureWorkspaceDir } from "./workspace-io.js";
5
+ import type { SessionOrigin } from "../../../src/types/session.js";
12
6
 
13
7
  const CHAT = WORKSPACE_DIRS.chat;
14
8
  const root = (rootOverride?: string) => rootOverride ?? workspacePath;
15
9
 
16
- /** Ensure the chat directory exists. Called once at session start. */
17
10
  export function ensureChatDir(): void {
18
11
  ensureWorkspaceDir(CHAT);
19
12
  }
@@ -26,21 +19,19 @@ function jsonlRel(sessionId: string): string {
26
19
  return path.posix.join(CHAT, `${sessionId}.jsonl`);
27
20
  }
28
21
 
29
- // ── Meta ────────────────────────────────────────────────────────
30
-
31
22
  export interface SessionMeta {
32
23
  roleId?: string;
33
24
  startedAt?: string;
34
25
  firstUserMessage?: string;
35
26
  claudeSessionId?: string;
36
27
  hasUnread?: boolean;
37
- origin?: "human" | "scheduler" | "skill" | "bridge";
28
+ isBookmarked?: boolean;
29
+ origin?: SessionOrigin;
38
30
  [key: string]: unknown;
39
31
  }
40
32
 
41
33
  export type ReadMetaResult = { kind: "missing" } | { kind: "ok"; meta: SessionMeta } | { kind: "corrupt"; raw: string };
42
34
 
43
- /** Read session metadata with full outcome discrimination. */
44
35
  export async function readSessionMetaFull(sessionId: string, rootOverride?: string): Promise<ReadMetaResult> {
45
36
  const raw = await readTextUnder(root(rootOverride), metaRel(sessionId));
46
37
  if (raw === null) return { kind: "missing" };
@@ -51,8 +42,7 @@ export async function readSessionMetaFull(sessionId: string, rootOverride?: stri
51
42
  }
52
43
  }
53
44
 
54
- /** Convenience: returns the meta or null. Treats corrupt as null
55
- * (callers that need to distinguish use readSessionMetaFull). */
45
+ // Treats corrupt as null callers that need to distinguish use readSessionMetaFull.
56
46
  export async function readSessionMeta(sessionId: string, rootOverride?: string): Promise<SessionMeta | null> {
57
47
  const result = await readSessionMetaFull(sessionId, rootOverride);
58
48
  return result.kind === "ok" ? result.meta : null;
@@ -103,18 +93,25 @@ export async function updateHasUnread(sessionId: string, hasUnread: boolean, roo
103
93
  await writeSessionMeta(sessionId, { ...meta, hasUnread }, rootOverride);
104
94
  }
105
95
 
106
- // ── Jsonl ───────────────────────────────────────────────────────
96
+ export async function updateIsBookmarked(sessionId: string, isBookmarked: boolean, rootOverride?: string): Promise<void> {
97
+ const meta = await readSessionMeta(sessionId, rootOverride);
98
+ if (!meta) return;
99
+ await writeSessionMeta(sessionId, { ...meta, isBookmarked }, rootOverride);
100
+ }
101
+
102
+ // Hard-deletes the session's .jsonl event log and .json meta sidecar.
103
+ // Missing files are tolerated — callers may invoke this for sessions
104
+ // whose meta or jsonl was never written (e.g. a crash mid-create).
105
+ export async function deleteSessionFiles(sessionId: string, rootOverride?: string): Promise<void> {
106
+ await rm(sessionJsonlAbsPath(sessionId, rootOverride), { force: true });
107
+ await rm(sessionMetaAbsPath(sessionId, rootOverride), { force: true });
108
+ }
107
109
 
108
110
  export function sessionJsonlAbsPath(sessionId: string, rootOverride?: string): string {
109
111
  return resolvePath(root(rootOverride), jsonlRel(sessionId));
110
112
  }
111
113
 
112
- /**
113
- * Resolve the absolute path of a session's metadata JSON file. The
114
- * jsonl variant is the event log; this one is the sidecar that holds
115
- * `hasUnread`, `roleId`, `startedAt`, `origin`, etc. Its mtime bumps
116
- * whenever any of those fields change via `writeSessionMeta`.
117
- */
114
+ // .json sidecar to the event-log jsonl. mtime bumps on every writeSessionMeta — used as a "session changed" signal.
118
115
  export function sessionMetaAbsPath(sessionId: string, rootOverride?: string): string {
119
116
  return resolvePath(root(rootOverride), metaRel(sessionId));
120
117
  }
@@ -123,13 +120,7 @@ export async function readSessionJsonl(sessionId: string, rootOverride?: string)
123
120
  return readTextUnder(root(rootOverride), jsonlRel(sessionId));
124
121
  }
125
122
 
126
- /**
127
- * Append a single line to the session event log (JSONL format).
128
- *
129
- * The function **ensures a trailing `\n`** — callers pass the raw
130
- * content and don't need to worry about line termination. This
131
- * prevents JSONL parse failures from missing newlines.
132
- */
123
+ // Always ends with `\n` to prevent JSONL parse failures from a missing terminator.
133
124
  export async function appendSessionLine(sessionId: string, line: string, rootOverride?: string): Promise<void> {
134
125
  const normalized = line.endsWith("\n") ? line : `${line}\n`;
135
126
  await appendFile(resolvePath(root(rootOverride), jsonlRel(sessionId)), normalized);
@@ -1,15 +1,14 @@
1
- import { mkdir, realpath, writeFile } from "fs/promises";
1
+ import { mkdir, realpath } from "fs/promises";
2
2
  import path from "path";
3
3
  import { WORKSPACE_DIRS, WORKSPACE_PATHS } from "../../workspace/paths.js";
4
4
  import { shortId } from "../id.js";
5
+ import { writeFileAtomic } from "./atomic.js";
5
6
  import { yearMonthUtc } from "./naming.js";
6
7
  import { resolveWithinRoot } from "./safe.js";
7
8
 
8
9
  const SPREADSHEETS_DIR = WORKSPACE_PATHS.spreadsheets;
9
10
 
10
- // Cached realpath of the spreadsheets directory. resolveWithinRoot
11
- // requires its root argument to be a realpath so symlinks are handled
12
- // correctly. Matches the pattern used in image-store.ts.
11
+ // resolveWithinRoot needs a realpath as its root so symlinks resolve correctly (same pattern as image-store).
13
12
  let spreadsheetsDirReal: string | null = null;
14
13
 
15
14
  async function ensureSpreadsheetsDir(): Promise<string> {
@@ -19,13 +18,9 @@ async function ensureSpreadsheetsDir(): Promise<string> {
19
18
  return spreadsheetsDirReal;
20
19
  }
21
20
 
22
- // Resolve a workspace-relative spreadsheet path (e.g. "spreadsheets/abc.json")
23
- // into an absolute path guaranteed to be inside the spreadsheets directory.
24
- // Throws on traversal attempts.
21
+ // Throws on traversal. Strips a leading "spreadsheets/" so callers can pass either the stored form or bare filename.
25
22
  async function safeResolve(relativePath: string): Promise<string> {
26
23
  const root = await ensureSpreadsheetsDir();
27
- // Strip the leading "spreadsheets/" prefix so callers can pass either
28
- // the stored form or just the filename.
29
24
  const name = relativePath.replace(new RegExp(`^${WORKSPACE_DIRS.spreadsheets}/`), "");
30
25
  const result = resolveWithinRoot(root, name);
31
26
  if (!result) {
@@ -34,33 +29,25 @@ async function safeResolve(relativePath: string): Promise<string> {
34
29
  return result;
35
30
  }
36
31
 
37
- /** Save sheets array as a JSON file. New files land under
38
- * `spreadsheets/YYYY/MM/` (UTC) so the dir doesn't accumulate
39
- * unbounded — see #764. Returns the workspace-relative path. */
32
+ // #764 sharded under spreadsheets/YYYY/MM/ (UTC) so the dir doesn't grow unbounded; #881 atomic.
40
33
  export async function saveSpreadsheet(sheets: unknown[]): Promise<string> {
41
34
  await ensureSpreadsheetsDir();
42
35
  const partition = yearMonthUtc();
43
- const parentAbs = path.join(SPREADSHEETS_DIR, partition);
44
- await mkdir(parentAbs, { recursive: true });
45
36
  const filename = `${shortId()}.json`;
46
- await writeFile(path.join(parentAbs, filename), JSON.stringify(sheets), "utf-8");
37
+ const absPath = path.join(SPREADSHEETS_DIR, partition, filename);
38
+ await writeFileAtomic(absPath, JSON.stringify(sheets));
47
39
  return path.posix.join(WORKSPACE_DIRS.spreadsheets, partition, filename);
48
40
  }
49
41
 
50
- /** Overwrite an existing spreadsheet file. */
51
42
  export async function overwriteSpreadsheet(relativePath: string, sheets: unknown[]): Promise<void> {
52
43
  const absPath = await safeResolve(relativePath);
53
- await writeFile(absPath, JSON.stringify(sheets), "utf-8");
44
+ await writeFileAtomic(absPath, JSON.stringify(sheets));
54
45
  }
55
46
 
56
- /** Check if a string is a spreadsheet file path (not inline data).
57
- * Rejects traversal attempts like "spreadsheets/../outside.json"
58
- * so the caller can't rely on the prefix/suffix alone. */
47
+ // Reject "spreadsheets/../outside.json" early; realpath check still runs server-side, but catch obvious cases here.
59
48
  export function isSpreadsheetPath(value: string): boolean {
60
49
  if (!value.startsWith(`${WORKSPACE_DIRS.spreadsheets}/`)) return false;
61
50
  if (!value.endsWith(".json")) return false;
62
- // Forbid .. segments anywhere in the path — a realpath check still
63
- // happens server-side, but this catches obvious cases early.
64
51
  const normalized = path.posix.normalize(value);
65
52
  if (normalized !== value) return false;
66
53
  if (normalized.includes("..")) return false;
@@ -0,0 +1,46 @@
1
+ // Domain file I/O for the translation cache. One JSON file per
2
+ // namespace under `data/translation/`. All writes go through the
3
+ // atomic helper per project rule.
4
+
5
+ import path from "node:path";
6
+ import { WORKSPACE_DIRS, workspacePath } from "../../workspace/paths.js";
7
+ import { loadJsonFile, writeJsonAtomic } from "./json.js";
8
+ import { emptyDictionary } from "../../services/translation/cache.js";
9
+ import type { DictionaryFile } from "../../services/translation/types.js";
10
+
11
+ function root(workspaceRoot?: string): string {
12
+ return workspaceRoot ?? workspacePath;
13
+ }
14
+
15
+ export function dictionaryPath(namespace: string, workspaceRoot?: string): string {
16
+ return path.join(root(workspaceRoot), WORKSPACE_DIRS.translation, `${namespace}.json`);
17
+ }
18
+
19
+ // Cache files live under the user's workspace and can be hand-edited
20
+ // or corrupted; treat the disk shape as untrusted and fall back to
21
+ // an empty dictionary on anything we can't recognize. Without this
22
+ // guard a `{}` or `{ sentences: null }` file would crash later at
23
+ // `dict.sentences[sentence]` and turn every request for the namespace
24
+ // into a 500 until the file was repaired.
25
+ function isValidDictionary(value: unknown): value is DictionaryFile {
26
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
27
+ const { sentences } = value as { sentences?: unknown };
28
+ if (typeof sentences !== "object" || sentences === null || Array.isArray(sentences)) return false;
29
+ for (const inner of Object.values(sentences)) {
30
+ if (typeof inner !== "object" || inner === null || Array.isArray(inner)) return false;
31
+ for (const translated of Object.values(inner as Record<string, unknown>)) {
32
+ if (typeof translated !== "string") return false;
33
+ }
34
+ }
35
+ return true;
36
+ }
37
+
38
+ export function loadDictionary(namespace: string, workspaceRoot?: string): DictionaryFile {
39
+ const raw = loadJsonFile<unknown>(dictionaryPath(namespace, workspaceRoot), null);
40
+ if (!isValidDictionary(raw)) return emptyDictionary();
41
+ return raw;
42
+ }
43
+
44
+ export async function saveDictionary(namespace: string, dict: DictionaryFile, workspaceRoot?: string): Promise<void> {
45
+ await writeJsonAtomic(dictionaryPath(namespace, workspaceRoot), dict);
46
+ }
@@ -1,12 +1,6 @@
1
- // Domain I/O: user-created scheduled tasks
2
- // config/scheduler/tasks.json
3
- //
4
- // Optional `root` parameter for test DI (defaults to workspacePath).
5
-
6
1
  import path from "path";
7
2
  import { mkdir } from "fs/promises";
8
- import { WORKSPACE_FILES } from "../../workspace/paths.js";
9
- import { workspacePath } from "../../workspace/paths.js";
3
+ import { WORKSPACE_FILES, workspacePath } from "../../workspace/paths.js";
10
4
  import { resolvePath } from "./workspace-io.js";
11
5
  import { loadJsonFile } from "./json.js";
12
6
  import { writeFileAtomic } from "./atomic.js";
@@ -1,15 +1,5 @@
1
- // Workspace-aware file I/O the single place implementation code
2
- // should reach for when reading/writing files under ~/mulmoclaude/.
3
- //
4
- // Combines WORKSPACE_PATHS (path resolution) with the atomic/safe
5
- // helpers (I/O primitives) so call sites never need raw `path.join`
6
- // + raw `fs.*` for workspace files.
7
- //
8
- // All writes go through writeFileAtomic so concurrent readers always
9
- // see a consistent file — never a half-written one.
10
- //
11
- // All reads swallow ENOENT and return null / fallback so callers can
12
- // do `if (!content)` instead of try/catch.
1
+ // All writes go through writeFileAtomic so concurrent readers never see a half-written file. All reads swallow ENOENT
2
+ // and return null/fallback so callers can branch on `!content` instead of try/catch.
13
3
 
14
4
  import { Stats, mkdirSync, promises, readFileSync, readdirSync, statSync } from "fs";
15
5
  import path from "path";
@@ -24,24 +14,10 @@ function rethrowUnexpected(err: unknown, context: string): null {
24
14
  throw err;
25
15
  }
26
16
 
27
- // ── Path resolution ─────────────────────────────────────────────
28
-
29
- /**
30
- * Resolve a workspace-relative path to an absolute path.
31
- * Use this instead of `path.join(workspacePath, rel)` in
32
- * implementation code — keeps the workspace root reference in
33
- * one place.
34
- */
35
17
  export function resolveWorkspacePath(relPath: string): string {
36
18
  return path.join(workspacePath, relPath);
37
19
  }
38
20
 
39
- // ── Read ────────────────────────────────────────────────────────
40
-
41
- /**
42
- * Read a text file under the workspace. Returns null on ENOENT;
43
- * logs and re-throws unexpected errors (EACCES, EPERM, etc.).
44
- */
45
21
  export async function readWorkspaceText(relPath: string): Promise<string | null> {
46
22
  try {
47
23
  return await promises.readFile(resolveWorkspacePath(relPath), "utf-8");
@@ -50,7 +26,6 @@ export async function readWorkspaceText(relPath: string): Promise<string | null>
50
26
  }
51
27
  }
52
28
 
53
- /** Sync variant. Same ENOENT-only swallow contract. */
54
29
  export function readWorkspaceTextSync(relPath: string): string | null {
55
30
  try {
56
31
  return readFileSync(resolveWorkspacePath(relPath), "utf-8");
@@ -59,10 +34,6 @@ export function readWorkspaceTextSync(relPath: string): string | null {
59
34
  }
60
35
  }
61
36
 
62
- /**
63
- * Read and parse a JSON file under the workspace. Returns
64
- * `fallback` if the file is missing, unreadable, or malformed.
65
- */
66
37
  export async function readWorkspaceJson<T>(relPath: string, fallback: T): Promise<T> {
67
38
  const text = await readWorkspaceText(relPath);
68
39
  if (text === null) return fallback;
@@ -73,7 +44,6 @@ export async function readWorkspaceJson<T>(relPath: string, fallback: T): Promis
73
44
  }
74
45
  }
75
46
 
76
- /** Sync variant of `readWorkspaceJson`. */
77
47
  export function readWorkspaceJsonSync<T>(relPath: string, fallback: T): T {
78
48
  const text = readWorkspaceTextSync(relPath);
79
49
  if (text === null) return fallback;
@@ -84,53 +54,23 @@ export function readWorkspaceJsonSync<T>(relPath: string, fallback: T): T {
84
54
  }
85
55
  }
86
56
 
87
- // ── Write ───────────────────────────────────────────────────────
88
-
89
- /**
90
- * Write a text file under the workspace atomically.
91
- * Parent directories are created if missing.
92
- */
93
57
  export async function writeWorkspaceText(relPath: string, content: string, opts?: { mode?: number }): Promise<void> {
94
58
  await writeFileAtomic(resolveWorkspacePath(relPath), content, opts);
95
59
  }
96
60
 
97
- /** Sync variant for startup / init paths. */
98
61
  export function writeWorkspaceTextSync(relPath: string, content: string, opts?: { mode?: number }): void {
99
62
  writeFileAtomicSync(resolveWorkspacePath(relPath), content, opts);
100
63
  }
101
64
 
102
- /**
103
- * Write a JSON value under the workspace atomically.
104
- * Pretty-printed with 2-space indent.
105
- */
106
65
  export async function writeWorkspaceJson(relPath: string, data: unknown, opts?: { mode?: number }): Promise<void> {
107
66
  await writeFileAtomic(resolveWorkspacePath(relPath), JSON.stringify(data, null, 2), opts);
108
67
  }
109
68
 
110
- // ── Rooted variants (for DI / testable modules) ────────────────
111
- //
112
- // Modules that take `root` as a parameter (journal, sources, etc.)
113
- // use these instead of raw path.join + fs.*. Same contract as the
114
- // workspace-* helpers, but root is caller-supplied.
115
- //
116
- // **IMPORTANT — internal paths only.** These helpers do NOT guard
117
- // against `..` traversal. They are designed for domain I/O modules
118
- // that pass compile-time-fixed relative paths like
119
- // `${WORKSPACE_DIRS.chat}/${id}.json`. User-supplied or HTTP-body
120
- // paths MUST go through `resolveWithinRoot()` in `safe.ts` instead.
121
-
122
- /**
123
- * Resolve root + relPath. Replaces raw `path.join(root, rel)`.
124
- *
125
- * For **internal fixed paths only** — never pass user input as
126
- * `relPath`. Use `resolveWithinRoot()` for user-supplied paths.
127
- */
69
+ // **Internal fixed paths only.** No `..` traversal guard — user-supplied paths MUST go through resolveWithinRoot() in safe.ts.
128
70
  export function resolvePath(root: string, relPath: string): string {
129
71
  return path.join(root, relPath);
130
72
  }
131
73
 
132
- /** Read text under an arbitrary root. Null on ENOENT; rethrows
133
- * unexpected errors. */
134
74
  export async function readTextUnder(root: string, relPath: string): Promise<string | null> {
135
75
  try {
136
76
  return await promises.readFile(path.join(root, relPath), "utf-8");
@@ -139,12 +79,10 @@ export async function readTextUnder(root: string, relPath: string): Promise<stri
139
79
  }
140
80
  }
141
81
 
142
- /** Write atomically under an arbitrary root. */
143
82
  export async function writeTextUnder(root: string, relPath: string, content: string): Promise<void> {
144
83
  await writeFileAtomic(path.join(root, relPath), content);
145
84
  }
146
85
 
147
- /** Sync read text under a root. Null on ENOENT. */
148
86
  export function readTextUnderSync(root: string, relPath: string): string | null {
149
87
  try {
150
88
  return readFileSync(path.join(root, relPath), "utf-8");
@@ -153,7 +91,6 @@ export function readTextUnderSync(root: string, relPath: string): string | null
153
91
  }
154
92
  }
155
93
 
156
- /** Sync readdir under a root. Empty on ENOENT. */
157
94
  export function readdirUnderSync(root: string, relPath: string): string[] {
158
95
  try {
159
96
  return readdirSync(path.join(root, relPath));
@@ -166,7 +103,6 @@ export function readdirUnderSync(root: string, relPath: string): string[] {
166
103
  }
167
104
  }
168
105
 
169
- /** Readdir under a root. Empty on ENOENT; rethrows unexpected. */
170
106
  export async function readdirUnder(root: string, relPath: string): Promise<string[]> {
171
107
  try {
172
108
  return await promises.readdir(path.join(root, relPath));
@@ -179,7 +115,6 @@ export async function readdirUnder(root: string, relPath: string): Promise<strin
179
115
  }
180
116
  }
181
117
 
182
- /** Stat under a root. Null on ENOENT; rethrows unexpected. */
183
118
  export async function statUnder(root: string, relPath: string): Promise<Stats | null> {
184
119
  try {
185
120
  return await promises.stat(path.join(root, relPath));
@@ -188,17 +123,10 @@ export async function statUnder(root: string, relPath: string): Promise<Stats |
188
123
  }
189
124
  }
190
125
 
191
- /** Ensure a directory exists under a root. */
192
126
  export async function ensureDirUnder(root: string, relPath: string): Promise<void> {
193
127
  await promises.mkdir(path.join(root, relPath), { recursive: true });
194
128
  }
195
129
 
196
- // ── Existence ───────────────────────────────────────────────────
197
-
198
- /**
199
- * Check whether a workspace-relative path exists on disk.
200
- * Returns false on ENOENT; rethrows unexpected errors.
201
- */
202
130
  export function existsInWorkspace(relPath: string): boolean {
203
131
  try {
204
132
  statSync(resolveWorkspacePath(relPath));
@@ -212,10 +140,6 @@ export function existsInWorkspace(relPath: string): boolean {
212
140
  }
213
141
  }
214
142
 
215
- /**
216
- * Ensure a workspace-relative directory exists. Creates it
217
- * (including parents) if missing. Idempotent.
218
- */
219
143
  export function ensureWorkspaceDir(relPath: string): void {
220
144
  mkdirSync(resolveWorkspacePath(relPath), { recursive: true });
221
145
  }