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,64 @@
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
+ import fs from "fs";
8
+ import path from "path";
9
+ import { workspacePath } from "../../workspace/paths.js";
10
+ import { WORKSPACE_FILES } from "../../../src/config/workspacePaths.js";
11
+ import { loadJsonFile } from "./json.js";
12
+ import { writeFileAtomicSync } from "./atomic.js";
13
+ import { log } from "../../system/logger/index.js";
14
+ import { isRecord } from "../types.js";
15
+
16
+ export interface ScheduleOverride {
17
+ /** Override interval in milliseconds (for interval-type schedules). */
18
+ intervalMs?: number;
19
+ /** Override time "HH:MM" in UTC (for daily-type schedules). */
20
+ time?: string;
21
+ }
22
+
23
+ export type ScheduleOverrides = Record<string, ScheduleOverride>;
24
+
25
+ /** Strict HH:MM validation — rejects 99:99 etc. */
26
+ export const UTC_HH_MM_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
27
+
28
+ function isScheduleOverride(value: unknown): value is ScheduleOverride {
29
+ if (!isRecord(value)) return false;
30
+ const obj = value;
31
+ const hasInterval = "intervalMs" in obj && typeof obj.intervalMs === "number" && obj.intervalMs > 0;
32
+ const hasTime = "time" in obj && typeof obj.time === "string" && UTC_HH_MM_RE.test(obj.time);
33
+ // At least one valid field required
34
+ return hasInterval || hasTime;
35
+ }
36
+
37
+ function overridesPath(root?: string): string {
38
+ return path.join(root ?? workspacePath, WORKSPACE_FILES.schedulerOverrides);
39
+ }
40
+
41
+ /** Load schedule overrides. Filters out invalid entries with a warning. */
42
+ export function loadSchedulerOverrides(root?: string): ScheduleOverrides {
43
+ const raw = loadJsonFile<unknown>(overridesPath(root), {});
44
+ if (!isRecord(raw)) {
45
+ log.warn("scheduler-overrides", "overrides.json is not an object");
46
+ return {};
47
+ }
48
+ const result: ScheduleOverrides = {};
49
+ for (const [key, value] of Object.entries(raw)) {
50
+ if (isScheduleOverride(value)) {
51
+ result[key] = value;
52
+ } else {
53
+ log.warn("scheduler-overrides", "invalid entry, skipping", { key });
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+
59
+ /** Save schedule overrides atomically. Creates directory if needed. */
60
+ export function saveSchedulerOverrides(overrides: ScheduleOverrides, root?: string): void {
61
+ const filePath = overridesPath(root);
62
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
63
+ writeFileAtomicSync(filePath, JSON.stringify(overrides, null, 2));
64
+ }
@@ -0,0 +1,136 @@
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";
8
+ import path from "node:path";
9
+ import { WORKSPACE_DIRS } from "../../workspace/paths.js";
10
+ import { workspacePath } from "../../workspace/paths.js";
11
+ import { readTextUnder, writeTextUnder, resolvePath, ensureWorkspaceDir } from "./workspace-io.js";
12
+
13
+ const CHAT = WORKSPACE_DIRS.chat;
14
+ const root = (rootOverride?: string) => rootOverride ?? workspacePath;
15
+
16
+ /** Ensure the chat directory exists. Called once at session start. */
17
+ export function ensureChatDir(): void {
18
+ ensureWorkspaceDir(CHAT);
19
+ }
20
+
21
+ function metaRel(id: string): string {
22
+ return path.posix.join(CHAT, `${id}.json`);
23
+ }
24
+
25
+ function jsonlRel(id: string): string {
26
+ return path.posix.join(CHAT, `${id}.jsonl`);
27
+ }
28
+
29
+ // ── Meta ────────────────────────────────────────────────────────
30
+
31
+ export interface SessionMeta {
32
+ roleId?: string;
33
+ startedAt?: string;
34
+ firstUserMessage?: string;
35
+ claudeSessionId?: string;
36
+ hasUnread?: boolean;
37
+ origin?: "human" | "scheduler" | "skill" | "bridge";
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ export type ReadMetaResult = { kind: "missing" } | { kind: "ok"; meta: SessionMeta } | { kind: "corrupt"; raw: string };
42
+
43
+ /** Read session metadata with full outcome discrimination. */
44
+ export async function readSessionMetaFull(id: string, rootOverride?: string): Promise<ReadMetaResult> {
45
+ const raw = await readTextUnder(root(rootOverride), metaRel(id));
46
+ if (raw === null) return { kind: "missing" };
47
+ try {
48
+ return { kind: "ok", meta: JSON.parse(raw) as SessionMeta };
49
+ } catch {
50
+ return { kind: "corrupt", raw };
51
+ }
52
+ }
53
+
54
+ /** Convenience: returns the meta or null. Treats corrupt as null
55
+ * (callers that need to distinguish use readSessionMetaFull). */
56
+ export async function readSessionMeta(id: string, rootOverride?: string): Promise<SessionMeta | null> {
57
+ const result = await readSessionMetaFull(id, rootOverride);
58
+ return result.kind === "ok" ? result.meta : null;
59
+ }
60
+
61
+ export async function writeSessionMeta(id: string, meta: SessionMeta, rootOverride?: string): Promise<void> {
62
+ await writeTextUnder(root(rootOverride), metaRel(id), JSON.stringify(meta, null, 2));
63
+ }
64
+
65
+ export async function createSessionMeta(id: string, roleId: string, firstUserMessage: string, rootOverride?: string, origin?: string): Promise<void> {
66
+ const meta: Record<string, unknown> = {
67
+ roleId,
68
+ startedAt: new Date().toISOString(),
69
+ firstUserMessage,
70
+ };
71
+ if (origin) meta.origin = origin;
72
+ await writeSessionMeta(id, meta, rootOverride);
73
+ }
74
+
75
+ export async function backfillOrigin(id: string, origin: SessionMeta["origin"], rootOverride?: string): Promise<void> {
76
+ const meta = await readSessionMeta(id, rootOverride);
77
+ if (!meta || meta.origin) return; // already set
78
+ await writeSessionMeta(id, { ...meta, origin }, rootOverride);
79
+ }
80
+
81
+ export async function backfillFirstUserMessage(id: string, message: string, rootOverride?: string): Promise<void> {
82
+ const meta = await readSessionMeta(id, rootOverride);
83
+ if (!meta || meta.firstUserMessage) return;
84
+ await writeSessionMeta(id, { ...meta, firstUserMessage: message }, rootOverride);
85
+ }
86
+
87
+ export async function setClaudeSessionId(id: string, claudeSessionId: string, rootOverride?: string): Promise<void> {
88
+ const meta = await readSessionMeta(id, rootOverride);
89
+ if (!meta) return;
90
+ await writeSessionMeta(id, { ...meta, claudeSessionId }, rootOverride);
91
+ }
92
+
93
+ export async function clearClaudeSessionId(id: string, rootOverride?: string): Promise<void> {
94
+ const meta = await readSessionMeta(id, rootOverride);
95
+ if (!meta) return;
96
+ const { claudeSessionId: __removed, ...rest } = meta;
97
+ await writeSessionMeta(id, rest, rootOverride);
98
+ }
99
+
100
+ export async function updateHasUnread(id: string, hasUnread: boolean, rootOverride?: string): Promise<void> {
101
+ const meta = await readSessionMeta(id, rootOverride);
102
+ if (!meta) return;
103
+ await writeSessionMeta(id, { ...meta, hasUnread }, rootOverride);
104
+ }
105
+
106
+ // ── Jsonl ───────────────────────────────────────────────────────
107
+
108
+ export function sessionJsonlAbsPath(id: string, rootOverride?: string): string {
109
+ return resolvePath(root(rootOverride), jsonlRel(id));
110
+ }
111
+
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
+ */
118
+ export function sessionMetaAbsPath(id: string, rootOverride?: string): string {
119
+ return resolvePath(root(rootOverride), metaRel(id));
120
+ }
121
+
122
+ export async function readSessionJsonl(id: string, rootOverride?: string): Promise<string | null> {
123
+ return readTextUnder(root(rootOverride), jsonlRel(id));
124
+ }
125
+
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
+ */
133
+ export async function appendSessionLine(id: string, line: string, rootOverride?: string): Promise<void> {
134
+ const normalized = line.endsWith("\n") ? line : `${line}\n`;
135
+ await appendFile(resolvePath(root(rootOverride), jsonlRel(id)), normalized);
136
+ }
@@ -0,0 +1,63 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+ import { WORKSPACE_DIRS, WORKSPACE_PATHS } from "../../workspace/paths.js";
5
+ import { resolveWithinRoot } from "./safe.js";
6
+
7
+ const SPREADSHEETS_DIR = WORKSPACE_PATHS.spreadsheets;
8
+
9
+ // Cached realpath of the spreadsheets directory. resolveWithinRoot
10
+ // requires its root argument to be a realpath so symlinks are handled
11
+ // correctly. Matches the pattern used in image-store.ts.
12
+ let spreadsheetsDirReal: string | null = null;
13
+
14
+ async function ensureSpreadsheetsDir(): Promise<string> {
15
+ if (spreadsheetsDirReal) return spreadsheetsDirReal;
16
+ await fs.mkdir(SPREADSHEETS_DIR, { recursive: true });
17
+ spreadsheetsDirReal = await fs.realpath(SPREADSHEETS_DIR);
18
+ return spreadsheetsDirReal;
19
+ }
20
+
21
+ // Resolve a workspace-relative spreadsheet path (e.g. "spreadsheets/abc.json")
22
+ // into an absolute path guaranteed to be inside the spreadsheets directory.
23
+ // Throws on traversal attempts.
24
+ async function safeResolve(relativePath: string): Promise<string> {
25
+ const root = await ensureSpreadsheetsDir();
26
+ // Strip the leading "spreadsheets/" prefix so callers can pass either
27
+ // the stored form or just the filename.
28
+ const name = relativePath.replace(new RegExp(`^${WORKSPACE_DIRS.spreadsheets}/`), "");
29
+ const result = resolveWithinRoot(root, name);
30
+ if (!result) {
31
+ throw new Error(`path traversal rejected: ${relativePath}`);
32
+ }
33
+ return result;
34
+ }
35
+
36
+ /** Save sheets array as a JSON file. Returns the workspace-relative path. */
37
+ export async function saveSpreadsheet(sheets: unknown[]): Promise<string> {
38
+ await ensureSpreadsheetsDir();
39
+ const id = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
40
+ const filename = `${id}.json`;
41
+ await fs.writeFile(path.join(SPREADSHEETS_DIR, filename), JSON.stringify(sheets), "utf-8");
42
+ return path.posix.join(WORKSPACE_DIRS.spreadsheets, filename);
43
+ }
44
+
45
+ /** Overwrite an existing spreadsheet file. */
46
+ export async function overwriteSpreadsheet(relativePath: string, sheets: unknown[]): Promise<void> {
47
+ const absPath = await safeResolve(relativePath);
48
+ await fs.writeFile(absPath, JSON.stringify(sheets), "utf-8");
49
+ }
50
+
51
+ /** Check if a string is a spreadsheet file path (not inline data).
52
+ * Rejects traversal attempts like "spreadsheets/../outside.json"
53
+ * so the caller can't rely on the prefix/suffix alone. */
54
+ export function isSpreadsheetPath(value: string): boolean {
55
+ if (!value.startsWith(`${WORKSPACE_DIRS.spreadsheets}/`)) return false;
56
+ if (!value.endsWith(".json")) return false;
57
+ // Forbid .. segments anywhere in the path — a realpath check still
58
+ // happens server-side, but this catches obvious cases early.
59
+ const normalized = path.posix.normalize(value);
60
+ if (normalized !== value) return false;
61
+ if (normalized.includes("..")) return false;
62
+ return true;
63
+ }
@@ -0,0 +1,29 @@
1
+ // Domain I/O: todo items + status columns
2
+ // data/todos/todos.json — items
3
+ // data/todos/columns.json — status columns
4
+ //
5
+ // Sync API. Optional `root` for test DI.
6
+
7
+ import { WORKSPACE_FILES } from "../../workspace/paths.js";
8
+ import { workspacePath } from "../../workspace/paths.js";
9
+ import { resolvePath } from "./workspace-io.js";
10
+ import { loadJsonFile } from "./json.js";
11
+ import { writeFileAtomicSync } from "./atomic.js";
12
+
13
+ const root = (r?: string) => r ?? workspacePath;
14
+
15
+ export function loadTodos<T>(fallback: T, r?: string): T {
16
+ return loadJsonFile(resolvePath(root(r), WORKSPACE_FILES.todosItems), fallback);
17
+ }
18
+
19
+ export function saveTodos(items: unknown, r?: string): void {
20
+ writeFileAtomicSync(resolvePath(root(r), WORKSPACE_FILES.todosItems), JSON.stringify(items, null, 2));
21
+ }
22
+
23
+ export function loadColumns<T>(fallback: T, r?: string): T {
24
+ return loadJsonFile(resolvePath(root(r), WORKSPACE_FILES.todosColumns), fallback);
25
+ }
26
+
27
+ export function saveColumns(columns: unknown, r?: string): void {
28
+ writeFileAtomicSync(resolvePath(root(r), WORKSPACE_FILES.todosColumns), JSON.stringify(columns, null, 2));
29
+ }
@@ -0,0 +1,25 @@
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
+ import path from "path";
7
+ import { mkdir } from "fs/promises";
8
+ import { WORKSPACE_FILES } from "../../workspace/paths.js";
9
+ import { workspacePath } from "../../workspace/paths.js";
10
+ import { resolvePath } from "./workspace-io.js";
11
+ import { loadJsonFile } from "./json.js";
12
+ import { writeFileAtomic } from "./atomic.js";
13
+
14
+ const root = (r?: string) => r ?? workspacePath;
15
+
16
+ export function loadUserTasks<T>(r?: string): T[] {
17
+ const tasks = loadJsonFile<T[]>(resolvePath(root(r), WORKSPACE_FILES.schedulerUserTasks), []);
18
+ return Array.isArray(tasks) ? tasks : [];
19
+ }
20
+
21
+ export async function saveUserTasks<T>(tasks: T[], r?: string): Promise<void> {
22
+ const filePath = resolvePath(root(r), WORKSPACE_FILES.schedulerUserTasks);
23
+ await mkdir(path.dirname(filePath), { recursive: true });
24
+ await writeFileAtomic(filePath, JSON.stringify(tasks, null, 2));
25
+ }
@@ -0,0 +1,221 @@
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.
13
+
14
+ import fs from "fs";
15
+ import path from "path";
16
+ import { workspacePath } from "../../workspace/paths.js";
17
+ import { writeFileAtomic, writeFileAtomicSync } from "./atomic.js";
18
+ import { log } from "../../system/logger/index.js";
19
+ import { isEnoent } from "./safe.js";
20
+
21
+ function rethrowUnexpected(err: unknown, context: string): null {
22
+ if (isEnoent(err)) return null;
23
+ log.error("workspace-io", context, { error: String(err) });
24
+ throw err;
25
+ }
26
+
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
+ export function resolveWorkspacePath(relPath: string): string {
36
+ return path.join(workspacePath, relPath);
37
+ }
38
+
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
+ export async function readWorkspaceText(relPath: string): Promise<string | null> {
46
+ try {
47
+ return await fs.promises.readFile(resolveWorkspacePath(relPath), "utf-8");
48
+ } catch (err) {
49
+ return rethrowUnexpected(err, `readWorkspaceText(${relPath})`);
50
+ }
51
+ }
52
+
53
+ /** Sync variant. Same ENOENT-only swallow contract. */
54
+ export function readWorkspaceTextSync(relPath: string): string | null {
55
+ try {
56
+ return fs.readFileSync(resolveWorkspacePath(relPath), "utf-8");
57
+ } catch (err) {
58
+ return rethrowUnexpected(err, `readWorkspaceTextSync(${relPath})`);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Read and parse a JSON file under the workspace. Returns
64
+ * `fallback` if the file is missing, unreadable, or malformed.
65
+ */
66
+ export async function readWorkspaceJson<T>(relPath: string, fallback: T): Promise<T> {
67
+ const text = await readWorkspaceText(relPath);
68
+ if (text === null) return fallback;
69
+ try {
70
+ return JSON.parse(text) as T;
71
+ } catch {
72
+ return fallback;
73
+ }
74
+ }
75
+
76
+ /** Sync variant of `readWorkspaceJson`. */
77
+ export function readWorkspaceJsonSync<T>(relPath: string, fallback: T): T {
78
+ const text = readWorkspaceTextSync(relPath);
79
+ if (text === null) return fallback;
80
+ try {
81
+ return JSON.parse(text) as T;
82
+ } catch {
83
+ return fallback;
84
+ }
85
+ }
86
+
87
+ // ── Write ───────────────────────────────────────────────────────
88
+
89
+ /**
90
+ * Write a text file under the workspace atomically.
91
+ * Parent directories are created if missing.
92
+ */
93
+ export async function writeWorkspaceText(relPath: string, content: string, opts?: { mode?: number }): Promise<void> {
94
+ await writeFileAtomic(resolveWorkspacePath(relPath), content, opts);
95
+ }
96
+
97
+ /** Sync variant for startup / init paths. */
98
+ export function writeWorkspaceTextSync(relPath: string, content: string, opts?: { mode?: number }): void {
99
+ writeFileAtomicSync(resolveWorkspacePath(relPath), content, opts);
100
+ }
101
+
102
+ /**
103
+ * Write a JSON value under the workspace atomically.
104
+ * Pretty-printed with 2-space indent.
105
+ */
106
+ export async function writeWorkspaceJson(relPath: string, data: unknown, opts?: { mode?: number }): Promise<void> {
107
+ await writeFileAtomic(resolveWorkspacePath(relPath), JSON.stringify(data, null, 2), opts);
108
+ }
109
+
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
+ */
128
+ export function resolvePath(root: string, relPath: string): string {
129
+ return path.join(root, relPath);
130
+ }
131
+
132
+ /** Read text under an arbitrary root. Null on ENOENT; rethrows
133
+ * unexpected errors. */
134
+ export async function readTextUnder(root: string, relPath: string): Promise<string | null> {
135
+ try {
136
+ return await fs.promises.readFile(path.join(root, relPath), "utf-8");
137
+ } catch (err) {
138
+ return rethrowUnexpected(err, `readTextUnder(${relPath})`);
139
+ }
140
+ }
141
+
142
+ /** Write atomically under an arbitrary root. */
143
+ export async function writeTextUnder(root: string, relPath: string, content: string): Promise<void> {
144
+ await writeFileAtomic(path.join(root, relPath), content);
145
+ }
146
+
147
+ /** Sync read text under a root. Null on ENOENT. */
148
+ export function readTextUnderSync(root: string, relPath: string): string | null {
149
+ try {
150
+ return fs.readFileSync(path.join(root, relPath), "utf-8");
151
+ } catch (err) {
152
+ return rethrowUnexpected(err, `readTextUnderSync(${relPath})`);
153
+ }
154
+ }
155
+
156
+ /** Sync readdir under a root. Empty on ENOENT. */
157
+ export function readdirUnderSync(root: string, relPath: string): string[] {
158
+ try {
159
+ return fs.readdirSync(path.join(root, relPath));
160
+ } catch (err) {
161
+ if (isEnoent(err)) return [];
162
+ log.error("workspace-io", `readdirUnderSync(${relPath})`, {
163
+ error: String(err),
164
+ });
165
+ throw err;
166
+ }
167
+ }
168
+
169
+ /** Readdir under a root. Empty on ENOENT; rethrows unexpected. */
170
+ export async function readdirUnder(root: string, relPath: string): Promise<string[]> {
171
+ try {
172
+ return await fs.promises.readdir(path.join(root, relPath));
173
+ } catch (err) {
174
+ if (isEnoent(err)) return [];
175
+ log.error("workspace-io", `readdirUnder(${relPath})`, {
176
+ error: String(err),
177
+ });
178
+ throw err;
179
+ }
180
+ }
181
+
182
+ /** Stat under a root. Null on ENOENT; rethrows unexpected. */
183
+ export async function statUnder(root: string, relPath: string): Promise<fs.Stats | null> {
184
+ try {
185
+ return await fs.promises.stat(path.join(root, relPath));
186
+ } catch (err) {
187
+ return rethrowUnexpected(err, `statUnder(${relPath})`);
188
+ }
189
+ }
190
+
191
+ /** Ensure a directory exists under a root. */
192
+ export async function ensureDirUnder(root: string, relPath: string): Promise<void> {
193
+ await fs.promises.mkdir(path.join(root, relPath), { recursive: true });
194
+ }
195
+
196
+ // ── Existence ───────────────────────────────────────────────────
197
+
198
+ /**
199
+ * Check whether a workspace-relative path exists on disk.
200
+ * Returns false on ENOENT; rethrows unexpected errors.
201
+ */
202
+ export function existsInWorkspace(relPath: string): boolean {
203
+ try {
204
+ fs.statSync(resolveWorkspacePath(relPath));
205
+ return true;
206
+ } catch (err) {
207
+ if (isEnoent(err)) return false;
208
+ log.error("workspace-io", `existsInWorkspace(${relPath})`, {
209
+ error: String(err),
210
+ });
211
+ throw err;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Ensure a workspace-relative directory exists. Creates it
217
+ * (including parents) if missing. Idempotent.
218
+ */
219
+ export function ensureWorkspaceDir(relPath: string): void {
220
+ fs.mkdirSync(resolveWorkspacePath(relPath), { recursive: true });
221
+ }
@@ -0,0 +1,59 @@
1
+ import { GoogleGenAI, type GenerateContentParameters } from "@google/genai";
2
+ import { env } from "../system/env.js";
3
+
4
+ export { isGeminiAvailable } from "../system/env.js";
5
+
6
+ export function getGeminiClient(): GoogleGenAI {
7
+ const apiKey = env.geminiApiKey;
8
+ if (!apiKey) throw new Error("GEMINI_API_KEY is not set");
9
+ return new GoogleGenAI({ apiKey });
10
+ }
11
+
12
+ // --- Image generation -----------------------------------------------
13
+
14
+ const DEFAULT_IMAGE_MODEL = "gemini-3.1-flash-image-preview";
15
+
16
+ const DEFAULT_IMAGE_CONFIG: GenerateContentParameters["config"] = {
17
+ responseModalities: ["TEXT", "IMAGE"],
18
+ imageConfig: { aspectRatio: "16:9" },
19
+ };
20
+
21
+ export interface GeminiImageResult {
22
+ // Raw base64 payload (no `data:` prefix). Undefined if Gemini
23
+ // declined to return an image, e.g. because the prompt was filtered.
24
+ imageData?: string;
25
+ // Optional text part returned alongside the image (or in lieu of
26
+ // it). Used as a fallback message when imageData is empty.
27
+ message?: string;
28
+ }
29
+
30
+ // Low-level wrapper around `ai.models.generateContent` that pulls
31
+ // the first inline image and text part out of the response. Use this
32
+ // when you need to pass custom `contents` (e.g. text + reference
33
+ // image for /edit-image). Pass `undefined` for `config` to omit it
34
+ // entirely from the request.
35
+ export async function generateGeminiImageContent(
36
+ contents: GenerateContentParameters["contents"],
37
+ config?: GenerateContentParameters["config"],
38
+ model: string = DEFAULT_IMAGE_MODEL,
39
+ ): Promise<GeminiImageResult> {
40
+ const ai = getGeminiClient();
41
+ const response = await ai.models.generateContent({
42
+ model,
43
+ contents,
44
+ ...(config && { config }),
45
+ });
46
+ const parts = response.candidates?.[0]?.content?.parts ?? [];
47
+ const result: GeminiImageResult = {};
48
+ for (const part of parts) {
49
+ if (part.text) result.message = part.text;
50
+ if (part.inlineData?.data) result.imageData = part.inlineData.data;
51
+ }
52
+ return result;
53
+ }
54
+
55
+ // Convenience wrapper for the common "text prompt → image" path.
56
+ // Uses the standard 16:9 image config.
57
+ export async function generateGeminiImageFromPrompt(prompt: string, model?: string): Promise<GeminiImageResult> {
58
+ return generateGeminiImageContent([{ text: prompt }], DEFAULT_IMAGE_CONFIG, model);
59
+ }
@@ -0,0 +1,69 @@
1
+ // .gitignore-aware path filtering for the Files API (#256 P2).
2
+ //
3
+ // Reads `.gitignore` files at each directory level during the tree
4
+ // walk and filters entries that match. Uses the `ignore` npm package
5
+ // which implements the full gitignore spec (negation, glob, anchoring,
6
+ // directory-only patterns, etc.).
7
+ //
8
+ // Design: the `GitignoreFilter` builds a chain of `ignore` instances
9
+ // as the walker descends. Each directory may add its own `.gitignore`
10
+ // rules on top of the parent's. The `ignores(relPath)` method tests
11
+ // the full chain so parent rules apply to children.
12
+ //
13
+ // Performance: `.gitignore` files are read synchronously during the
14
+ // walk (one readFileSync per directory that has a `.gitignore`). For
15
+ // the workspace scale (~hundreds of dirs) this is negligible. If
16
+ // profiling shows otherwise, cache the parsed ignore instances.
17
+
18
+ import fs from "fs";
19
+ import path from "path";
20
+ import ignore, { type Ignore } from "ignore";
21
+
22
+ export class GitignoreFilter {
23
+ private ig: Ignore;
24
+
25
+ constructor(rules?: string) {
26
+ this.ig = ignore();
27
+ if (rules) {
28
+ this.ig.add(rules);
29
+ }
30
+ }
31
+
32
+ /** Test whether a workspace-relative path should be hidden. */
33
+ ignores(relPath: string): boolean {
34
+ if (!relPath) return false;
35
+ return this.ig.ignores(relPath);
36
+ }
37
+
38
+ /** Create a child filter that inherits this filter's rules and
39
+ * adds any `.gitignore` found in `dirAbsPath`. */
40
+ childForDir(dirAbsPath: string): GitignoreFilter {
41
+ const child = new GitignoreFilter();
42
+ // Inherit parent rules
43
+ child.ig = ignore().add(this.ig);
44
+ // Add local .gitignore if present
45
+ const gitignorePath = path.join(dirAbsPath, ".gitignore");
46
+ try {
47
+ const content = fs.readFileSync(gitignorePath, "utf-8");
48
+ child.ig.add(content);
49
+ } catch {
50
+ // No .gitignore in this directory — just inherit parent
51
+ }
52
+ return child;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Create a root filter from the workspace's `.gitignore`.
58
+ * Returns a filter that always returns false (ignores nothing) if
59
+ * no `.gitignore` exists at the workspace root.
60
+ */
61
+ export function createRootFilter(workspaceRoot: string): GitignoreFilter {
62
+ const gitignorePath = path.join(workspaceRoot, ".gitignore");
63
+ try {
64
+ const content = fs.readFileSync(gitignorePath, "utf-8");
65
+ return new GitignoreFilter(content);
66
+ } catch {
67
+ return new GitignoreFilter();
68
+ }
69
+ }
@@ -0,0 +1,15 @@
1
+ // HTTP utility helpers shared across server code.
2
+
3
+ /**
4
+ * Safely extract the response body text. Returns empty string
5
+ * on any error (network reset, invalid encoding, etc.).
6
+ * Replaces the repeated `.text().catch(() => "")` pattern.
7
+ */
8
+ export async function safeResponseText(res: Response, maxLength = 200): Promise<string> {
9
+ try {
10
+ const text = await res.text();
11
+ return text.slice(0, maxLength);
12
+ } catch {
13
+ return "";
14
+ }
15
+ }