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,45 @@
1
+ // Consolidated workspace file I/O (#366).
2
+ //
3
+ // This barrel re-exports every public helper so call sites can do:
4
+ //
5
+ // import { writeFileAtomic, readWorkspaceText } from "../utils/files/index.js";
6
+ //
7
+ // Grouped by concern:
8
+ //
9
+ // atomic.ts — write-then-rename primitives
10
+ // safe.ts — ENOENT-swallowing wrappers (stat, readdir, readText, resolveWithinRoot)
11
+ // json.ts — JSON read/write (sync legacy + async atomic)
12
+ // workspace-io.ts — workspace-aware helpers (path resolve + I/O in one call)
13
+
14
+ export { writeFileAtomic, writeFileAtomicSync, type WriteAtomicOptions } from "./atomic.js";
15
+
16
+ export { isEnoent, readTextSafeSync, statSafe, statSafeAsync, readDirSafe, readDirSafeAsync, readTextOrNull, resolveWithinRoot } from "./safe.js";
17
+
18
+ export { loadJsonFile, saveJsonFile, writeJsonAtomic, readJsonOrNull } from "./json.js";
19
+
20
+ export {
21
+ resolveWorkspacePath,
22
+ resolvePath,
23
+ readWorkspaceText,
24
+ readWorkspaceTextSync,
25
+ readWorkspaceJson,
26
+ readWorkspaceJsonSync,
27
+ writeWorkspaceText,
28
+ writeWorkspaceTextSync,
29
+ writeWorkspaceJson,
30
+ existsInWorkspace,
31
+ ensureWorkspaceDir,
32
+ readTextUnder,
33
+ writeTextUnder,
34
+ readdirUnder,
35
+ statUnder,
36
+ ensureDirUnder,
37
+ } from "./workspace-io.js";
38
+
39
+ // ── Domain I/O ──────────────────────────────────────────────────
40
+ export * from "./session-io.js";
41
+ export * from "./todos-io.js";
42
+ export * from "./scheduler-io.js";
43
+ export * from "./html-io.js";
44
+ export * from "./reference-dirs-io.js";
45
+ export * from "./scheduler-overrides-io.js";
@@ -0,0 +1,213 @@
1
+ // Domain I/O: workspace journal (summaries)
2
+ // conversations/summaries/_state.json — journal state
3
+ // conversations/summaries/_index.md — browseable index
4
+ // conversations/summaries/daily/YYYY/MM/DD.md — daily summaries
5
+ // conversations/summaries/topics/<slug>.md — topic files
6
+ // conversations/summaries/archive/topics/ — archived topics
7
+ //
8
+ // All functions take optional `root` for test DI.
9
+ // Path helpers (summariesRoot, dailyPathFor, topicPathFor) live in
10
+ // journal/paths.ts — this module wraps them with I/O.
11
+
12
+ import path from "node:path";
13
+ import fsp from "node:fs/promises";
14
+ import { workspacePath } from "../../workspace/paths.js";
15
+ import { writeFileAtomic } from "./atomic.js";
16
+ import { isEnoent } from "./safe.js";
17
+ import { log } from "../../system/logger/index.js";
18
+ import { summariesRoot, dailyPathFor, topicPathFor, TOPICS_DIR, INDEX_FILE, STATE_FILE, DAILY_DIR, ARCHIVE_DIR } from "../../workspace/journal/paths.js";
19
+
20
+ import fs from "node:fs";
21
+
22
+ const root = (rootOverride?: string) => rootOverride ?? workspacePath;
23
+
24
+ // ── State ───────────────────────────────────────────────────────
25
+
26
+ export function journalStateExists(rootOverride?: string): boolean {
27
+ const filePath = path.join(summariesRoot(root(rootOverride)), STATE_FILE);
28
+ try {
29
+ fs.statSync(filePath);
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ export async function readJournalState<T>(fallback: T, rootOverride?: string): Promise<T> {
37
+ const filePath = path.join(summariesRoot(root(rootOverride)), STATE_FILE);
38
+ try {
39
+ return JSON.parse(await fsp.readFile(filePath, "utf-8")) as T;
40
+ } catch (err) {
41
+ if (isEnoent(err)) return fallback;
42
+ log.error("journal-io", "readJournalState failed", { error: String(err) });
43
+ return fallback;
44
+ }
45
+ }
46
+
47
+ export async function writeJournalState(state: unknown, rootOverride?: string): Promise<void> {
48
+ const filePath = path.join(summariesRoot(root(rootOverride)), STATE_FILE);
49
+ await writeFileAtomic(filePath, JSON.stringify(state, null, 2));
50
+ }
51
+
52
+ // ── Index ───────────────────────────────────────────────────────
53
+
54
+ export async function writeJournalIndex(md: string, rootOverride?: string): Promise<void> {
55
+ const filePath = path.join(summariesRoot(root(rootOverride)), INDEX_FILE);
56
+ await writeFileAtomic(filePath, md);
57
+ }
58
+
59
+ // ── Daily summaries ─────────────────────────────────────────────
60
+
61
+ export async function readDailySummary(date: string, rootOverride?: string): Promise<string | null> {
62
+ try {
63
+ return await fsp.readFile(dailyPathFor(root(rootOverride), date), "utf-8");
64
+ } catch (err) {
65
+ if (isEnoent(err)) return null;
66
+ log.error("journal-io", `readDailySummary(${date}) failed`, {
67
+ error: String(err),
68
+ });
69
+ return null;
70
+ }
71
+ }
72
+
73
+ export async function writeDailySummary(date: string, content: string, rootOverride?: string): Promise<void> {
74
+ await writeFileAtomic(dailyPathFor(root(rootOverride), date), content);
75
+ }
76
+
77
+ // ── Topics ──────────────────────────────────────────────────────
78
+
79
+ export async function readTopicFile(slug: string, rootOverride?: string): Promise<string | null> {
80
+ try {
81
+ return await fsp.readFile(topicPathFor(root(rootOverride), slug), "utf-8");
82
+ } catch (err) {
83
+ if (isEnoent(err)) return null;
84
+ // EACCES/EPERM must propagate — swallowing them would cause
85
+ // appendOrCreateTopic to clobber an unreadable file.
86
+ throw err;
87
+ }
88
+ }
89
+
90
+ export async function writeTopicFile(slug: string, content: string, rootOverride?: string): Promise<void> {
91
+ await writeFileAtomic(topicPathFor(root(rootOverride), slug), content);
92
+ }
93
+
94
+ /** Append content to an existing topic, or create a new file. */
95
+ export async function appendOrCreateTopic(slug: string, content: string, rootOverride?: string): Promise<"created" | "updated"> {
96
+ const existing = await readTopicFile(slug, rootOverride);
97
+ if (existing === null) {
98
+ await writeTopicFile(slug, content, rootOverride);
99
+ return "created";
100
+ }
101
+ await writeTopicFile(slug, `${existing.trimEnd()}\n\n${content}\n`, rootOverride);
102
+ return "updated";
103
+ }
104
+
105
+ /** List topic slugs (filenames without .md). */
106
+ export async function listTopicSlugs(rootOverride?: string): Promise<string[]> {
107
+ const dir = path.join(summariesRoot(root(rootOverride)), TOPICS_DIR);
108
+ try {
109
+ const files = await fsp.readdir(dir);
110
+ return files.filter((file) => file.endsWith(".md")).map((file) => file.replace(/\.md$/, ""));
111
+ } catch (err) {
112
+ if (isEnoent(err)) return [];
113
+ log.error("journal-io", "listTopicSlugs failed", { error: String(err) });
114
+ return [];
115
+ }
116
+ }
117
+
118
+ /** Read all topic files at once. Returns slug→content map. */
119
+ export async function readAllTopicFiles(rootOverride?: string): Promise<Map<string, string>> {
120
+ const dir = path.join(summariesRoot(root(rootOverride)), TOPICS_DIR);
121
+ const out = new Map<string, string>();
122
+ let files: string[];
123
+ try {
124
+ files = await fsp.readdir(dir);
125
+ } catch {
126
+ return out;
127
+ }
128
+ for (const file of files) {
129
+ if (!file.endsWith(".md")) continue;
130
+ try {
131
+ const content = await fsp.readFile(path.join(dir, file), "utf-8");
132
+ out.set(file.replace(/\.md$/, ""), content);
133
+ } catch {
134
+ // skip unreadable files
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+
140
+ /** Move a topic to the archive directory. Returns false if the
141
+ * source doesn't exist or the move fails. */
142
+ export async function archiveTopic(slug: string, rootOverride?: string): Promise<boolean> {
143
+ const src = topicPathFor(root(rootOverride), slug);
144
+ const dst = path.join(summariesRoot(root(rootOverride)), ARCHIVE_DIR, TOPICS_DIR, `${slug}.md`);
145
+ try {
146
+ await fsp.mkdir(path.dirname(dst), { recursive: true });
147
+ await fsp.rename(src, dst);
148
+ return true;
149
+ } catch (err) {
150
+ log.warn("journal-io", `archiveTopic(${slug}) failed`, {
151
+ error: String(err),
152
+ });
153
+ return false;
154
+ }
155
+ }
156
+
157
+ // ── Daily file listing ──────────────────────────────────────────
158
+
159
+ export interface DailyFileEntry {
160
+ year: string;
161
+ month: string;
162
+ day: string;
163
+ }
164
+
165
+ export async function listDailyFiles(rootOverride?: string): Promise<DailyFileEntry[]> {
166
+ const dailyRoot = path.join(summariesRoot(root(rootOverride)), DAILY_DIR);
167
+ const years = await safeReaddir(dailyRoot);
168
+ const out: DailyFileEntry[] = [];
169
+ for (const year of years.filter(isYearDir)) {
170
+ const entries = await listDaysForYear(dailyRoot, year);
171
+ out.push(...entries);
172
+ }
173
+ return out;
174
+ }
175
+
176
+ const YEAR_RE = /^\d{4}$/;
177
+ const MONTH_RE = /^\d{2}$/;
178
+ const isYearDir = (name: string) => YEAR_RE.test(name);
179
+ const isMonthDir = (name: string) => MONTH_RE.test(name);
180
+
181
+ async function listDaysForYear(dailyRoot: string, year: string): Promise<DailyFileEntry[]> {
182
+ const months = await safeReaddir(path.join(dailyRoot, year));
183
+ const out: DailyFileEntry[] = [];
184
+ for (const month of months.filter(isMonthDir)) {
185
+ const dayFiles = await safeReaddir(path.join(dailyRoot, year, month));
186
+ for (const dayFile of dayFiles) {
187
+ if (dayFile.endsWith(".md")) {
188
+ out.push({ year, month, day: dayFile.replace(/\.md$/, "") });
189
+ }
190
+ }
191
+ }
192
+ return out;
193
+ }
194
+
195
+ async function safeReaddir(dir: string): Promise<string[]> {
196
+ try {
197
+ return await fsp.readdir(dir);
198
+ } catch {
199
+ return [];
200
+ }
201
+ }
202
+
203
+ // ── Archived topic count ────────────────────────────────────────
204
+
205
+ export async function countArchivedTopics(rootOverride?: string): Promise<number> {
206
+ const dir = path.join(summariesRoot(root(rootOverride)), ARCHIVE_DIR, TOPICS_DIR);
207
+ try {
208
+ const files = await fsp.readdir(dir);
209
+ return files.filter((file) => file.endsWith(".md")).length;
210
+ } catch {
211
+ return 0;
212
+ }
213
+ }
@@ -0,0 +1,69 @@
1
+ // JSON file helpers — synchronous (legacy) and async (preferred).
2
+ //
3
+ // Moved from server/utils/file.ts (issue #366 Phase 1). The old
4
+ // file re-exports these for backwards compat.
5
+
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import { writeFileAtomic } from "./atomic.js";
9
+ import { isEnoent } from "./safe.js";
10
+ import { log } from "../../system/logger/index.js";
11
+
12
+ // ── Sync helpers ────────────────────────────────────────────────
13
+
14
+ /**
15
+ * Read and parse a JSON file synchronously. Returns `defaultValue`
16
+ * on ENOENT (file not yet created) or JSON corruption (logs the
17
+ * error but doesn't crash — user data files must not take down the
18
+ * server). Rethrows unexpected errors (EACCES, EPERM).
19
+ */
20
+ export function loadJsonFile<T>(filePath: string, defaultValue: T): T {
21
+ let raw: string;
22
+ try {
23
+ raw = fs.readFileSync(filePath, "utf-8");
24
+ } catch (err) {
25
+ if (isEnoent(err)) return defaultValue;
26
+ log.error("json", "loadJsonFile read failed", {
27
+ path: filePath,
28
+ error: String(err),
29
+ });
30
+ throw err;
31
+ }
32
+ try {
33
+ return JSON.parse(raw) as T;
34
+ } catch (err) {
35
+ log.error("json", "loadJsonFile parse failed, using default", {
36
+ path: filePath,
37
+ error: String(err),
38
+ });
39
+ return defaultValue;
40
+ }
41
+ }
42
+
43
+ export function saveJsonFile(filePath: string, data: unknown): void {
44
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
45
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
46
+ }
47
+
48
+ // ── Async ───────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * JSON-pretty-print `data` and write atomically.
52
+ */
53
+ export async function writeJsonAtomic(filePath: string, data: unknown, opts: Parameters<typeof writeFileAtomic>[2] = {}): Promise<void> {
54
+ await writeFileAtomic(filePath, JSON.stringify(data, null, 2), opts);
55
+ }
56
+
57
+ /**
58
+ * Read a JSON file and parse it. Returns null if the file is missing,
59
+ * unreadable, or malformed.
60
+ */
61
+ export async function readJsonOrNull<T>(filePath: string): Promise<T | null> {
62
+ try {
63
+ const content = await fs.promises.readFile(filePath, "utf-8");
64
+ const parsed: T = JSON.parse(content);
65
+ return parsed;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
@@ -0,0 +1,33 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { workspacePath } from "../../workspace/workspace.js";
4
+ import { WORKSPACE_DIRS } from "../../workspace/paths.js";
5
+ import { buildArtifactPathRandom } from "./naming.js";
6
+
7
+ /**
8
+ * Save markdown content as a file. Returns the workspace-relative path.
9
+ * `prefix` is slugified; a random id is always appended to prevent
10
+ * collisions between concurrent writers sharing the same prefix.
11
+ */
12
+ export async function saveMarkdown(content: string, prefix: string): Promise<string> {
13
+ const relPath = buildArtifactPathRandom(WORKSPACE_DIRS.markdowns, prefix, ".md", "document");
14
+ await fs.writeFile(path.join(workspacePath, relPath), content, "utf-8");
15
+ return relPath;
16
+ }
17
+
18
+ /** Read a markdown file and return its content. */
19
+ export async function loadMarkdown(relativePath: string): Promise<string> {
20
+ const absPath = path.join(workspacePath, relativePath);
21
+ return fs.readFile(absPath, "utf-8");
22
+ }
23
+
24
+ /** Overwrite an existing markdown file. */
25
+ export async function overwriteMarkdown(relativePath: string, content: string): Promise<void> {
26
+ const absPath = path.join(workspacePath, relativePath);
27
+ await fs.writeFile(absPath, content, "utf-8");
28
+ }
29
+
30
+ /** Check if a string is a markdown file path (not inline content). */
31
+ export function isMarkdownPath(value: string): boolean {
32
+ return value.startsWith(`${WORKSPACE_DIRS.markdowns}/`) && value.endsWith(".md");
33
+ }
@@ -0,0 +1,50 @@
1
+ // Workspace file naming conventions.
2
+ //
3
+ // Centralizes the `slug-${Date.now()}.ext` pattern used across
4
+ // multiple plugins (chart, presentHtml, markdown, spreadsheet, image).
5
+ // Call sites pass a human title + extension; this module handles
6
+ // slugification and timestamp suffixing.
7
+
8
+ import path from "node:path";
9
+ import crypto from "node:crypto";
10
+ import { slugify } from "../slug.js";
11
+
12
+ // Length of the random hex suffix appended by `buildArtifactPathRandom`.
13
+ // 16 chars = 64 bits ≈ birthday-collision at 2^32 entries — effectively
14
+ // impossible for any realistic per-workspace artifact volume.
15
+ const RANDOM_SUFFIX_LEN = 16;
16
+
17
+ /**
18
+ * Build a workspace-relative path for a new artifact file.
19
+ *
20
+ * @param dir Workspace-relative directory (e.g. WORKSPACE_DIRS.charts)
21
+ * @param title Human-readable title (slugified for the filename)
22
+ * @param ext File extension with leading dot (e.g. ".html", ".json")
23
+ * @param fallbackSlug Slug to use when title is empty/undefined
24
+ * @returns Workspace-relative path like "artifacts/charts/sales-1776135210389.chart.json"
25
+ */
26
+ export function buildArtifactPath(dir: string, title: string | undefined, ext: string, fallbackSlug = "file"): string {
27
+ const slug = title ? slugify(title) || fallbackSlug : fallbackSlug;
28
+ const fname = `${slug}-${Date.now()}${ext}`;
29
+ return path.posix.join(dir, fname);
30
+ }
31
+
32
+ /**
33
+ * Like `buildArtifactPath`, but appends a random hex id instead of a
34
+ * timestamp. Use when multiple concurrent writers may share the same
35
+ * prefix within the same millisecond (e.g. LLM-supplied `filenamePrefix`
36
+ * on the `presentDocument` route).
37
+ *
38
+ * @param dir Workspace-relative directory
39
+ * @param prefix Human-readable prefix (slugified via `slugify`)
40
+ * @param ext File extension with leading dot
41
+ * @param fallbackSlug Slug to use when the sanitized prefix is empty
42
+ */
43
+ export function buildArtifactPathRandom(dir: string, prefix: string, ext: string, fallbackSlug = "file"): string {
44
+ // Pass fallbackSlug as slugify's default so it overrides slugify's
45
+ // built-in "page" default when `prefix` sanitizes to empty.
46
+ const slug = slugify(prefix, fallbackSlug);
47
+ const id = crypto.randomUUID().replace(/-/g, "").slice(0, RANDOM_SUFFIX_LEN);
48
+ const fname = `${slug}-${id}${ext}`;
49
+ return path.posix.join(dir, fname);
50
+ }
@@ -0,0 +1,45 @@
1
+ // Domain I/O for reference directories.
2
+ //
3
+ // Reads/writes config/reference-dirs.json and checks host paths.
4
+ // All fs access is funneled through shared helpers so path changes
5
+ // propagate from a single constant.
6
+
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import { WORKSPACE_DIRS, workspacePath } from "../../workspace/paths.js";
10
+ import { loadJsonFile } from "./json.js";
11
+ import { writeFileAtomicSync } from "./atomic.js";
12
+ import { log } from "../../system/logger/index.js";
13
+
14
+ const CONFIG_FILE_NAME = "reference-dirs.json";
15
+
16
+ function configPath(root: string): string {
17
+ return path.join(root, WORKSPACE_DIRS.configs, CONFIG_FILE_NAME);
18
+ }
19
+
20
+ /** Read reference-dirs.json. Returns [] on missing/corrupt file. */
21
+ export function readReferenceDirsJson(root?: string): unknown[] {
22
+ const filePath = configPath(root ?? workspacePath);
23
+ const parsed = loadJsonFile<unknown>(filePath, []);
24
+ if (!Array.isArray(parsed)) {
25
+ log.warn("reference-dirs-io", "reference-dirs.json is not an array");
26
+ return [];
27
+ }
28
+ return parsed;
29
+ }
30
+
31
+ /** Write reference-dirs.json atomically. Creates config/ if needed. */
32
+ export function writeReferenceDirsJson(entries: readonly unknown[], root?: string): void {
33
+ const filePath = configPath(root ?? workspacePath);
34
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
35
+ writeFileAtomicSync(filePath, JSON.stringify(entries, null, 2));
36
+ }
37
+
38
+ /** Check whether a host path exists and is a directory. */
39
+ export function isExistingDirectory(hostPath: string): boolean {
40
+ try {
41
+ return fs.statSync(hostPath).isDirectory();
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
@@ -0,0 +1,45 @@
1
+ // Domain I/O: custom roles
2
+ // config/roles/<id>.json
3
+ //
4
+ // Optional `root` for test DI.
5
+
6
+ import path from "node:path";
7
+ import fs from "node:fs";
8
+ import { WORKSPACE_DIRS } from "../../workspace/paths.js";
9
+ import { workspacePath } from "../../workspace/paths.js";
10
+ import { writeFileAtomicSync } from "./atomic.js";
11
+ import { isEnoent } from "./safe.js";
12
+
13
+ const root = (r?: string) => r ?? workspacePath;
14
+
15
+ function roleFilePath(id: string, r?: string): string {
16
+ return path.join(root(r), WORKSPACE_DIRS.roles, `${id}.json`);
17
+ }
18
+
19
+ /** Check if a custom role file exists. */
20
+ export function roleExists(id: string, r?: string): boolean {
21
+ try {
22
+ fs.statSync(roleFilePath(id, r));
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /** Delete a custom role file. Returns false if not found. */
30
+ export function deleteRole(id: string, r?: string): boolean {
31
+ try {
32
+ fs.unlinkSync(roleFilePath(id, r));
33
+ return true;
34
+ } catch (err) {
35
+ if (isEnoent(err)) return false;
36
+ throw err;
37
+ }
38
+ }
39
+
40
+ /** Save (create or overwrite) a custom role file atomically. */
41
+ export function saveRole(id: string, data: unknown, r?: string): void {
42
+ const dir = path.join(root(r), WORKSPACE_DIRS.roles);
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ writeFileAtomicSync(roleFilePath(id, r), JSON.stringify(data, null, 2));
45
+ }
@@ -0,0 +1,106 @@
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.
9
+
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { isErrorWithCode } from "../types.js";
13
+
14
+ /** Check if an error is ENOENT (file/dir not found). */
15
+ export function isEnoent(err: unknown): boolean {
16
+ return isErrorWithCode(err) && err.code === "ENOENT";
17
+ }
18
+
19
+ /** Read a binary file by absolute path. Null on ENOENT. */
20
+ export function readBinarySafeSync(absPath: string): Buffer | null {
21
+ try {
22
+ return fs.readFileSync(absPath);
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /** Read a text file by absolute path (async). Null on ENOENT. */
29
+ export async function readTextSafe(absPath: string): Promise<string | null> {
30
+ try {
31
+ return await fs.promises.readFile(absPath, "utf-8");
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /** Read a text file by absolute path (sync). Null on ENOENT. */
38
+ export function readTextSafeSync(absPath: string): string | null {
39
+ try {
40
+ return fs.readFileSync(absPath, "utf-8");
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ export function statSafe(absPath: string): fs.Stats | null {
47
+ try {
48
+ return fs.statSync(absPath);
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ export async function statSafeAsync(absPath: string): Promise<fs.Stats | null> {
55
+ try {
56
+ return await fs.promises.stat(absPath);
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ export function readDirSafe(absPath: string): fs.Dirent[] {
63
+ try {
64
+ return fs.readdirSync(absPath, { withFileTypes: true });
65
+ } catch {
66
+ return [];
67
+ }
68
+ }
69
+
70
+ export async function readDirSafeAsync(absPath: string): Promise<fs.Dirent[]> {
71
+ try {
72
+ return await fs.promises.readdir(absPath, { withFileTypes: true });
73
+ } catch {
74
+ return [];
75
+ }
76
+ }
77
+
78
+ export async function readTextOrNull(file: string): Promise<string | null> {
79
+ try {
80
+ return await fs.promises.readFile(file, "utf-8");
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
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
+ */
93
+ export function resolveWithinRoot(rootReal: string, relPath: string): string | null {
94
+ const normalized = path.normalize(relPath || "");
95
+ const resolved = path.resolve(rootReal, normalized);
96
+ let resolvedReal: string;
97
+ try {
98
+ resolvedReal = fs.realpathSync(resolved);
99
+ } catch {
100
+ return null;
101
+ }
102
+ if (resolvedReal !== rootReal && !resolvedReal.startsWith(rootReal + path.sep)) {
103
+ return null;
104
+ }
105
+ return resolvedReal;
106
+ }
@@ -0,0 +1,20 @@
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";
8
+ import { resolvePath } from "./workspace-io.js";
9
+ import { loadJsonFile } from "./json.js";
10
+ import { writeFileAtomicSync } from "./atomic.js";
11
+
12
+ const root = (r?: string) => r ?? workspacePath;
13
+
14
+ export function loadSchedulerItems<T>(fallback: T, r?: string): T {
15
+ return loadJsonFile(resolvePath(root(r), WORKSPACE_FILES.schedulerItems), fallback);
16
+ }
17
+
18
+ export function saveSchedulerItems(items: unknown, r?: string): void {
19
+ writeFileAtomicSync(resolvePath(root(r), WORKSPACE_FILES.schedulerItems), JSON.stringify(items, null, 2));
20
+ }