mulmoclaude 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (486) hide show
  1. package/Dockerfile.sandbox +100 -0
  2. package/README.md +17 -4
  3. package/bin/mulmoclaude.js +46 -15
  4. package/bin/prepare-dist.js +18 -2
  5. package/client/assets/chunk-CernVdwh.js +1 -0
  6. package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
  7. package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
  8. package/client/assets/index-BbgSjFQ8.js +4968 -0
  9. package/client/assets/index-ECD0lgIv.css +2 -0
  10. package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
  11. package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
  12. package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
  13. package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
  14. package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
  15. package/client/assets/vue-1e_vz2LW.js +1 -0
  16. package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
  17. package/client/index.html +33 -2
  18. package/package.json +20 -18
  19. package/sandbox-entrypoint.sh +106 -0
  20. package/server/accounting/accountNormalize.ts +32 -0
  21. package/server/accounting/defaultAccounts.ts +87 -0
  22. package/server/accounting/eventPublisher.ts +51 -0
  23. package/server/accounting/journal.ts +252 -0
  24. package/server/accounting/openingBalances.ts +114 -0
  25. package/server/accounting/report.ts +237 -0
  26. package/server/accounting/service.ts +718 -0
  27. package/server/accounting/snapshotCache.ts +333 -0
  28. package/server/accounting/timeSeries.ts +265 -0
  29. package/server/accounting/types.ts +148 -0
  30. package/server/agent/activeTools.ts +128 -0
  31. package/server/agent/attachmentConverter.ts +10 -5
  32. package/server/agent/backend/claude-code.ts +8 -2
  33. package/server/agent/backend/types.ts +1 -1
  34. package/server/agent/config.ts +101 -31
  35. package/server/agent/index.ts +45 -33
  36. package/server/agent/mcp-server.ts +146 -69
  37. package/server/agent/mcp-tools/index.ts +1 -5
  38. package/server/agent/mcp-tools/notify.ts +2 -22
  39. package/server/agent/mcp-tools/x.ts +0 -4
  40. package/server/agent/mcpHealth.ts +168 -0
  41. package/server/agent/plugin-names.ts +20 -77
  42. package/server/agent/prompt.ts +259 -51
  43. package/server/agent/resumeFailover.ts +1 -1
  44. package/server/agent/stream.ts +0 -1
  45. package/server/api/auth/bearerAuth.ts +5 -5
  46. package/server/api/csrfGuard.ts +1 -1
  47. package/server/api/routes/accounting.ts +366 -0
  48. package/server/api/routes/agent.ts +509 -46
  49. package/server/api/routes/attachment.ts +104 -0
  50. package/server/api/routes/chart.ts +2 -1
  51. package/server/api/routes/config.ts +12 -12
  52. package/server/api/routes/files.ts +105 -48
  53. package/server/api/routes/image.ts +70 -25
  54. package/server/api/routes/journal.ts +35 -0
  55. package/server/api/routes/mulmo-script.ts +358 -118
  56. package/server/api/routes/mulmoScriptValidate.ts +1 -1
  57. package/server/api/routes/news.ts +1 -1
  58. package/server/api/routes/notifications.ts +92 -22
  59. package/server/api/routes/notifier.ts +98 -0
  60. package/server/api/routes/pdf.ts +188 -48
  61. package/server/api/routes/plugins.ts +34 -14
  62. package/server/api/routes/presentHtml.ts +58 -3
  63. package/server/api/routes/roles.ts +1 -8
  64. package/server/api/routes/runtime-plugin.ts +224 -0
  65. package/server/api/routes/scheduler.ts +7 -5
  66. package/server/api/routes/schedulerHandlers.ts +1 -1
  67. package/server/api/routes/schedulerTasks.ts +8 -7
  68. package/server/api/routes/sessions.ts +234 -121
  69. package/server/api/routes/skills.ts +56 -51
  70. package/server/api/routes/sources.ts +52 -45
  71. package/server/api/routes/translation.ts +44 -0
  72. package/server/api/routes/wiki/frontmatter.ts +13 -65
  73. package/server/api/routes/wiki/history.ts +261 -0
  74. package/server/api/routes/wiki/pageIndex.ts +1 -1
  75. package/server/api/routes/wiki.ts +50 -26
  76. package/server/events/file-change.ts +83 -0
  77. package/server/events/notifications.ts +247 -91
  78. package/server/events/pub-sub/index.ts +1 -1
  79. package/server/events/relay-client.ts +5 -5
  80. package/server/events/scheduler-adapter.ts +2 -2
  81. package/server/events/session-store/index.ts +110 -22
  82. package/server/events/task-manager/index.ts +10 -9
  83. package/server/index.ts +509 -33
  84. package/server/notifier/engine.ts +419 -0
  85. package/server/notifier/legacy-adapters.ts +76 -0
  86. package/server/notifier/runtime-api.ts +74 -0
  87. package/server/notifier/store.ts +70 -0
  88. package/server/notifier/types.ts +121 -0
  89. package/server/plugins/dev-loader.ts +171 -0
  90. package/server/plugins/dev-watcher.ts +150 -0
  91. package/server/plugins/diagnostics.ts +188 -0
  92. package/server/plugins/preset-list.ts +52 -0
  93. package/server/plugins/preset-loader.ts +112 -0
  94. package/server/plugins/runtime-chat-api.ts +38 -0
  95. package/server/plugins/runtime-loader.ts +430 -0
  96. package/server/plugins/runtime-registry.ts +112 -0
  97. package/server/plugins/runtime-tasks-api.ts +50 -0
  98. package/server/plugins/runtime.ts +378 -0
  99. package/server/services/translation/cache.ts +72 -0
  100. package/server/services/translation/index.ts +106 -0
  101. package/server/services/translation/llm.ts +140 -0
  102. package/server/services/translation/types.ts +35 -0
  103. package/server/system/credentials.ts +13 -2
  104. package/server/system/env.ts +6 -1
  105. package/server/system/logger/formatters.ts +46 -4
  106. package/server/system/logger/index.ts +4 -4
  107. package/server/system/logger/sinks.ts +26 -5
  108. package/server/system/logger/types.ts +2 -2
  109. package/server/utils/dev-plugin-args.d.mts +11 -0
  110. package/server/utils/dev-plugin-args.mjs +43 -0
  111. package/server/utils/errors.ts +13 -4
  112. package/server/utils/files/accounting-io.ts +295 -0
  113. package/server/utils/files/atomic.ts +17 -49
  114. package/server/utils/files/attachment-store.ts +182 -0
  115. package/server/utils/files/html-io.ts +1 -7
  116. package/server/utils/files/html-store.ts +19 -0
  117. package/server/utils/files/image-store.ts +20 -22
  118. package/server/utils/files/index.ts +5 -15
  119. package/server/utils/files/journal-io.ts +7 -35
  120. package/server/utils/files/json.ts +2 -29
  121. package/server/utils/files/markdown-image-fill.ts +6 -37
  122. package/server/utils/files/markdown-store.ts +6 -21
  123. package/server/utils/files/naming.ts +3 -39
  124. package/server/utils/files/plugins-io.ts +100 -0
  125. package/server/utils/files/reference-dirs-io.ts +1 -9
  126. package/server/utils/files/roles-io.ts +2 -10
  127. package/server/utils/files/safe.ts +17 -19
  128. package/server/utils/files/scheduler-io.ts +1 -7
  129. package/server/utils/files/scheduler-overrides-io.ts +3 -12
  130. package/server/utils/files/session-io.ts +21 -30
  131. package/server/utils/files/spreadsheet-store.ts +9 -22
  132. package/server/utils/files/translation-io.ts +46 -0
  133. package/server/utils/files/user-tasks-io.ts +1 -7
  134. package/server/utils/files/workspace-io.ts +3 -79
  135. package/server/utils/gemini.ts +33 -11
  136. package/server/utils/html/htmlArtifactSplicer.ts +41 -0
  137. package/server/utils/markdown/frontmatter.ts +112 -0
  138. package/server/utils/regex.ts +56 -0
  139. package/server/utils/router.ts +41 -0
  140. package/server/utils/slug.ts +5 -3
  141. package/server/utils/time.ts +12 -0
  142. package/server/workspace/chat-index/indexer.ts +15 -2
  143. package/server/workspace/chat-index/summarizer.ts +1 -1
  144. package/server/workspace/custom-dirs.ts +1 -1
  145. package/server/workspace/helps/gemini.md +1 -1
  146. package/server/workspace/helps/guide.md +61 -0
  147. package/server/workspace/helps/index.md +4 -0
  148. package/server/workspace/helps/presenthtml.md +80 -0
  149. package/server/workspace/helps/sandbox.md +7 -0
  150. package/server/workspace/helps/storyteller.md +101 -0
  151. package/server/workspace/helps/telegram.md +1 -0
  152. package/server/workspace/helps/wiki.md +9 -7
  153. package/server/workspace/journal/archivist-cli.ts +7 -33
  154. package/server/workspace/journal/archivist-schemas.ts +5 -43
  155. package/server/workspace/journal/dailyPass.ts +34 -187
  156. package/server/workspace/journal/diff.ts +3 -28
  157. package/server/workspace/journal/index.ts +10 -81
  158. package/server/workspace/journal/indexFile.ts +3 -24
  159. package/server/workspace/journal/latestDaily.ts +51 -0
  160. package/server/workspace/journal/memoryExtractor.ts +4 -20
  161. package/server/workspace/journal/optimizationPass.ts +4 -21
  162. package/server/workspace/journal/paths.ts +4 -23
  163. package/server/workspace/journal/state.ts +6 -29
  164. package/server/workspace/memory/io.ts +213 -0
  165. package/server/workspace/memory/llm-classifier.ts +158 -0
  166. package/server/workspace/memory/migrate.ts +263 -0
  167. package/server/workspace/memory/run.ts +84 -0
  168. package/server/workspace/memory/topic-cluster.ts +218 -0
  169. package/server/workspace/memory/topic-detect.ts +67 -0
  170. package/server/workspace/memory/topic-index-hook.ts +128 -0
  171. package/server/workspace/memory/topic-io.ts +180 -0
  172. package/server/workspace/memory/topic-migrate.ts +248 -0
  173. package/server/workspace/memory/topic-run.ts +172 -0
  174. package/server/workspace/memory/topic-swap.ts +135 -0
  175. package/server/workspace/memory/topic-types.ts +142 -0
  176. package/server/workspace/memory/types.ts +83 -0
  177. package/server/workspace/news/reader.ts +4 -5
  178. package/server/workspace/paths.ts +124 -47
  179. package/server/workspace/roles.ts +2 -11
  180. package/server/workspace/skills/parser.ts +38 -55
  181. package/server/workspace/skills/user-tasks.ts +1 -2
  182. package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
  183. package/server/workspace/skills-preset.ts +196 -0
  184. package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
  185. package/server/workspace/sources/fetchers/index.ts +1 -1
  186. package/server/workspace/sources/fetchers/rssParser.ts +1 -1
  187. package/server/workspace/sources/pipeline/index.ts +2 -2
  188. package/server/workspace/sources/pipeline/notify.ts +3 -3
  189. package/server/workspace/sources/pipeline/write.ts +2 -2
  190. package/server/workspace/sources/registry.ts +39 -61
  191. package/server/workspace/sources/robots.ts +1 -1
  192. package/server/workspace/tool-trace/classify.ts +2 -1
  193. package/server/workspace/tool-trace/index.ts +1 -1
  194. package/server/workspace/tool-trace/writeSearch.ts +6 -1
  195. package/server/workspace/wiki-backlinks/index.ts +19 -7
  196. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
  197. package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
  198. package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
  199. package/server/workspace/wiki-history/provision.ts +181 -0
  200. package/server/workspace/wiki-pages/io.ts +217 -0
  201. package/server/workspace/wiki-pages/snapshot.ts +380 -0
  202. package/server/workspace/workspace.ts +75 -13
  203. package/src/App.vue +115 -40
  204. package/src/_runtime/protocol-vue.ts +21 -0
  205. package/src/_runtime/vue.ts +22 -0
  206. package/src/components/ChatInput.vue +14 -10
  207. package/src/components/CopyChatButton.vue +76 -0
  208. package/src/components/FileContentRenderer.vue +67 -14
  209. package/src/components/FileTree.vue +2 -2
  210. package/src/components/FilesView.vue +17 -1
  211. package/src/components/NewsView.vue +16 -2
  212. package/src/components/NotificationBell.vue +320 -93
  213. package/src/components/PageChatComposer.vue +5 -4
  214. package/src/components/PluginLauncher.vue +42 -6
  215. package/src/components/PluginScopedRoot.vue +87 -0
  216. package/src/components/RoleSelector.vue +12 -1
  217. package/src/components/RolesView.vue +562 -0
  218. package/src/components/SentAttachmentChip.vue +102 -0
  219. package/src/components/SessionHistoryPanel.vue +109 -20
  220. package/src/components/SessionRoleIcon.vue +7 -4
  221. package/src/components/SessionSidebar.vue +20 -7
  222. package/src/components/SessionTabBar.vue +1 -1
  223. package/src/components/SettingsMcpTab.vue +4 -4
  224. package/src/components/SettingsModal.vue +2 -0
  225. package/src/components/SidebarHeader.vue +16 -5
  226. package/src/components/SourcesManager.vue +23 -9
  227. package/src/components/SourcesView.vue +1 -1
  228. package/src/components/StackView.vue +102 -6
  229. package/src/components/SuggestionsPanel.vue +105 -16
  230. package/src/components/SystemFileBanner.vue +1 -1
  231. package/src/components/TodoExplorer.vue +4 -5
  232. package/src/components/todo/TodoAddDialog.vue +2 -3
  233. package/src/components/todo/TodoEditDialog.vue +1 -2
  234. package/src/components/todo/TodoEditPanel.vue +2 -3
  235. package/src/components/todo/TodoKanbanView.vue +8 -5
  236. package/src/components/todo/TodoListView.vue +3 -5
  237. package/src/components/todo/TodoTableView.vue +7 -5
  238. package/src/composables/useAccountingChannel.ts +58 -0
  239. package/src/composables/useActiveSession.ts +4 -25
  240. package/src/composables/useAppApi.ts +6 -44
  241. package/src/composables/useClipboardCopy.ts +3 -20
  242. package/src/composables/useContentDisplay.ts +33 -2
  243. package/src/composables/useDevPluginReload.ts +23 -0
  244. package/src/composables/useDynamicFavicon.ts +5 -31
  245. package/src/composables/useEventListeners.ts +0 -20
  246. package/src/composables/useExpandedDirs.ts +4 -15
  247. package/src/composables/useFaviconState.ts +12 -46
  248. package/src/composables/useFileChange.ts +53 -0
  249. package/src/composables/useFreshPluginData.ts +6 -43
  250. package/src/composables/useHealth.ts +14 -43
  251. package/src/composables/useImageErrorRepair.ts +104 -0
  252. package/src/composables/useLatestDaily.ts +40 -0
  253. package/src/composables/useMarkdownDoc.ts +39 -0
  254. package/src/composables/useMarkdownLinkHandler.ts +1 -1
  255. package/src/composables/useMcpTools.ts +3 -16
  256. package/src/composables/useNotifications.ts +138 -112
  257. package/src/composables/usePdfDownload.ts +17 -3
  258. package/src/composables/usePendingCalls.ts +8 -26
  259. package/src/composables/usePluginErrorBoundary.ts +68 -0
  260. package/src/composables/usePubSub.ts +9 -17
  261. package/src/composables/useRunElapsed.ts +5 -22
  262. package/src/composables/useSandboxStatus.ts +4 -20
  263. package/src/composables/useSessionDerived.ts +7 -15
  264. package/src/composables/useSessionHistory.ts +70 -29
  265. package/src/composables/useSessionSync.ts +25 -3
  266. package/src/composables/useSkillsList.ts +59 -0
  267. package/src/composables/useTranslatedQueries.ts +109 -0
  268. package/src/config/apiRoutes.ts +181 -80
  269. package/src/config/historyFilters.ts +5 -3
  270. package/src/config/hostEvents.ts +17 -0
  271. package/src/config/mcpCatalog.ts +277 -5
  272. package/src/config/pubsubChannels.ts +134 -12
  273. package/src/config/roles.ts +212 -147
  274. package/src/config/systemFileDescriptors.ts +5 -5
  275. package/src/config/toolNames.ts +52 -30
  276. package/src/config/workspacePaths.ts +26 -2
  277. package/src/lang/de.ts +483 -27
  278. package/src/lang/en.ts +448 -27
  279. package/src/lang/es.ts +474 -27
  280. package/src/lang/fr.ts +476 -27
  281. package/src/lang/ja.ts +465 -27
  282. package/src/lang/ko.ts +466 -27
  283. package/src/lang/pt-BR.ts +473 -27
  284. package/src/lang/zh.ts +463 -27
  285. package/src/lib/vue-i18n.ts +1 -1
  286. package/src/lib/wiki-page/slug.ts +66 -0
  287. package/src/main.ts +85 -0
  288. package/src/plugins/_extras.ts +58 -0
  289. package/src/plugins/_generated/metas.ts +42 -0
  290. package/src/plugins/_generated/registrations.ts +44 -0
  291. package/src/plugins/_generated/server-bindings.ts +47 -0
  292. package/src/plugins/accounting/Preview.vue +106 -0
  293. package/src/plugins/accounting/View.vue +632 -0
  294. package/src/plugins/accounting/actions.ts +34 -0
  295. package/src/plugins/accounting/api.ts +301 -0
  296. package/src/plugins/accounting/components/AccountEditor.vue +250 -0
  297. package/src/plugins/accounting/components/AccountRow.vue +50 -0
  298. package/src/plugins/accounting/components/AccountsList.vue +102 -0
  299. package/src/plugins/accounting/components/AccountsModal.vue +300 -0
  300. package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
  301. package/src/plugins/accounting/components/BookSettings.vue +284 -0
  302. package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
  303. package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
  304. package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
  305. package/src/plugins/accounting/components/JournalList.vue +553 -0
  306. package/src/plugins/accounting/components/Ledger.vue +206 -0
  307. package/src/plugins/accounting/components/NewBookForm.vue +211 -0
  308. package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
  309. package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
  310. package/src/plugins/accounting/components/accountDraft.ts +13 -0
  311. package/src/plugins/accounting/components/accountNumbering.ts +103 -0
  312. package/src/plugins/accounting/components/accountValidation.ts +75 -0
  313. package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
  314. package/src/plugins/accounting/countries.ts +158 -0
  315. package/src/plugins/accounting/currencies.ts +64 -0
  316. package/src/plugins/accounting/dates.ts +51 -0
  317. package/src/plugins/accounting/definition.ts +199 -0
  318. package/src/plugins/accounting/fiscalYear.ts +136 -0
  319. package/src/plugins/accounting/index.ts +49 -0
  320. package/src/plugins/accounting/meta.ts +91 -0
  321. package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
  322. package/src/plugins/api.ts +125 -0
  323. package/src/plugins/canvas/View.vue +38 -28
  324. package/src/plugins/canvas/definition.ts +10 -8
  325. package/src/plugins/canvas/index.ts +15 -8
  326. package/src/plugins/canvas/meta.ts +12 -0
  327. package/src/plugins/chart/Preview.vue +1 -1
  328. package/src/plugins/chart/View.vue +2 -2
  329. package/src/plugins/chart/definition.ts +12 -2
  330. package/src/plugins/chart/index.ts +15 -7
  331. package/src/plugins/chart/meta.ts +18 -0
  332. package/src/plugins/editImages/definition.ts +44 -0
  333. package/src/plugins/editImages/index.ts +43 -0
  334. package/src/plugins/editImages/meta.ts +5 -0
  335. package/src/plugins/generateImage/View.vue +3 -1
  336. package/src/plugins/generateImage/definition.ts +2 -0
  337. package/src/plugins/generateImage/index.ts +13 -5
  338. package/src/plugins/generateImage/meta.ts +5 -0
  339. package/src/plugins/index.ts +35 -0
  340. package/src/plugins/manageRoles/Preview.vue +7 -4
  341. package/src/plugins/manageRoles/View.vue +12 -8
  342. package/src/plugins/manageRoles/definition.ts +6 -0
  343. package/src/plugins/manageRoles/index.ts +7 -6
  344. package/src/plugins/manageSkills/View.vue +11 -7
  345. package/src/plugins/manageSkills/definition.ts +4 -1
  346. package/src/plugins/manageSkills/index.ts +14 -7
  347. package/src/plugins/manageSkills/meta.ts +21 -0
  348. package/src/plugins/manageSource/definition.ts +4 -1
  349. package/src/plugins/manageSource/index.ts +15 -7
  350. package/src/plugins/manageSource/meta.ts +21 -0
  351. package/src/plugins/markdown/Preview.vue +10 -8
  352. package/src/plugins/markdown/View.vue +84 -17
  353. package/src/plugins/markdown/definition.ts +7 -1
  354. package/src/plugins/markdown/index.ts +15 -8
  355. package/src/plugins/markdown/meta.ts +16 -0
  356. package/src/plugins/meta-types.ts +97 -0
  357. package/src/plugins/metas.ts +224 -0
  358. package/src/plugins/presentForm/Preview.vue +4 -15
  359. package/src/plugins/presentForm/View.vue +35 -78
  360. package/src/plugins/presentForm/definition.ts +7 -6
  361. package/src/plugins/presentForm/index.ts +12 -5
  362. package/src/plugins/presentForm/meta.ts +11 -0
  363. package/src/plugins/presentForm/plugin.ts +8 -9
  364. package/src/plugins/presentForm/types.ts +0 -24
  365. package/src/plugins/presentHtml/Preview.vue +1 -8
  366. package/src/plugins/presentHtml/View.vue +401 -30
  367. package/src/plugins/presentHtml/definition.ts +8 -5
  368. package/src/plugins/presentHtml/index.ts +15 -8
  369. package/src/plugins/presentHtml/meta.ts +14 -0
  370. package/src/plugins/presentMulmoScript/View.vue +327 -107
  371. package/src/plugins/presentMulmoScript/definition.ts +34 -7
  372. package/src/plugins/presentMulmoScript/helpers.ts +4 -5
  373. package/src/plugins/presentMulmoScript/index.ts +20 -7
  374. package/src/plugins/presentMulmoScript/meta.ts +52 -0
  375. package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
  376. package/src/plugins/scheduler/Preview.vue +5 -2
  377. package/src/plugins/scheduler/TasksTab.vue +16 -36
  378. package/src/plugins/scheduler/View.vue +22 -54
  379. package/src/plugins/scheduler/automationsDefinition.ts +14 -9
  380. package/src/plugins/scheduler/automationsMeta.ts +5 -0
  381. package/src/plugins/scheduler/calendarDefinition.ts +4 -7
  382. package/src/plugins/scheduler/calendarMeta.ts +28 -0
  383. package/src/plugins/scheduler/formatSchedule.ts +6 -24
  384. package/src/plugins/scheduler/index.ts +26 -52
  385. package/src/plugins/scope.ts +57 -0
  386. package/src/plugins/server-bindings-types.ts +38 -0
  387. package/src/plugins/server.ts +32 -0
  388. package/src/plugins/skill/Preview.vue +25 -0
  389. package/src/plugins/skill/View.vue +125 -0
  390. package/src/plugins/skill/definition.ts +23 -0
  391. package/src/plugins/skill/index.ts +36 -0
  392. package/src/plugins/skill/plugin.ts +31 -0
  393. package/src/plugins/skill/types.ts +21 -0
  394. package/src/plugins/spreadsheet/Preview.vue +1 -3
  395. package/src/plugins/spreadsheet/View.vue +29 -49
  396. package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
  397. package/src/plugins/spreadsheet/definition.ts +5 -2
  398. package/src/plugins/spreadsheet/index.ts +15 -8
  399. package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
  400. package/src/plugins/spreadsheet/meta.ts +14 -0
  401. package/src/plugins/textResponse/Preview.vue +9 -1
  402. package/src/plugins/textResponse/View.vue +59 -8
  403. package/src/plugins/textResponse/index.ts +11 -3
  404. package/src/plugins/textResponse/plugin.ts +8 -10
  405. package/src/plugins/textResponse/types.ts +28 -0
  406. package/src/plugins/wiki/Preview.vue +6 -4
  407. package/src/plugins/wiki/View.vue +463 -254
  408. package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
  409. package/src/plugins/wiki/helpers.ts +17 -0
  410. package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
  411. package/src/plugins/wiki/history/HistoryTab.vue +167 -0
  412. package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
  413. package/src/plugins/wiki/history/api.ts +52 -0
  414. package/src/plugins/wiki/history/diff.ts +145 -0
  415. package/src/plugins/wiki/index.ts +42 -32
  416. package/src/plugins/wiki/meta.ts +10 -0
  417. package/src/plugins/wiki/pageEditLoader.ts +53 -0
  418. package/src/plugins/wiki/route.ts +8 -0
  419. package/src/router/guards.ts +2 -1
  420. package/src/router/index.ts +19 -0
  421. package/src/router/pageRoutes.ts +1 -0
  422. package/src/tools/index.ts +50 -51
  423. package/src/tools/runtimeLoader.ts +141 -0
  424. package/src/tools/types.ts +44 -1
  425. package/src/types/notification.ts +23 -0
  426. package/src/types/pastedFile.ts +10 -0
  427. package/src/types/session.ts +61 -3
  428. package/src/types/sse.ts +21 -6
  429. package/src/utils/agent/eventDispatch.ts +12 -9
  430. package/src/utils/agent/pastedAttachment.ts +35 -0
  431. package/src/utils/agent/request.ts +32 -3
  432. package/src/utils/agent/toolCalls.ts +7 -1
  433. package/src/utils/api.ts +1 -1
  434. package/src/utils/chat/exportMarkdown.ts +243 -0
  435. package/src/utils/errors.ts +10 -2
  436. package/src/utils/files/expandedDirs.ts +1 -1
  437. package/src/utils/filesPreview/todoPreview.ts +13 -2
  438. package/src/utils/format/date.ts +1 -3
  439. package/src/utils/format/jsonSyntax.ts +5 -0
  440. package/src/utils/html/iframeHeightReporterScript.ts +62 -0
  441. package/src/utils/html/previewCsp.ts +29 -2
  442. package/src/utils/image/htmlSrcAttrs.ts +122 -0
  443. package/src/utils/image/imageRepairInlineScript.ts +115 -0
  444. package/src/utils/image/resolve.ts +17 -3
  445. package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
  446. package/src/utils/markdown/frontmatter.ts +125 -0
  447. package/src/utils/markdown/taskList.ts +7 -2
  448. package/src/utils/plugin/runtime.ts +132 -0
  449. package/src/utils/session/mergeSessions.ts +40 -37
  450. package/src/utils/session/sessionEntries.ts +74 -18
  451. package/src/utils/session/sessionHelpers.ts +54 -10
  452. package/src/utils/tools/result.ts +76 -14
  453. package/src/vite-env.d.ts +6 -0
  454. package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
  455. package/client/assets/index-CY-WpQUm.css +0 -2
  456. package/client/assets/index-DbTz2Mfs.js +0 -4911
  457. package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
  458. package/server/api/routes/html.ts +0 -114
  459. package/server/api/routes/todos.ts +0 -293
  460. package/server/api/routes/todosColumnsHandlers.ts +0 -333
  461. package/server/api/routes/todosHandlers.ts +0 -274
  462. package/server/api/routes/todosItemsHandlers.ts +0 -386
  463. package/server/utils/files/todos-io.ts +0 -29
  464. package/src/components/NotificationToast.vue +0 -75
  465. package/src/plugins/editImage/definition.ts +0 -27
  466. package/src/plugins/editImage/index.ts +0 -37
  467. package/src/plugins/presentHtml/helpers.ts +0 -72
  468. package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
  469. package/src/plugins/scheduler/legacyShape.ts +0 -34
  470. package/src/plugins/todo/Preview.vue +0 -68
  471. package/src/plugins/todo/View.vue +0 -378
  472. package/src/plugins/todo/composables/useTodos.ts +0 -179
  473. package/src/plugins/todo/definition.ts +0 -45
  474. package/src/plugins/todo/index.ts +0 -62
  475. package/src/plugins/todo/labels.ts +0 -163
  476. package/src/plugins/todo/priority.ts +0 -98
  477. package/src/plugins/todo/viewModes.ts +0 -19
  478. package/src/plugins/wiki/definition.ts +0 -25
  479. package/src/tools/legacyPluginNames.ts +0 -13
  480. package/src/utils/format/frontmatter.ts +0 -80
  481. package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
  482. package/src/utils/notification/dispatch.ts +0 -58
  483. /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
  484. /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
  485. /package/src/plugins/{editImage → editImages}/View.vue +0 -0
  486. /package/src/{config/schedulerActions.ts → plugins/scheduler/actions.ts} +0 -0
@@ -1,15 +1,21 @@
1
1
  import { Router, Request, Response } from "express";
2
2
  import { realpathSync } from "fs";
3
3
  import { readdir, stat } from "fs/promises";
4
- import { readTextSafe } from "../../utils/files/safe.js";
4
+ import { readTextSafe, resolveWithinRoot } from "../../utils/files/safe.js";
5
5
  import path from "path";
6
6
  import { workspacePath } from "../../workspace/workspace.js";
7
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";
8
+ import {
9
+ readSessionMeta as readSessionMetaIO,
10
+ readSessionJsonl,
11
+ sessionJsonlAbsPath,
12
+ sessionMetaAbsPath,
13
+ updateIsBookmarked,
14
+ deleteSessionFiles,
15
+ } from "../../utils/files/session-io.js";
16
+ import { readManifest, removeSessionFromIndex } from "../../workspace/chat-index/indexer.js";
11
17
  import type { ChatIndexEntry } from "../../workspace/chat-index/types.js";
12
- import { markRead, getSession } from "../../events/session-store/index.js";
18
+ import { markRead, getSession, evictSession, publishSessionsChanged } from "../../events/session-store/index.js";
13
19
  import { notFound } from "../../utils/httpError.js";
14
20
  import { API_ROUTES } from "../../../src/config/apiRoutes.js";
15
21
  import { EVENT_TYPES } from "../../../src/types/events.js";
@@ -25,6 +31,7 @@ interface SessionMeta {
25
31
  startedAt: string;
26
32
  firstUserMessage?: string;
27
33
  hasUnread?: boolean;
34
+ isBookmarked?: boolean;
28
35
  origin?: SessionOrigin;
29
36
  }
30
37
 
@@ -66,6 +73,9 @@ export interface SessionSummary {
66
73
  keywords?: string[];
67
74
  // Where this session originated (#486). Missing = "human".
68
75
  origin?: SessionOrigin;
76
+ // User-toggled bookmark — surfaced in the history panel as a
77
+ // dedicated filter chip and a green role-icon tint.
78
+ isBookmarked?: boolean;
69
79
  // Live state from the in-memory session store. Absent when the
70
80
  // session has no active entry in the store (i.e. idle / historical).
71
81
  isRunning?: boolean;
@@ -97,73 +107,98 @@ const router = Router();
97
107
  // SESSIONS_LIST_WINDOW_DAYS to override (0 = no cutoff).
98
108
  const WINDOW_MS = env.sessionsListWindowDays * ONE_DAY_MS;
99
109
 
110
+ interface SessionRowContext {
111
+ chatDir: string;
112
+ cutoff: number;
113
+ indexById: Map<string, ChatIndexEntry>;
114
+ }
115
+
116
+ interface SessionRow {
117
+ summary: SessionSummary;
118
+ changeMs: number;
119
+ }
120
+
121
+ // Build a SessionSummary from the gathered inputs. Conditional spreads
122
+ // honour the server tsconfig's `exactOptionalPropertyTypes`; that's
123
+ // why each optional field is set with `if (… !== undefined)` rather
124
+ // than spread into the object literal directly.
125
+ function buildSessionSummary(
126
+ sessionId: string,
127
+ meta: SessionMeta,
128
+ indexEntry: ChatIndexEntry | undefined,
129
+ fileStat: { mtimeMs: number },
130
+ live: ReturnType<typeof getSession>,
131
+ ): SessionSummary {
132
+ // Prefer AI title → meta.firstUserMessage → empty.
133
+ const preview = indexEntry?.title ?? meta.firstUserMessage ?? "";
134
+ const summary: SessionSummary = {
135
+ id: sessionId,
136
+ roleId: meta.roleId,
137
+ startedAt: meta.startedAt,
138
+ updatedAt: new Date(fileStat.mtimeMs).toISOString(),
139
+ preview,
140
+ hasUnread: live?.hasUnread ?? meta.hasUnread ?? false,
141
+ };
142
+ if (meta.origin) summary.origin = meta.origin;
143
+ if (meta.isBookmarked) summary.isBookmarked = true;
144
+ if (indexEntry?.summary !== undefined) summary.summary = indexEntry.summary;
145
+ if (indexEntry?.keywords !== undefined) summary.keywords = indexEntry.keywords;
146
+ if (live) {
147
+ // Background generations (image/audio/movie) keep the session
148
+ // "busy" even when the agent turn has ended, so the sidebar
149
+ // indicator stays lit across view navigation.
150
+ summary.isRunning = live.isRunning || Object.keys(live.pendingGenerations).length > 0;
151
+ summary.statusMessage = live.statusMessage;
152
+ }
153
+ return summary;
154
+ }
155
+
156
+ // Load one session's row, or null when the session should be skipped
157
+ // (cutoff window, missing meta, any I/O error). The `metaMtimeMs`
158
+ // fallback lets a brand-new session contribute 0 to its changeMs
159
+ // instead of crashing the whole listing.
160
+ async function loadSessionRow(sessionId: string, ctx: SessionRowContext): Promise<SessionRow | null> {
161
+ try {
162
+ // stat only — no readFile on .jsonl content
163
+ const fileStat = await stat(sessionJsonlAbsPath(sessionId));
164
+ if (ctx.cutoff > 0 && fileStat.mtimeMs < ctx.cutoff) return null;
165
+
166
+ const meta = await readSessionMeta(ctx.chatDir, sessionId);
167
+ if (!meta) return null;
168
+
169
+ // The meta sidecar bumps its mtime on hasUnread / origin writes —
170
+ // feed it into changeMs so cursor-based refetches pick up drains
171
+ // of background generations (which only touch meta, not the
172
+ // jsonl).
173
+ const metaMtimeMs = await stat(sessionMetaAbsPath(sessionId))
174
+ .then((stats) => stats.mtimeMs)
175
+ .catch(() => 0);
176
+
177
+ const indexEntry = ctx.indexById.get(sessionId);
178
+ const live = getSession(sessionId);
179
+ return {
180
+ summary: buildSessionSummary(sessionId, meta, indexEntry, fileStat, live),
181
+ changeMs: sessionChangeMs(fileStat.mtimeMs, indexEntry?.indexedAt, metaMtimeMs),
182
+ };
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
100
188
  // Read the full session list off disk. Each row carries its
101
189
  // `changeMs` — the later of the jsonl mtime and the chat-index
102
190
  // `indexedAt` — so the handler can filter against `?since=` and
103
191
  // compute the new cursor without re-statting anything.
104
- export async function loadAllSessions(): Promise<{ summary: SessionSummary; changeMs: number }[]> {
192
+ export async function loadAllSessions(): Promise<SessionRow[]> {
105
193
  const chatDir = WORKSPACE_PATHS.chat;
106
194
  const manifest = await readManifest(workspacePath);
107
195
  const indexById = new Map<string, ChatIndexEntry>(manifest.entries.map((entry) => [entry.id, entry]));
108
196
  const cutoff = WINDOW_MS > 0 ? Date.now() - WINDOW_MS : 0;
197
+ const ctx: SessionRowContext = { chatDir, cutoff, indexById };
109
198
 
110
199
  const files = (await readdir(chatDir)).filter((fileName) => fileName.endsWith(".jsonl"));
111
- const rows = await Promise.all(
112
- files.map(async (file) => {
113
- const sessionId = file.replace(".jsonl", "");
114
- try {
115
- // stat only — no readFile on .jsonl content
116
- const fileStat = await stat(sessionJsonlAbsPath(sessionId));
117
- if (cutoff > 0 && fileStat.mtimeMs < cutoff) return null;
118
-
119
- const meta = await readSessionMeta(chatDir, sessionId);
120
- if (!meta) return null;
121
-
122
- // The meta sidecar bumps its mtime on hasUnread / origin
123
- // writes — feed it into changeMs so cursor-based refetches
124
- // pick up drains of background generations (which only touch
125
- // meta, not the jsonl). Missing stat (brand-new session
126
- // before its first meta write) contributes 0.
127
- const metaMtimeMs = await stat(sessionMetaAbsPath(sessionId))
128
- .then((stats) => stats.mtimeMs)
129
- .catch(() => 0);
130
-
131
- const indexEntry = indexById.get(sessionId);
132
- // Prefer AI title → meta.firstUserMessage → empty.
133
- // `summary` and `keywords` are spread conditionally
134
- // to respect the server tsconfig's
135
- // exactOptionalPropertyTypes.
136
- const preview = indexEntry?.title ?? meta.firstUserMessage ?? "";
137
-
138
- const live = getSession(sessionId);
139
- const summary: SessionSummary = {
140
- id: sessionId,
141
- roleId: meta.roleId,
142
- startedAt: meta.startedAt,
143
- updatedAt: new Date(fileStat.mtimeMs).toISOString(),
144
- preview,
145
- hasUnread: live?.hasUnread ?? meta.hasUnread ?? false,
146
- };
147
- if (meta.origin) summary.origin = meta.origin;
148
- if (indexEntry?.summary !== undefined) summary.summary = indexEntry.summary;
149
- if (indexEntry?.keywords !== undefined) summary.keywords = indexEntry.keywords;
150
- if (live) {
151
- // Background generations (image/audio/movie) keep the session
152
- // "busy" even when the agent turn has ended, so the sidebar
153
- // indicator stays lit across view navigation.
154
- summary.isRunning = live.isRunning || Object.keys(live.pendingGenerations).length > 0;
155
- summary.statusMessage = live.statusMessage;
156
- }
157
- return {
158
- summary,
159
- changeMs: sessionChangeMs(fileStat.mtimeMs, indexEntry?.indexedAt, metaMtimeMs),
160
- };
161
- } catch {
162
- return null;
163
- }
164
- }),
165
- );
166
- return rows.filter((row): row is { summary: SessionSummary; changeMs: number } => row !== null);
200
+ const rows = await Promise.all(files.map((file) => loadSessionRow(file.replace(".jsonl", ""), ctx)));
201
+ return rows.filter((row): row is SessionRow => row !== null);
167
202
  }
168
203
 
169
204
  router.get(API_ROUTES.sessions.list, async (req: Request<object, SessionsResponse, object, SessionsQuery>, res: Response<SessionsResponse>) => {
@@ -211,6 +246,79 @@ interface SessionErrorResponse {
211
246
  error: string;
212
247
  }
213
248
 
249
+ // Narrow type predicate for the presentMulmoScript tool-result shape
250
+ // the enrichment helper inspects. Returning `entry is …` lets the
251
+ // caller drop the redundant nested checks once the predicate passes.
252
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
253
+ function isPresentMulmoScriptToolResult(entry: any): entry is {
254
+ source: "tool";
255
+ type: string;
256
+ result: { toolName: "presentMulmoScript"; data: { filePath: string } & Record<string, unknown> };
257
+ } & Record<string, unknown> {
258
+ return (
259
+ entry?.source === "tool" &&
260
+ entry?.type === EVENT_TYPES.toolResult &&
261
+ entry?.result?.toolName === "presentMulmoScript" &&
262
+ typeof entry?.result?.data?.filePath === "string"
263
+ );
264
+ }
265
+
266
+ // Re-read the MulmoScript JSON pointed at by `entry.result.data.filePath`
267
+ // and merge it into a copy of the entry. Returns the original entry on
268
+ // any failure (missing file, traversal escape, parse error, absolute
269
+ // path) so the detail route never breaks because of a single rotted
270
+ // link. Path-traversal guard is realpath-based — see
271
+ // resolveWithinRoot for why.
272
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
273
+ async function enrichWithMulmoScript(entry: any): Promise<any> {
274
+ try {
275
+ const storiesDir = path.resolve(WORKSPACE_PATHS.stories);
276
+ let storiesReal: string;
277
+ try {
278
+ storiesReal = realpathSync(storiesDir);
279
+ } catch {
280
+ return entry;
281
+ }
282
+ const scriptRelPath: string = entry.result.data.filePath;
283
+ if (path.isAbsolute(scriptRelPath)) return entry;
284
+ const relFromStories = scriptRelPath.startsWith("stories/") ? scriptRelPath.slice("stories/".length) : scriptRelPath;
285
+ const scriptPath = resolveWithinRoot(storiesReal, relFromStories);
286
+ if (!scriptPath) return entry;
287
+ const scriptJson = (await readTextSafe(scriptPath)) ?? "";
288
+ return {
289
+ ...entry,
290
+ result: {
291
+ ...entry.result,
292
+ data: {
293
+ ...entry.result.data,
294
+ script: JSON.parse(scriptJson),
295
+ },
296
+ },
297
+ };
298
+ } catch {
299
+ return entry;
300
+ }
301
+ }
302
+
303
+ // Parse one JSONL line into a session entry, applying the legacy-meta
304
+ // skip and the presentMulmoScript enrichment in one place. Returns
305
+ // null when the line should not appear in the response (parse error
306
+ // or legacy meta).
307
+ async function parseSessionEntry(line: string): Promise<unknown> {
308
+ let entry: unknown;
309
+ try {
310
+ entry = JSON.parse(line);
311
+ } catch {
312
+ return null;
313
+ }
314
+ // Skip legacy metadata entries now stored in .json
315
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
316
+ const typed = entry as any;
317
+ if (typed?.type === EVENT_TYPES.sessionMeta || typed?.type === EVENT_TYPES.claudeSessionId) return null;
318
+ if (isPresentMulmoScriptToolResult(typed)) return enrichWithMulmoScript(typed);
319
+ return typed;
320
+ }
321
+
214
322
  router.get(API_ROUTES.sessions.detail, async (req: Request<SessionIdParams>, res: Response<unknown[] | SessionErrorResponse>) => {
215
323
  const { id: sessionId } = req.params;
216
324
  const chatDir = WORKSPACE_PATHS.chat;
@@ -223,65 +331,7 @@ router.get(API_ROUTES.sessions.detail, async (req: Request<SessionIdParams>, res
223
331
  notFound(res, `Session ${sessionId} not found`);
224
332
  return;
225
333
  }
226
- const entries = (
227
- await Promise.all(
228
- content
229
- .split("\n")
230
- .filter(Boolean)
231
- .map(async (line) => {
232
- try {
233
- const entry = JSON.parse(line);
234
- // Skip legacy metadata entries now stored in .json
235
- if (entry.type === EVENT_TYPES.sessionMeta || entry.type === EVENT_TYPES.claudeSessionId) return null;
236
- // For presentMulmoScript results, re-read the script from disk
237
- if (
238
- entry.source === "tool" &&
239
- entry.type === EVENT_TYPES.toolResult &&
240
- entry.result?.toolName === "presentMulmoScript" &&
241
- entry.result?.data?.filePath
242
- ) {
243
- try {
244
- // Realpath-based traversal check defeats symlink
245
- // escapes — see resolveWithinRoot in utils/fs.ts.
246
- // Resolve the stories dir's realpath so the
247
- // boundary check works even when stories/ itself
248
- // is a legitimate symlink to another disk.
249
- const storiesDir = path.resolve(WORKSPACE_PATHS.stories);
250
- let storiesReal: string;
251
- try {
252
- storiesReal = realpathSync(storiesDir);
253
- } catch {
254
- return entry;
255
- }
256
- const scriptRelPath: string = entry.result.data.filePath;
257
- if (path.isAbsolute(scriptRelPath)) return entry;
258
- // Strip optional "stories/" prefix so the
259
- // remainder is relative to storiesReal.
260
- const relFromStories = scriptRelPath.startsWith("stories/") ? scriptRelPath.slice("stories/".length) : scriptRelPath;
261
- const scriptPath = resolveWithinRoot(storiesReal, relFromStories);
262
- if (!scriptPath) return entry;
263
- const scriptJson = (await readTextSafe(scriptPath)) ?? "";
264
- return {
265
- ...entry,
266
- result: {
267
- ...entry.result,
268
- data: {
269
- ...entry.result.data,
270
- script: JSON.parse(scriptJson),
271
- },
272
- },
273
- };
274
- } catch {
275
- // file missing — return original entry
276
- }
277
- }
278
- return entry;
279
- } catch {
280
- return null;
281
- }
282
- }),
283
- )
284
- ).filter(Boolean);
334
+ const entries = (await Promise.all(content.split("\n").filter(Boolean).map(parseSessionEntry))).filter(Boolean);
285
335
  // Prepend metadata as session_meta entry for the frontend
286
336
  const result = meta ? [{ type: EVENT_TYPES.sessionMeta, ...meta }, ...entries] : entries;
287
337
  log.info("sessions", "detail: ok", { sessionId, entries: result.length });
@@ -302,4 +352,67 @@ router.post(API_ROUTES.sessions.markRead, async (req: Request<SessionIdParams>,
302
352
  res.json({ ok: true });
303
353
  });
304
354
 
355
+ // Toggle the user-set bookmark flag on a session's meta sidecar.
356
+ router.post(
357
+ API_ROUTES.sessions.bookmark,
358
+ async (
359
+ req: Request<SessionIdParams, { ok: boolean } | SessionErrorResponse, { bookmarked: boolean }>,
360
+ res: Response<{ ok: boolean } | SessionErrorResponse>,
361
+ ) => {
362
+ const { id: sessionId } = req.params;
363
+ const bookmarked = Boolean(req.body?.bookmarked);
364
+ log.info("sessions", "bookmark: start", { sessionId, bookmarked });
365
+ try {
366
+ await updateIsBookmarked(sessionId, bookmarked);
367
+ // Meta-mtime bumps on the write — cursor diff will pick up the
368
+ // change on the next refetch — but every other tab also needs
369
+ // to know to refetch right now.
370
+ publishSessionsChanged();
371
+ log.info("sessions", "bookmark: ok", { sessionId, bookmarked });
372
+ res.json({ ok: true });
373
+ } catch (err) {
374
+ log.error("sessions", "bookmark: threw", { sessionId, error: errorMessage(err) });
375
+ res.status(500).json({ error: "Failed to update bookmark" });
376
+ }
377
+ },
378
+ );
379
+
380
+ // Hard-delete a session: remove the jsonl, meta sidecar, AND the
381
+ // chat-index per-session file + manifest entry, then evict the
382
+ // in-memory store entry and broadcast `deletedIds` so every tab
383
+ // prunes its caches.
384
+ //
385
+ // Order matters:
386
+ // 1. Refuse if the session is currently running. The agent route
387
+ // (server/api/routes/agent.ts) writes via `appendSessionLine` /
388
+ // `endRun`; deleting the files out from under a live run would
389
+ // either resurrect them or corrupt mid-stream state. Caller
390
+ // should cancel first, then retry the delete.
391
+ // 2. Delete the on-disk artifacts (jsonl + meta + index entry +
392
+ // manifest prune). If any of these throw we abort BEFORE
393
+ // evicting / broadcasting, so clients don't see "session gone"
394
+ // while the file lingers.
395
+ // 3. Only after disk is clean do we evict from the store and fire
396
+ // `notifySessionsChanged({ deletedIds })`. Now the broadcast is
397
+ // a truthful statement.
398
+ router.delete(API_ROUTES.sessions.detail, async (req: Request<SessionIdParams>, res: Response<{ ok: boolean } | SessionErrorResponse>) => {
399
+ const { id: sessionId } = req.params;
400
+ log.info("sessions", "delete: start", { sessionId });
401
+ if (getSession(sessionId)?.isRunning) {
402
+ log.warn("sessions", "delete: refused — session running", { sessionId });
403
+ res.status(409).json({ error: "Session is running. Cancel the run before deleting." });
404
+ return;
405
+ }
406
+ try {
407
+ await deleteSessionFiles(sessionId);
408
+ await removeSessionFromIndex(workspacePath, sessionId);
409
+ evictSession(sessionId);
410
+ log.info("sessions", "delete: ok", { sessionId });
411
+ res.json({ ok: true });
412
+ } catch (err) {
413
+ log.error("sessions", "delete: threw", { sessionId, error: errorMessage(err) });
414
+ res.status(500).json({ error: "Failed to delete session" });
415
+ }
416
+ });
417
+
305
418
  export default router;
@@ -17,6 +17,7 @@ import { deleteProjectSkill, discoverSkills, saveProjectSkill, updateProjectSkil
17
17
  import type { Skill, SkillSummary } from "../../workspace/skills/index.js";
18
18
  import { workspacePath } from "../../workspace/workspace.js";
19
19
  import { API_ROUTES } from "../../../src/config/apiRoutes.js";
20
+ import { bindRoute } from "../../utils/router.js";
20
21
  import { log } from "../../system/logger/index.js";
21
22
  import { refreshScheduledSkills } from "../../workspace/skills/scheduler.js";
22
23
  import { logBackgroundError } from "../../utils/logBackgroundError.js";
@@ -52,7 +53,7 @@ interface DeleteSkillResponse {
52
53
  name: string;
53
54
  }
54
55
 
55
- router.get(API_ROUTES.skills.list, async (_req: Request, res: Response<SkillsListResponse>) => {
56
+ bindRoute(router, API_ROUTES.skills.list, async (_req: Request, res: Response<SkillsListResponse>) => {
56
57
  const skills = await discoverSkills({ workspaceRoot: workspacePath });
57
58
  log.info("skills", "list: ok", { count: skills.length });
58
59
  res.json({
@@ -64,7 +65,7 @@ router.get(API_ROUTES.skills.list, async (_req: Request, res: Response<SkillsLis
64
65
  });
65
66
  });
66
67
 
67
- router.get(API_ROUTES.skills.detail, async (req: Request<{ name: string }>, res: Response<SkillDetailResponse | ErrorResponse>) => {
68
+ bindRoute(router, API_ROUTES.skills.detail, async (req: Request<{ name: string }>, res: Response<SkillDetailResponse | ErrorResponse>) => {
68
69
  log.info("skills", "detail: start", { name: req.params.name });
69
70
  const skills = await discoverSkills({ workspaceRoot: workspacePath });
70
71
  const skill = skills.find((candidate) => candidate.name === req.params.name);
@@ -76,7 +77,7 @@ router.get(API_ROUTES.skills.detail, async (req: Request<{ name: string }>, res:
76
77
  res.json({ skill });
77
78
  });
78
79
 
79
- router.post(API_ROUTES.skills.create, async (req: Request<object, unknown, SaveSkillBody>, res: Response<SaveSkillResponse | ErrorResponse>) => {
80
+ bindRoute(router, API_ROUTES.skills.create, async (req: Request<object, unknown, SaveSkillBody>, res: Response<SaveSkillResponse | ErrorResponse>) => {
80
81
  const { name, description, body } = req.body ?? {};
81
82
  log.info("skills", "create: start", { name: typeof name === "string" ? name : undefined });
82
83
  if (typeof name !== "string") {
@@ -135,54 +136,58 @@ interface UpdateSkillResponse {
135
136
  path: string;
136
137
  }
137
138
 
138
- router.put(API_ROUTES.skills.update, async (req: Request<{ name: string }, unknown, UpdateSkillBody>, res: Response<UpdateSkillResponse | ErrorResponse>) => {
139
- const { name } = req.params;
140
- const { description, body } = req.body ?? {};
141
- log.info("skills", "update: start", { name });
142
- if (typeof description !== "string") {
143
- log.warn("skills", "update: invalid description", { name });
144
- badRequest(res, "description must be a string");
145
- return;
146
- }
147
- if (typeof body !== "string") {
148
- log.warn("skills", "update: invalid body", { name });
149
- badRequest(res, "body must be a string");
150
- return;
151
- }
152
- const result = await updateProjectSkill({
153
- workspaceRoot: workspacePath,
154
- name,
155
- description,
156
- body,
157
- });
158
- if (result.kind === "updated") {
159
- log.info("skills", "updated", { name });
160
- refreshScheduledSkills().catch(logBackgroundError("skills"));
161
- res.json({ updated: true, path: result.path });
162
- return;
163
- }
164
- if (result.kind === "invalid-slug") {
165
- log.warn("skills", "update: invalid slug", { slug: result.slug });
166
- badRequest(res, `invalid slug: "${result.slug}"`);
167
- return;
168
- }
169
- if (result.kind === "missing-field") {
170
- log.warn("skills", "update: missing field", { name, field: result.field });
171
- badRequest(res, `${result.field} must be a non-empty string`);
172
- return;
173
- }
174
- if (result.kind === "user-scope") {
175
- log.warn("skills", "update: user scope refused", { name: result.name });
176
- forbidden(res, `cannot update user-scope skill "${result.name}" — only project-scope skills are writable.`);
177
- return;
178
- }
179
- if (result.kind === "not-found") {
180
- log.warn("skills", "update: not found", { name: result.name });
181
- notFound(res, `skill not found: ${result.name}`);
182
- }
183
- });
184
-
185
- router.delete(API_ROUTES.skills.remove, async (req: Request<{ name: string }>, res: Response<DeleteSkillResponse | ErrorResponse>) => {
139
+ bindRoute(
140
+ router,
141
+ API_ROUTES.skills.update,
142
+ async (req: Request<{ name: string }, unknown, UpdateSkillBody>, res: Response<UpdateSkillResponse | ErrorResponse>) => {
143
+ const { name } = req.params;
144
+ const { description, body } = req.body ?? {};
145
+ log.info("skills", "update: start", { name });
146
+ if (typeof description !== "string") {
147
+ log.warn("skills", "update: invalid description", { name });
148
+ badRequest(res, "description must be a string");
149
+ return;
150
+ }
151
+ if (typeof body !== "string") {
152
+ log.warn("skills", "update: invalid body", { name });
153
+ badRequest(res, "body must be a string");
154
+ return;
155
+ }
156
+ const result = await updateProjectSkill({
157
+ workspaceRoot: workspacePath,
158
+ name,
159
+ description,
160
+ body,
161
+ });
162
+ if (result.kind === "updated") {
163
+ log.info("skills", "updated", { name });
164
+ refreshScheduledSkills().catch(logBackgroundError("skills"));
165
+ res.json({ updated: true, path: result.path });
166
+ return;
167
+ }
168
+ if (result.kind === "invalid-slug") {
169
+ log.warn("skills", "update: invalid slug", { slug: result.slug });
170
+ badRequest(res, `invalid slug: "${result.slug}"`);
171
+ return;
172
+ }
173
+ if (result.kind === "missing-field") {
174
+ log.warn("skills", "update: missing field", { name, field: result.field });
175
+ badRequest(res, `${result.field} must be a non-empty string`);
176
+ return;
177
+ }
178
+ if (result.kind === "user-scope") {
179
+ log.warn("skills", "update: user scope refused", { name: result.name });
180
+ forbidden(res, `cannot update user-scope skill "${result.name}" only project-scope skills are writable.`);
181
+ return;
182
+ }
183
+ if (result.kind === "not-found") {
184
+ log.warn("skills", "update: not found", { name: result.name });
185
+ notFound(res, `skill not found: ${result.name}`);
186
+ }
187
+ },
188
+ );
189
+
190
+ bindRoute(router, API_ROUTES.skills.remove, async (req: Request<{ name: string }>, res: Response<DeleteSkillResponse | ErrorResponse>) => {
186
191
  log.info("skills", "delete: start", { name: req.params.name });
187
192
  const result = await deleteProjectSkill({
188
193
  workspaceRoot: workspacePath,