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,294 @@
1
+ import { Router, Request, Response } from "express";
2
+ import fs from "fs";
3
+ import { readdir, stat } from "fs/promises";
4
+ import { readTextSafe } from "../../utils/files/safe.js";
5
+ import path from "path";
6
+ import { workspacePath } from "../../workspace/workspace.js";
7
+ import { WORKSPACE_PATHS } from "../../workspace/paths.js";
8
+ import { readSessionMeta as readSessionMetaIO, readSessionJsonl, sessionJsonlAbsPath, sessionMetaAbsPath } from "../../utils/files/session-io.js";
9
+ import { readManifest } from "../../workspace/chat-index/indexer.js";
10
+ import { resolveWithinRoot } from "../../utils/files/safe.js";
11
+ import type { ChatIndexEntry } from "../../workspace/chat-index/types.js";
12
+ import { markRead, getSession } from "../../events/session-store/index.js";
13
+ import { notFound } from "../../utils/httpError.js";
14
+ import { API_ROUTES } from "../../../src/config/apiRoutes.js";
15
+ import { EVENT_TYPES } from "../../../src/types/events.js";
16
+ import type { SessionOrigin } from "../../../src/types/session.js";
17
+ import { env } from "../../system/env.js";
18
+ import { ONE_DAY_MS } from "../../utils/time.js";
19
+ import { encodeCursor, parseCursor, sessionChangeMs } from "./sessionsCursor.js";
20
+
21
+ interface SessionMeta {
22
+ roleId: string;
23
+ startedAt: string;
24
+ firstUserMessage?: string;
25
+ hasUnread?: boolean;
26
+ origin?: SessionOrigin;
27
+ }
28
+
29
+ async function readSessionMeta(__chatDir: string, id: string): Promise<SessionMeta | null> {
30
+ // Try new-style .json meta first
31
+ const meta = await readSessionMetaIO(id);
32
+ if (meta?.roleId && meta?.startedAt) {
33
+ return meta as SessionMeta;
34
+ }
35
+ // Legacy: read first line of .jsonl
36
+ const jsonl = await readSessionJsonl(id);
37
+ if (jsonl) {
38
+ const first = jsonl.split("\n").find(Boolean);
39
+ if (first) {
40
+ try {
41
+ const parsed = JSON.parse(first);
42
+ if (parsed.roleId && parsed.startedAt) return parsed;
43
+ } catch {
44
+ // ignore
45
+ }
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+ export interface SessionSummary {
52
+ id: string;
53
+ roleId: string;
54
+ startedAt: string;
55
+ // ISO timestamp of the jsonl file's most recent mtime — i.e. the
56
+ // last time the session had an event appended. Clients sort the
57
+ // sidebar history list by this so active sessions float to the top.
58
+ updatedAt: string;
59
+ preview: string;
60
+ // Populated when the chat indexer has produced a summary for this
61
+ // session. The frontend renders `summary` as a smaller second line
62
+ // under the preview in the history popup. See #123.
63
+ summary?: string;
64
+ keywords?: string[];
65
+ // Where this session originated (#486). Missing = "human".
66
+ origin?: SessionOrigin;
67
+ // Live state from the in-memory session store. Absent when the
68
+ // session has no active entry in the store (i.e. idle / historical).
69
+ isRunning?: boolean;
70
+ hasUnread?: boolean;
71
+ statusMessage?: string;
72
+ }
73
+
74
+ // Public response envelope for GET /api/sessions (issue #205).
75
+ //
76
+ // `cursor` — opaque string clients echo back as `?since=` on the
77
+ // next call to receive only sessions that have changed.
78
+ // `deletedIds` — always `[]` for now (no session-delete code path
79
+ // exists yet). Kept in the shape so the client already
80
+ // merges it; when deletion lands, populating this will
81
+ // be a server-only change.
82
+ interface SessionsResponse {
83
+ sessions: SessionSummary[];
84
+ cursor: string;
85
+ deletedIds: string[];
86
+ }
87
+
88
+ interface SessionsQuery {
89
+ since?: string;
90
+ }
91
+
92
+ const router = Router();
93
+
94
+ // Sessions older than this are excluded from the listing. Set
95
+ // SESSIONS_LIST_WINDOW_DAYS to override (0 = no cutoff).
96
+ const WINDOW_MS = env.sessionsListWindowDays * ONE_DAY_MS;
97
+
98
+ // Read the full session list off disk. Each row carries its
99
+ // `changeMs` — the later of the jsonl mtime and the chat-index
100
+ // `indexedAt` — so the handler can filter against `?since=` and
101
+ // compute the new cursor without re-statting anything.
102
+ export async function loadAllSessions(): Promise<{ summary: SessionSummary; changeMs: number }[]> {
103
+ const chatDir = WORKSPACE_PATHS.chat;
104
+ const manifest = await readManifest(workspacePath);
105
+ const indexById = new Map<string, ChatIndexEntry>(manifest.entries.map((e) => [e.id, e]));
106
+ const cutoff = WINDOW_MS > 0 ? Date.now() - WINDOW_MS : 0;
107
+
108
+ const files = (await readdir(chatDir)).filter((f) => f.endsWith(".jsonl"));
109
+ const rows = await Promise.all(
110
+ files.map(async (file) => {
111
+ const id = file.replace(".jsonl", "");
112
+ try {
113
+ // stat only — no readFile on .jsonl content
114
+ const fileStat = await stat(sessionJsonlAbsPath(id));
115
+ if (cutoff > 0 && fileStat.mtimeMs < cutoff) return null;
116
+
117
+ const meta = await readSessionMeta(chatDir, id);
118
+ if (!meta) return null;
119
+
120
+ // The meta sidecar bumps its mtime on hasUnread / origin
121
+ // writes — feed it into changeMs so cursor-based refetches
122
+ // pick up drains of background generations (which only touch
123
+ // meta, not the jsonl). Missing stat (brand-new session
124
+ // before its first meta write) contributes 0.
125
+ const metaMtimeMs = await stat(sessionMetaAbsPath(id))
126
+ .then((s) => s.mtimeMs)
127
+ .catch(() => 0);
128
+
129
+ const indexEntry = indexById.get(id);
130
+ // Prefer AI title → meta.firstUserMessage → empty.
131
+ // `summary` and `keywords` are spread conditionally
132
+ // to respect the server tsconfig's
133
+ // exactOptionalPropertyTypes.
134
+ const preview = indexEntry?.title ?? meta.firstUserMessage ?? "";
135
+
136
+ const live = getSession(id);
137
+ const summary: SessionSummary = {
138
+ id,
139
+ roleId: meta.roleId,
140
+ startedAt: meta.startedAt,
141
+ updatedAt: new Date(fileStat.mtimeMs).toISOString(),
142
+ preview,
143
+ hasUnread: live?.hasUnread ?? meta.hasUnread ?? false,
144
+ };
145
+ if (meta.origin) summary.origin = meta.origin;
146
+ if (indexEntry?.summary !== undefined) summary.summary = indexEntry.summary;
147
+ if (indexEntry?.keywords !== undefined) summary.keywords = indexEntry.keywords;
148
+ if (live) {
149
+ // Background generations (image/audio/movie) keep the session
150
+ // "busy" even when the agent turn has ended, so the sidebar
151
+ // indicator stays lit across view navigation.
152
+ summary.isRunning = live.isRunning || Object.keys(live.pendingGenerations).length > 0;
153
+ summary.statusMessage = live.statusMessage;
154
+ }
155
+ return {
156
+ summary,
157
+ changeMs: sessionChangeMs(fileStat.mtimeMs, indexEntry?.indexedAt, metaMtimeMs),
158
+ };
159
+ } catch {
160
+ return null;
161
+ }
162
+ }),
163
+ );
164
+ return rows.filter((r): r is { summary: SessionSummary; changeMs: number } => r !== null);
165
+ }
166
+
167
+ router.get(API_ROUTES.sessions.list, async (req: Request<object, SessionsResponse, object, SessionsQuery>, res: Response<SessionsResponse>) => {
168
+ try {
169
+ const sinceMs = parseCursor(req.query.since);
170
+ const rows = await loadAllSessions();
171
+
172
+ // Cursor = max(changeMs) across every visible session, regardless
173
+ // of whether it's in the diff. Echoing the same cursor back on an
174
+ // empty diff (nothing changed since `?since=`) is fine; the
175
+ // client no-ops.
176
+ const maxChangeMs = rows.reduce((acc, r) => Math.max(acc, r.changeMs), 0);
177
+
178
+ const filtered = sinceMs > 0 ? rows.filter((r) => r.changeMs > sinceMs) : rows;
179
+
180
+ const sessions = filtered.map((r) => r.summary);
181
+ sessions.sort((a, b) => {
182
+ const byUpdated = new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
183
+ if (byUpdated !== 0) return byUpdated;
184
+ return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
185
+ });
186
+
187
+ res.json({
188
+ sessions,
189
+ cursor: encodeCursor(maxChangeMs),
190
+ // No session-delete code path exists today — issue #205 picked
191
+ // approach A (tombstones) so the client already merges this
192
+ // field; populating it becomes a server-only change when
193
+ // deletion lands.
194
+ deletedIds: [],
195
+ });
196
+ } catch {
197
+ res.json({ sessions: [], cursor: encodeCursor(0), deletedIds: [] });
198
+ }
199
+ });
200
+
201
+ interface SessionIdParams {
202
+ id: string;
203
+ }
204
+
205
+ interface SessionErrorResponse {
206
+ error: string;
207
+ }
208
+
209
+ router.get(API_ROUTES.sessions.detail, async (req: Request<SessionIdParams>, res: Response<unknown[] | SessionErrorResponse>) => {
210
+ const { id } = req.params;
211
+ const chatDir = WORKSPACE_PATHS.chat;
212
+ try {
213
+ const meta = await readSessionMeta(chatDir, id);
214
+ const content = await readSessionJsonl(id);
215
+ if (!content) {
216
+ notFound(res, `Session ${id} not found`);
217
+ return;
218
+ }
219
+ const entries = (
220
+ await Promise.all(
221
+ content
222
+ .split("\n")
223
+ .filter(Boolean)
224
+ .map(async (line) => {
225
+ try {
226
+ const entry = JSON.parse(line);
227
+ // Skip legacy metadata entries now stored in .json
228
+ if (entry.type === EVENT_TYPES.sessionMeta || entry.type === EVENT_TYPES.claudeSessionId) return null;
229
+ // For presentMulmoScript results, re-read the script from disk
230
+ if (
231
+ entry.source === "tool" &&
232
+ entry.type === EVENT_TYPES.toolResult &&
233
+ entry.result?.toolName === "presentMulmoScript" &&
234
+ entry.result?.data?.filePath
235
+ ) {
236
+ try {
237
+ // Realpath-based traversal check defeats symlink
238
+ // escapes — see resolveWithinRoot in utils/fs.ts.
239
+ // Resolve the stories dir's realpath so the
240
+ // boundary check works even when stories/ itself
241
+ // is a legitimate symlink to another disk.
242
+ const storiesDir = path.resolve(WORKSPACE_PATHS.stories);
243
+ let storiesReal: string;
244
+ try {
245
+ storiesReal = fs.realpathSync(storiesDir);
246
+ } catch {
247
+ return entry;
248
+ }
249
+ const scriptRelPath: string = entry.result.data.filePath;
250
+ if (path.isAbsolute(scriptRelPath)) return entry;
251
+ // Strip optional "stories/" prefix so the
252
+ // remainder is relative to storiesReal.
253
+ const relFromStories = scriptRelPath.startsWith("stories/") ? scriptRelPath.slice("stories/".length) : scriptRelPath;
254
+ const scriptPath = resolveWithinRoot(storiesReal, relFromStories);
255
+ if (!scriptPath) return entry;
256
+ const scriptJson = (await readTextSafe(scriptPath)) ?? "";
257
+ return {
258
+ ...entry,
259
+ result: {
260
+ ...entry.result,
261
+ data: {
262
+ ...entry.result.data,
263
+ script: JSON.parse(scriptJson),
264
+ },
265
+ },
266
+ };
267
+ } catch {
268
+ // file missing — return original entry
269
+ }
270
+ }
271
+ return entry;
272
+ } catch {
273
+ return null;
274
+ }
275
+ }),
276
+ )
277
+ ).filter(Boolean);
278
+ // Prepend metadata as session_meta entry for the frontend
279
+ const result = meta ? [{ type: EVENT_TYPES.sessionMeta, ...meta }, ...entries] : entries;
280
+ res.json(result);
281
+ } catch {
282
+ notFound(res, "Session not found");
283
+ }
284
+ });
285
+
286
+ // Mark a session as read (clears the hasUnread flag in the session store).
287
+ // Awaits persistence so the response only arrives after the disk write
288
+ // completes — prevents the client from refetching stale hasUnread values.
289
+ router.post(API_ROUTES.sessions.markRead, async (req: Request<SessionIdParams>, res: Response<{ ok: boolean }>) => {
290
+ await markRead(req.params.id);
291
+ res.json({ ok: true });
292
+ });
293
+
294
+ export default router;
@@ -0,0 +1,59 @@
1
+ // Cursor logic for `GET /api/sessions?since=<cursor>` (issue #205).
2
+ //
3
+ // Kept separate from `sessions.ts` so the pure logic can be unit
4
+ // tested without an Express harness.
5
+ //
6
+ // The cursor is deliberately opaque to the client: today it encodes
7
+ // the max "change timestamp" (ms since epoch) as `"v1:<ms>"`, where a
8
+ // session's change timestamp is `max(jsonlMtimeMs, indexedAtMs)`. We
9
+ // prefix with `v1:` so a future encoding change (e.g. adding a
10
+ // deletion generation counter for approach A when deletion lands)
11
+ // can bump the prefix without clients caring — they always echo back
12
+ // whatever the server handed them.
13
+
14
+ const CURSOR_PREFIX = "v1:";
15
+
16
+ /**
17
+ * Encode a change timestamp (ms) as an opaque cursor string.
18
+ *
19
+ * `changeMs <= 0` is allowed and yields `"v1:0"` — that's the
20
+ * "beginning of time" cursor a client will never hold but which we
21
+ * fall back to when an incoming cursor is malformed.
22
+ */
23
+ export function encodeCursor(changeMs: number): string {
24
+ const ms = Number.isFinite(changeMs) && changeMs > 0 ? Math.floor(changeMs) : 0;
25
+ return `${CURSOR_PREFIX}${ms}`;
26
+ }
27
+
28
+ /**
29
+ * Parse an incoming `?since=` cursor back to the ms timestamp it
30
+ * encodes. Anything the client sends that we don't recognise — old
31
+ * format, truncated, typo, empty — returns 0 so the client gets a
32
+ * full resend instead of a broken sidebar. This is intentionally
33
+ * forgiving; the failure mode is "downloads slightly more than
34
+ * needed once" which is the behaviour clients had pre-#205 anyway.
35
+ */
36
+ export function parseCursor(raw: unknown): number {
37
+ if (typeof raw !== "string") return 0;
38
+ if (!raw.startsWith(CURSOR_PREFIX)) return 0;
39
+ const n = Number(raw.slice(CURSOR_PREFIX.length));
40
+ return Number.isFinite(n) && n > 0 ? n : 0;
41
+ }
42
+
43
+ /**
44
+ * Compute the per-session "change timestamp" in ms — the latest of:
45
+ * - jsonl mtime (new user/assistant turn)
46
+ * - chat-index `indexedAt` (AI-generated title / summary, updated in
47
+ * the background and doesn't touch the jsonl)
48
+ * - meta json mtime (hasUnread flip, origin update — also sidecar
49
+ * to the jsonl)
50
+ *
51
+ * Missing / malformed `indexedAt` or `metaMtimeMs` contributes 0 so
52
+ * they don't pull the timestamp backward.
53
+ */
54
+ export function sessionChangeMs(jsonlMtimeMs: number, indexedAtIso: string | undefined, metaMtimeMs: number | undefined = undefined): number {
55
+ const indexedAtMs = indexedAtIso !== undefined ? new Date(indexedAtIso).getTime() : NaN;
56
+ const safeIndexed = Number.isFinite(indexedAtMs) ? indexedAtMs : 0;
57
+ const safeMeta = typeof metaMtimeMs === "number" && Number.isFinite(metaMtimeMs) ? metaMtimeMs : 0;
58
+ return Math.max(jsonlMtimeMs, safeIndexed, safeMeta);
59
+ }
@@ -0,0 +1,195 @@
1
+ // REST surface for Claude Code skills.
2
+ //
3
+ // GET /api/skills → { skills: SkillSummary[] } phase 0
4
+ // GET /api/skills/:name → { skill: Skill } | 404 phase 0
5
+ // POST /api/skills → { saved: true, path } | 400/409 phase 1
6
+ // PUT /api/skills/:name → { updated: true, path } | 400/403/404 phase 2
7
+ // DELETE /api/skills/:name → { deleted: true } | 400/403/404 phase 1
8
+ //
9
+ // Discovery reads both ~/.claude/skills/ (user) and
10
+ // <workspace>/.claude/skills/ (project); project wins on name
11
+ // collision. Writes are confined to the project scope —
12
+ // `saveProjectSkill` / `updateProjectSkill` / `deleteProjectSkill`
13
+ // enforce that.
14
+
15
+ import { Router, Request, Response } from "express";
16
+ import { deleteProjectSkill, discoverSkills, saveProjectSkill, updateProjectSkill } from "../../workspace/skills/index.js";
17
+ import type { Skill, SkillSummary } from "../../workspace/skills/index.js";
18
+ import { workspacePath } from "../../workspace/workspace.js";
19
+ import { API_ROUTES } from "../../../src/config/apiRoutes.js";
20
+ import { log } from "../../system/logger/index.js";
21
+ import { refreshScheduledSkills } from "../../workspace/skills/scheduler.js";
22
+ import { logBackgroundError } from "../../utils/logBackgroundError.js";
23
+ import { badRequest, conflict, forbidden, notFound } from "../../utils/httpError.js";
24
+
25
+ const router = Router();
26
+
27
+ interface SkillsListResponse {
28
+ skills: SkillSummary[];
29
+ }
30
+
31
+ interface SkillDetailResponse {
32
+ skill: Skill;
33
+ }
34
+
35
+ interface ErrorResponse {
36
+ error: string;
37
+ }
38
+
39
+ interface SaveSkillBody {
40
+ name?: unknown;
41
+ description?: unknown;
42
+ body?: unknown;
43
+ }
44
+
45
+ interface SaveSkillResponse {
46
+ saved: true;
47
+ path: string;
48
+ }
49
+
50
+ interface DeleteSkillResponse {
51
+ deleted: true;
52
+ name: string;
53
+ }
54
+
55
+ router.get(API_ROUTES.skills.list, async (_req: Request, res: Response<SkillsListResponse>) => {
56
+ const skills = await discoverSkills({ workspaceRoot: workspacePath });
57
+ res.json({
58
+ skills: skills.map((s) => ({
59
+ name: s.name,
60
+ description: s.description,
61
+ source: s.source,
62
+ })),
63
+ });
64
+ });
65
+
66
+ router.get(API_ROUTES.skills.detail, async (req: Request<{ name: string }>, res: Response<SkillDetailResponse | ErrorResponse>) => {
67
+ const skills = await discoverSkills({ workspaceRoot: workspacePath });
68
+ const skill = skills.find((s) => s.name === req.params.name);
69
+ if (!skill) {
70
+ notFound(res, `skill not found: ${req.params.name}`);
71
+ return;
72
+ }
73
+ res.json({ skill });
74
+ });
75
+
76
+ router.post(API_ROUTES.skills.create, async (req: Request<object, unknown, SaveSkillBody>, res: Response<SaveSkillResponse | ErrorResponse>) => {
77
+ const { name, description, body } = req.body ?? {};
78
+ if (typeof name !== "string") {
79
+ badRequest(res, "name must be a string");
80
+ return;
81
+ }
82
+ if (typeof description !== "string") {
83
+ badRequest(res, "description must be a string");
84
+ return;
85
+ }
86
+ if (typeof body !== "string") {
87
+ badRequest(res, "body must be a string");
88
+ return;
89
+ }
90
+ const result = await saveProjectSkill({
91
+ workspaceRoot: workspacePath,
92
+ name,
93
+ description,
94
+ body,
95
+ });
96
+ if (result.kind === "saved") {
97
+ log.info("skills", "saved", { name });
98
+ refreshScheduledSkills().catch(logBackgroundError("skills"));
99
+ res.json({ saved: true, path: result.path });
100
+ return;
101
+ }
102
+ if (result.kind === "invalid-slug") {
103
+ badRequest(
104
+ res,
105
+ `invalid slug: "${result.slug}". Use lowercase letters, digits, and hyphens (1-64 chars, no leading/trailing hyphen, no consecutive hyphens).`,
106
+ );
107
+ return;
108
+ }
109
+ if (result.kind === "missing-field") {
110
+ badRequest(res, `${result.field} must be a non-empty string`);
111
+ return;
112
+ }
113
+ if (result.kind === "exists") {
114
+ conflict(res, `skill already exists: ${result.name}. Choose a different name or delete the existing one first.`);
115
+ }
116
+ });
117
+
118
+ interface UpdateSkillBody {
119
+ description?: unknown;
120
+ body?: unknown;
121
+ }
122
+
123
+ interface UpdateSkillResponse {
124
+ updated: true;
125
+ path: string;
126
+ }
127
+
128
+ router.put(API_ROUTES.skills.update, async (req: Request<{ name: string }, unknown, UpdateSkillBody>, res: Response<UpdateSkillResponse | ErrorResponse>) => {
129
+ const { name } = req.params;
130
+ const { description, body } = req.body ?? {};
131
+ if (typeof description !== "string") {
132
+ badRequest(res, "description must be a string");
133
+ return;
134
+ }
135
+ if (typeof body !== "string") {
136
+ badRequest(res, "body must be a string");
137
+ return;
138
+ }
139
+ const result = await updateProjectSkill({
140
+ workspaceRoot: workspacePath,
141
+ name,
142
+ description,
143
+ body,
144
+ });
145
+ if (result.kind === "updated") {
146
+ log.info("skills", "updated", { name });
147
+ refreshScheduledSkills().catch(logBackgroundError("skills"));
148
+ res.json({ updated: true, path: result.path });
149
+ return;
150
+ }
151
+ if (result.kind === "invalid-slug") {
152
+ badRequest(res, `invalid slug: "${result.slug}"`);
153
+ return;
154
+ }
155
+ if (result.kind === "missing-field") {
156
+ badRequest(res, `${result.field} must be a non-empty string`);
157
+ return;
158
+ }
159
+ if (result.kind === "user-scope") {
160
+ forbidden(res, `cannot update user-scope skill "${result.name}" — only project-scope skills are writable.`);
161
+ return;
162
+ }
163
+ if (result.kind === "not-found") {
164
+ notFound(res, `skill not found: ${result.name}`);
165
+ }
166
+ });
167
+
168
+ router.delete(API_ROUTES.skills.remove, async (req: Request<{ name: string }>, res: Response<DeleteSkillResponse | ErrorResponse>) => {
169
+ const result = await deleteProjectSkill({
170
+ workspaceRoot: workspacePath,
171
+ name: req.params.name,
172
+ });
173
+ if (result.kind === "deleted") {
174
+ log.info("skills", "deleted", { name: result.name });
175
+ refreshScheduledSkills().catch(logBackgroundError("skills"));
176
+ res.json({ deleted: true, name: result.name });
177
+ return;
178
+ }
179
+ if (result.kind === "invalid-slug") {
180
+ badRequest(res, `invalid slug: "${result.slug}"`);
181
+ return;
182
+ }
183
+ if (result.kind === "user-scope") {
184
+ forbidden(
185
+ res,
186
+ `cannot delete user-scope skill "${result.name}" — only project-scope skills under ~/mulmoclaude/.claude/skills/ are writable from MulmoClaude.`,
187
+ );
188
+ return;
189
+ }
190
+ if (result.kind === "not-found") {
191
+ notFound(res, `skill not found: ${result.name}`);
192
+ }
193
+ });
194
+
195
+ export default router;