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
@@ -0,0 +1,106 @@
1
+ // Translation service factory. Reads the on-disk cache, routes
2
+ // only the missing sentences through an injected `translateBatch`,
3
+ // merges new translations back, and returns the assembled result
4
+ // in the caller's input order.
5
+ //
6
+ // The injection seam is deliberate: production wires
7
+ // `defaultTranslateBatch` from `./llm.ts` (which spawns the
8
+ // `claude` CLI), unit tests pass a deterministic fake.
9
+
10
+ import { loadDictionary, saveDictionary } from "../../utils/files/translation-io.js";
11
+ import { assembleResult, mergeTranslations, splitHitMiss } from "./cache.js";
12
+ import type { TranslateRequest, TranslateResponse, TranslationService, TranslationServiceDeps } from "./types.js";
13
+
14
+ const NAMESPACE_RE = /^[a-zA-Z0-9_-]+$/;
15
+ // Fixed-length alternation, no nested quantifiers — safe from ReDoS.
16
+ // eslint-disable-next-line security/detect-unsafe-regex -- single-pass match against a 2- or 5-char locale code, no backtracking.
17
+ const LANGUAGE_RE = /^[a-z]{2}(?:-[A-Z]{2})?$/;
18
+
19
+ // Bound the request shape so a single call cannot blow past the
20
+ // `claude -p <json>` argv limit (POSIX `E2BIG`, typically ~128 KiB)
21
+ // or balloon the per-call cost. UI-string callers stay well inside
22
+ // these — Role suggested queries are ~5 strings × ~50 chars.
23
+ const MAX_SENTENCES = 256;
24
+ const MAX_SENTENCE_CHARS = 1024;
25
+ const MAX_TOTAL_CHARS = 32 * 1024;
26
+
27
+ export class TranslationInputError extends Error {
28
+ constructor(message: string) {
29
+ super(message);
30
+ this.name = "TranslationInputError";
31
+ }
32
+ }
33
+
34
+ function validateRequest(req: TranslateRequest): void {
35
+ if (typeof req.namespace !== "string" || !NAMESPACE_RE.test(req.namespace)) {
36
+ throw new TranslationInputError(`invalid namespace: ${JSON.stringify(req.namespace)}`);
37
+ }
38
+ if (typeof req.targetLanguage !== "string" || !LANGUAGE_RE.test(req.targetLanguage)) {
39
+ throw new TranslationInputError(`invalid targetLanguage: ${JSON.stringify(req.targetLanguage)}`);
40
+ }
41
+ if (!Array.isArray(req.sentences) || req.sentences.length === 0) {
42
+ throw new TranslationInputError("sentences must be a non-empty array");
43
+ }
44
+ if (req.sentences.length > MAX_SENTENCES) {
45
+ throw new TranslationInputError(`sentences exceeds ${MAX_SENTENCES} entries`);
46
+ }
47
+ let totalChars = 0;
48
+ for (const sentence of req.sentences) {
49
+ if (typeof sentence !== "string" || sentence.length === 0) {
50
+ throw new TranslationInputError("sentences must contain non-empty strings");
51
+ }
52
+ if (sentence.length > MAX_SENTENCE_CHARS) {
53
+ throw new TranslationInputError(`sentence exceeds ${MAX_SENTENCE_CHARS} characters`);
54
+ }
55
+ totalChars += sentence.length;
56
+ if (totalChars > MAX_TOTAL_CHARS) {
57
+ throw new TranslationInputError(`total sentence length exceeds ${MAX_TOTAL_CHARS} characters`);
58
+ }
59
+ }
60
+ }
61
+
62
+ export function createTranslationService(deps: TranslationServiceDeps): TranslationService {
63
+ const { translateBatch, workspaceRoot } = deps;
64
+ // Per-namespace serialization chain. Two concurrent translate() calls
65
+ // on the same namespace would otherwise race the read-merge-write
66
+ // step; chaining ensures the second sees the first's persisted output.
67
+ const chains = new Map<string, Promise<unknown>>();
68
+
69
+ async function runOnce(req: TranslateRequest): Promise<TranslateResponse> {
70
+ const dict = loadDictionary(req.namespace, workspaceRoot);
71
+ const { cached, misses } = splitHitMiss(dict, req.sentences, req.targetLanguage);
72
+ if (misses.length === 0) {
73
+ return { translations: assembleResult(req.sentences, cached, new Map()) };
74
+ }
75
+ const translated = await translateBatch({ targetLanguage: req.targetLanguage, sentences: misses });
76
+ if (translated.length !== misses.length) {
77
+ throw new Error(`[translation] translateBatch returned ${translated.length} translations for ${misses.length} sentences`);
78
+ }
79
+ const fresh = new Map<string, string>();
80
+ misses.forEach((sentence, index) => fresh.set(sentence, translated[index]));
81
+ const next = mergeTranslations(dict, req.targetLanguage, fresh);
82
+ await saveDictionary(req.namespace, next, workspaceRoot);
83
+ return { translations: assembleResult(req.sentences, cached, fresh) };
84
+ }
85
+
86
+ function serialize<T>(namespace: string, runner: () => Promise<T>): Promise<T> {
87
+ const prev = chains.get(namespace) ?? Promise.resolve();
88
+ const next = prev.catch(() => undefined).then(runner);
89
+ const tracked = next.catch(() => undefined);
90
+ chains.set(namespace, tracked);
91
+ tracked.then(() => {
92
+ if (chains.get(namespace) === tracked) chains.delete(namespace);
93
+ });
94
+ return next;
95
+ }
96
+
97
+ async function translate(req: TranslateRequest): Promise<TranslateResponse> {
98
+ validateRequest(req);
99
+ if (req.targetLanguage === "en") {
100
+ return { translations: [...req.sentences] };
101
+ }
102
+ return serialize(req.namespace, () => runOnce(req));
103
+ }
104
+
105
+ return { translate };
106
+ }
@@ -0,0 +1,140 @@
1
+ // Production `translateBatch`: spawns the `claude` CLI in print
2
+ // mode with a JSON schema so the response array length is
3
+ // guaranteed to match the request. Same auth model as the rest of
4
+ // the server (no API key required).
5
+ //
6
+ // Patterned after `server/workspace/chat-index/summarizer.ts`.
7
+
8
+ import { spawn, type ChildProcess } from "node:child_process";
9
+ import { tmpdir } from "node:os";
10
+ import { CLI_SUBPROCESS_TIMEOUT_MS } from "../../utils/time.js";
11
+ import { errorMessage } from "../../utils/errors.js";
12
+ import { formatSpawnFailure } from "../../utils/spawn.js";
13
+ import { isRecord } from "../../utils/types.js";
14
+ import { ClaudeCliNotFoundError } from "../../workspace/journal/archivist-cli.js";
15
+ import type { TranslateBatchFn } from "./types.js";
16
+
17
+ const SYSTEM_PROMPT =
18
+ "You are a translation engine. The user input is a JSON object with `targetLanguage` (BCP-47) " +
19
+ "and `sentences` (an array of English source strings). Translate each sentence into the target " +
20
+ "language and return strict JSON matching the provided schema. The output `translations` array " +
21
+ "MUST have the same length and order as the input `sentences` array. Preserve placeholders " +
22
+ "such as `{name}`, `{count}`, `%s`, and HTML tags verbatim.";
23
+
24
+ const SCHEMA = {
25
+ type: "object",
26
+ properties: {
27
+ translations: {
28
+ type: "array",
29
+ items: { type: "string" },
30
+ },
31
+ },
32
+ required: ["translations"],
33
+ };
34
+
35
+ // Small per-call cap. A few dozen short UI strings on haiku costs
36
+ // fractions of a cent; this guards against runaway prompts only.
37
+ const MAX_BUDGET_USD = 0.5;
38
+
39
+ interface ClaudeJsonEnvelope {
40
+ is_error?: boolean;
41
+ result?: string;
42
+ structured_output?: unknown;
43
+ }
44
+
45
+ export function parseTranslations(stdout: string): string[] {
46
+ let parsed: ClaudeJsonEnvelope;
47
+ try {
48
+ parsed = JSON.parse(stdout.trim());
49
+ } catch (err) {
50
+ throw new Error(`[translation] failed to parse claude json output: ${errorMessage(err)}`);
51
+ }
52
+ if (parsed.is_error) {
53
+ throw new Error(`[translation] claude returned error: ${parsed.result ?? "unknown"}`);
54
+ }
55
+ if (!isRecord(parsed.structured_output)) {
56
+ throw new Error("[translation] structured_output missing or not an object");
57
+ }
58
+ const { translations } = parsed.structured_output as Record<string, unknown>;
59
+ if (!Array.isArray(translations) || !translations.every((value): value is string => typeof value === "string")) {
60
+ throw new Error("[translation] translations is not a string array");
61
+ }
62
+ return translations;
63
+ }
64
+
65
+ function buildArgs(promptInput: string): string[] {
66
+ return [
67
+ "--print",
68
+ "--no-session-persistence",
69
+ "--output-format",
70
+ "json",
71
+ "--model",
72
+ "haiku",
73
+ "--max-budget-usd",
74
+ String(MAX_BUDGET_USD),
75
+ "--json-schema",
76
+ JSON.stringify(SCHEMA),
77
+ "--system-prompt",
78
+ SYSTEM_PROMPT,
79
+ "-p",
80
+ promptInput,
81
+ ];
82
+ }
83
+
84
+ interface SpawnState {
85
+ stdout: string;
86
+ stderr: string;
87
+ settled: boolean;
88
+ }
89
+
90
+ type RejectFn = (err: Error) => void;
91
+ type ResolveFn = (value: string) => void;
92
+
93
+ function onTimeout(state: SpawnState, proc: ChildProcess, reject: RejectFn, timeoutMs: number): void {
94
+ if (state.settled) return;
95
+ state.settled = true;
96
+ proc.kill("SIGKILL");
97
+ reject(new Error(`[translation] claude translate timed out after ${timeoutMs}ms`));
98
+ }
99
+
100
+ function onError(state: SpawnState, timer: ReturnType<typeof setTimeout>, err: Error & { code?: string }, reject: RejectFn): void {
101
+ if (state.settled) return;
102
+ state.settled = true;
103
+ clearTimeout(timer);
104
+ reject(err.code === "ENOENT" ? new ClaudeCliNotFoundError() : err);
105
+ }
106
+
107
+ function onClose(state: SpawnState, timer: ReturnType<typeof setTimeout>, code: number | null, resolve: ResolveFn, reject: RejectFn): void {
108
+ if (state.settled) return;
109
+ state.settled = true;
110
+ clearTimeout(timer);
111
+ if (code !== 0) {
112
+ reject(new Error(formatSpawnFailure("[translation]", code, state.stdout, state.stderr)));
113
+ return;
114
+ }
115
+ resolve(state.stdout);
116
+ }
117
+
118
+ function spawnClaudeTranslate(promptInput: string, timeoutMs: number): Promise<string> {
119
+ return new Promise<string>((resolve, reject) => {
120
+ // Run from tmpdir so claude does not load the project's
121
+ // CLAUDE.md / plugins / memory and inflate the context.
122
+ const proc = spawn("claude", buildArgs(promptInput), { cwd: tmpdir(), stdio: ["ignore", "pipe", "pipe"] });
123
+ const state: SpawnState = { stdout: "", stderr: "", settled: false };
124
+ const timer = setTimeout(() => onTimeout(state, proc, reject, timeoutMs), timeoutMs);
125
+ proc.stdout.on("data", (chunk: Buffer) => {
126
+ state.stdout += chunk.toString();
127
+ });
128
+ proc.stderr.on("data", (chunk: Buffer) => {
129
+ state.stderr += chunk.toString();
130
+ });
131
+ proc.on("error", (err: Error & { code?: string }) => onError(state, timer, err, reject));
132
+ proc.on("close", (code) => onClose(state, timer, code, resolve, reject));
133
+ });
134
+ }
135
+
136
+ export const defaultTranslateBatch: TranslateBatchFn = async ({ targetLanguage, sentences }) => {
137
+ const promptInput = JSON.stringify({ targetLanguage, sentences });
138
+ const stdout = await spawnClaudeTranslate(promptInput, CLI_SUBPROCESS_TIMEOUT_MS);
139
+ return parseTranslations(stdout);
140
+ };
@@ -0,0 +1,35 @@
1
+ // Public types for the translation service. The on-disk dictionary
2
+ // shape is the authoritative cache format — see plans/done/feat-translation-service.md.
3
+
4
+ export interface TranslateRequest {
5
+ /** Dictionary namespace; also the cache filename. */
6
+ readonly namespace: string;
7
+ /** BCP-47 short code, e.g. `ja`, `pt-BR`. */
8
+ readonly targetLanguage: string;
9
+ /** English source sentences in the order the caller wants them returned. */
10
+ readonly sentences: readonly string[];
11
+ }
12
+
13
+ export interface TranslateResponse {
14
+ readonly translations: readonly string[];
15
+ }
16
+
17
+ export interface DictionaryFile {
18
+ readonly sentences: Record<string, Record<string, string>>;
19
+ }
20
+
21
+ export interface TranslateBatchInput {
22
+ readonly targetLanguage: string;
23
+ readonly sentences: readonly string[];
24
+ }
25
+
26
+ export type TranslateBatchFn = (input: TranslateBatchInput) => Promise<string[]>;
27
+
28
+ export interface TranslationServiceDeps {
29
+ readonly translateBatch: TranslateBatchFn;
30
+ readonly workspaceRoot?: string;
31
+ }
32
+
33
+ export interface TranslationService {
34
+ readonly translate: (req: TranslateRequest) => Promise<TranslateResponse>;
35
+ }
@@ -85,6 +85,17 @@ async function renewTokenViaPty(): Promise<boolean> {
85
85
  let responded = false;
86
86
  let buffer = "";
87
87
  let settled = false;
88
+ // Mutual reference: `finish`'s body needs `timeout` (clearTimeout)
89
+ // and `timeout`'s callback needs `finish`. Predeclared with `let`
90
+ // and assigned exactly once below. `prefer-const` would prefer a
91
+ // direct `const timeout = setTimeout(...)` form, but that needs
92
+ // `finish` already in scope inside the callback, which then
93
+ // forces `clearTimeout(timeout)` inside `finish`'s body to
94
+ // reference an undefined-at-textual-position const — i.e. the
95
+ // chicken-and-egg pair has no const-only spelling. The actual
96
+ // value is single-write at runtime; lint heuristic disagrees.
97
+ // eslint-disable-next-line prefer-const -- mutual-reference pair, see comment above
98
+ let timeout: ReturnType<typeof setTimeout>;
88
99
 
89
100
  const finish = (success: boolean) => {
90
101
  if (settled) return;
@@ -94,7 +105,7 @@ async function renewTokenViaPty(): Promise<boolean> {
94
105
  resolve(success);
95
106
  };
96
107
 
97
- const timeout = setTimeout(() => {
108
+ timeout = setTimeout(() => {
98
109
  log.error("credentials", `Token renewal timed out after ${PTY_TIMEOUT_MS / ONE_SECOND_MS}s`);
99
110
  finish(false);
100
111
  }, PTY_TIMEOUT_MS);
@@ -210,7 +221,7 @@ export async function refreshCredentials(): Promise<boolean> {
210
221
 
211
222
  // Atomic so a readers mid-refresh can't see a truncated creds
212
223
  // file; mode preserves the 0o600 we always set on this file.
213
- await writeFileAtomic(CREDENTIALS_PATH, credentials + "\n", { mode: 0o600 });
224
+ await writeFileAtomic(CREDENTIALS_PATH, `${credentials}\n`, { mode: 0o600 });
214
225
  log.info("credentials", "Fresh credentials written to ~/.claude/.credentials.json");
215
226
  return true;
216
227
  } catch (err) {
@@ -58,6 +58,12 @@ export const env = Object.freeze({
58
58
 
59
59
  // Sandbox / Docker
60
60
  disableSandbox: asFlag(process.env.DISABLE_SANDBOX),
61
+ // Debug aid: also persist `tool_call` events to the session
62
+ // jsonl (the `tool_result` side already lands on disk). Off by
63
+ // default because args can be large and may carry payload bytes
64
+ // the user didn't expect to land in this exact form. See
65
+ // plans/done/feat-persist-tool-calls.md / issue #1096.
66
+ persistToolCalls: asFlag(process.env.PERSIST_TOOL_CALLS),
61
67
  // Host-credential opt-ins for the Docker sandbox (#259). Both off
62
68
  // by default. See docs/sandbox-credentials.md for the contract.
63
69
  sandboxSshAgentForward: asFlag(process.env.SANDBOX_SSH_AGENT_FORWARD),
@@ -107,7 +113,6 @@ export const env = Object.freeze({
107
113
  mcpSessionId: process.env.SESSION_ID ?? "",
108
114
  mcpHost: process.env.MCP_HOST ?? "localhost",
109
115
  mcpPluginNames: asCsv(process.env.PLUGIN_NAMES),
110
- mcpRoleIds: asCsv(process.env.ROLE_IDS),
111
116
  });
112
117
 
113
118
  // ── Derived helpers ─────────────────────────────────────────────────
@@ -1,4 +1,4 @@
1
- import type { Formatter, LogRecord } from "./types.js";
1
+ import type { Formatter, LogLevel, LogRecord } from "./types.js";
2
2
 
3
3
  function formatData(data: Record<string, unknown> | undefined): string {
4
4
  if (!data) return "";
@@ -23,11 +23,53 @@ function stringifyScalar(value: unknown): string {
23
23
  }
24
24
  }
25
25
 
26
- export const formatText: Formatter = (record: LogRecord): string => {
27
- const level = record.level.toUpperCase().padEnd(5);
28
- return `${record.time} ${level} [${record.prefix}] ${record.message}${formatData(record.data)}`;
26
+ // ANSI escape codes small enough to inline rather than pull a dep.
27
+ // `1m` adds bold so the level word reads at a glance even on busy
28
+ // terminals. The level field stays width-padded with `.padEnd(5)` so
29
+ // the colour escape doesn't push subsequent columns out of alignment.
30
+ const ANSI_RESET = "\x1b[0m";
31
+ const LEVEL_COLOR: Record<LogLevel, string> = {
32
+ error: "\x1b[1;31m", // bold red
33
+ warn: "\x1b[1;33m", // bold yellow
34
+ info: "\x1b[1;36m", // bold cyan
35
+ debug: "\x1b[2;37m", // dim white (close to gray)
29
36
  };
30
37
 
38
+ // Levels for which the colour wraps the entire log line, not just the
39
+ // level word. The intent is that a `tail -f` reader can spot warnings
40
+ // and errors without parsing — the whole row glows red / yellow.
41
+ // Info / debug stay quiet so a chatty server doesn't fill the terminal
42
+ // with cyan or grey.
43
+ const WHOLE_LINE_COLOR: ReadonlySet<LogLevel> = new Set(["error", "warn"]);
44
+
45
+ export const formatText: Formatter = (record: LogRecord): string => formatTextLine(record, false);
46
+
47
+ /**
48
+ * Same shape as {@link formatText} but wraps the output in ANSI colour
49
+ * escapes per level. Console sinks select this variant only when
50
+ * stdout/stderr is a real TTY (or `FORCE_COLOR` is set) and `NO_COLOR`
51
+ * is unset — file sinks and CI logs always get the plain
52
+ * {@link formatText}.
53
+ *
54
+ * - `error` / `warn`: the whole line is wrapped (bold red / bold yellow)
55
+ * so the row stands out at a glance.
56
+ * - `info` / `debug`: only the level word is coloured, keeping the
57
+ * chatty levels visually quiet.
58
+ */
59
+ export const formatTextColor: Formatter = (record: LogRecord): string => formatTextLine(record, true);
60
+
61
+ function formatTextLine(record: LogRecord, color: boolean): string {
62
+ const levelText = record.level.toUpperCase().padEnd(5);
63
+ const colorCode = LEVEL_COLOR[record.level];
64
+ if (color && WHOLE_LINE_COLOR.has(record.level)) {
65
+ // Whole-row wrap. Avoid double-wrapping the level slot — one
66
+ // colour code at the start + reset at the end is enough.
67
+ return `${colorCode}${record.time} ${levelText} [${record.prefix}] ${record.message}${formatData(record.data)}${ANSI_RESET}`;
68
+ }
69
+ const level = color ? `${colorCode}${levelText}${ANSI_RESET}` : levelText;
70
+ return `${record.time} ${level} [${record.prefix}] ${record.message}${formatData(record.data)}`;
71
+ }
72
+
31
73
  export const formatJson: Formatter = (record: LogRecord): string => {
32
74
  const payload: Record<string, unknown> = {
33
75
  time: record.time,
@@ -9,10 +9,10 @@ export type { LoggerConfig } from "./config.js";
9
9
  export { resolveConfig, DEFAULT_CONFIG } from "./config.js";
10
10
 
11
11
  export interface Logger {
12
- error(prefix: string, message: string, data?: Record<string, unknown>): void;
13
- warn(prefix: string, message: string, data?: Record<string, unknown>): void;
14
- info(prefix: string, message: string, data?: Record<string, unknown>): void;
15
- debug(prefix: string, message: string, data?: Record<string, unknown>): void;
12
+ error: (prefix: string, message: string, data?: Record<string, unknown>) => void;
13
+ warn: (prefix: string, message: string, data?: Record<string, unknown>) => void;
14
+ info: (prefix: string, message: string, data?: Record<string, unknown>) => void;
15
+ debug: (prefix: string, message: string, data?: Record<string, unknown>) => void;
16
16
  }
17
17
 
18
18
  export function createLogger(config: LoggerConfig): Logger {
@@ -1,7 +1,7 @@
1
1
  import { appendFile } from "fs/promises";
2
2
  import path from "path";
3
3
  import type { ConsoleSinkConfig, FileSinkConfig, TelemetrySinkConfig } from "./config.js";
4
- import { formatJson, formatText } from "./formatters.js";
4
+ import { formatJson, formatText, formatTextColor } from "./formatters.js";
5
5
  import { dailyFileName, enforceMaxFiles, ensureDir } from "./rotation.js";
6
6
  import type { Formatter, LogRecord, Sink } from "./types.js";
7
7
 
@@ -9,15 +9,36 @@ function pickFormatter(kind: "text" | "json"): Formatter {
9
9
  return kind === "json" ? formatJson : formatText;
10
10
  }
11
11
 
12
+ // Decide whether to emit ANSI escapes on this stream:
13
+ // 1. `NO_COLOR=<anything>` → off, always (https://no-color.org/).
14
+ // 2. `FORCE_COLOR=<non-zero>` → on, even when the stream isn't a
15
+ // TTY. Concurrently / yarn / npm scripts set this on child
16
+ // processes by default, so `yarn dev` piped output keeps colour.
17
+ // `FORCE_COLOR=0` is treated as "explicitly off".
18
+ // 3. Otherwise: on iff the stream itself is a real TTY.
19
+ function colorEnabled(stream: { isTTY?: boolean }): boolean {
20
+ if (process.env.NO_COLOR && process.env.NO_COLOR.length > 0) return false;
21
+ const force = process.env.FORCE_COLOR;
22
+ if (force !== undefined && force !== "" && force !== "0" && force !== "false") return true;
23
+ if (force === "0" || force === "false") return false;
24
+ return stream.isTTY === true;
25
+ }
26
+
12
27
  export function createConsoleSink(config: ConsoleSinkConfig): Sink {
13
- const fmt = pickFormatter(config.format);
28
+ // Pre-resolve a (plain, colour) pair once. `text` may upgrade to the
29
+ // ANSI variant per record based on which stream the record will hit;
30
+ // `json` stays uncoloured so structured-log consumers get a clean
31
+ // stream regardless of the terminal.
32
+ const plain = pickFormatter(config.format);
33
+ const colored: Formatter = config.format === "text" ? formatTextColor : plain;
34
+
14
35
  return {
15
36
  name: "console",
16
37
  level: config.level,
17
38
  write(record: LogRecord) {
18
- const line = fmt(record) + "\n";
19
39
  const stream = record.level === "error" || record.level === "warn" ? process.stderr : process.stdout;
20
- stream.write(line);
40
+ const fmt = colorEnabled(stream) ? colored : plain;
41
+ stream.write(`${fmt(record)}\n`);
21
42
  },
22
43
  };
23
44
  }
@@ -72,7 +93,7 @@ export function createFileSink(config: FileSinkConfig, deps: FileSinkDeps = {}):
72
93
  write(record: LogRecord) {
73
94
  const nowDate = now();
74
95
  const rotated = maybeRotate(nowDate);
75
- const line = fmt(record) + "\n";
96
+ const line = `${fmt(record)}\n`;
76
97
  const filePath = currentPath;
77
98
  enqueue(() => writeLine(filePath, line));
78
99
  // Enforce retention AFTER the write so maxFiles counts include
@@ -21,9 +21,9 @@ export interface LogRecord {
21
21
  export interface Sink {
22
22
  name: string;
23
23
  level: LogLevel;
24
- write(record: LogRecord): void;
24
+ write: (record: LogRecord) => void;
25
25
  // Drains any pending async I/O. Tests call this; production can ignore.
26
- flush?(): Promise<void>;
26
+ flush?: () => Promise<void>;
27
27
  }
28
28
 
29
29
  export type Formatter = (record: LogRecord) => string;
@@ -0,0 +1,11 @@
1
+ // Type declarations for dev-plugin-args.mjs. See the .mjs file for
2
+ // rationale on why the shared helper lives in plain JS.
3
+
4
+ export interface DevPluginArg {
5
+ rawInput: string;
6
+ absPath: string;
7
+ }
8
+
9
+ export type ParseDevPluginArgsResult = { ok: true; resolved: DevPluginArg[] } | { ok: false; reason: string };
10
+
11
+ export function parseDevPluginArgs(argv: readonly string[], cwd: string): ParseDevPluginArgsResult;
@@ -0,0 +1,43 @@
1
+ // Shared `--dev-plugin` argv parser for the npm launcher
2
+ // (`packages/mulmoclaude/bin/mulmoclaude.js`) and any other Node entry
3
+ // point that needs to interpret the same flags.
4
+ //
5
+ // Kept as plain `.mjs` for the same reason as `port.mjs`: the launcher
6
+ // runs BEFORE tsx is wired up, so it can't import from a `.ts` file.
7
+ // Sibling `dev-plugin-args.d.mts` carries the type declarations.
8
+ //
9
+ // The parser is pure — no `process.cwd()`, no `console.*`, no
10
+ // `process.exit()`. The caller passes argv + cwd in, and decides what
11
+ // to log and when to exit. That keeps it unit-testable from
12
+ // node:test without spawning subprocesses.
13
+
14
+ import path from "node:path";
15
+
16
+ /** Parse repeated `--dev-plugin <path>` flags out of argv.
17
+ *
18
+ * Returns one of:
19
+ * { ok: true, resolved: [{ rawInput, absPath }] }
20
+ * { ok: false, reason }
21
+ *
22
+ * Errors are surfaced as a single human-readable string so the caller
23
+ * can write it to stderr. The launcher's invariants are:
24
+ * - missing argument value (`--dev-plugin` at end of argv, or
25
+ * followed by another `-`-prefixed flag) → reject
26
+ * - relative paths resolve against `cwd`
27
+ * - duplicate flags are accumulated in argv order; the caller
28
+ * detects same-name collisions later, against the resolved package
29
+ * names from disk.
30
+ */
31
+ export function parseDevPluginArgs(argv, cwd) {
32
+ const resolved = [];
33
+ for (let i = 0; i < argv.length; i++) {
34
+ if (argv[i] !== "--dev-plugin") continue;
35
+ const raw = argv[i + 1];
36
+ if (raw === undefined || raw.startsWith("-")) {
37
+ return { ok: false, reason: "--dev-plugin requires a path argument" };
38
+ }
39
+ resolved.push({ rawInput: raw, absPath: path.resolve(cwd, raw) });
40
+ i++;
41
+ }
42
+ return { ok: true, resolved };
43
+ }
@@ -2,15 +2,24 @@
2
2
  // `err instanceof Error ? err.message : String(err)` — searching for
3
3
  // one canonical helper is easier than grepping for the inline form.
4
4
  //
5
+ // Non-Error objects with a `details` (gRPC convention) or `message`
6
+ // string field have that field surfaced — without this, gRPC errors
7
+ // like `{ code, details, metadata }` show up to users as
8
+ // `[object Object]`.
9
+ //
5
10
  // The optional `fallback` covers the common route-handler idiom where
6
11
  // a throw of a plain non-Error value should surface as a descriptive
7
- // message ("rebuild failed") rather than `String(err)` noise like
8
- // `[object Object]`. Prefer passing a fallback at error-response
9
- // boundaries — omit it for logging contexts where `String(err)` is
10
- // fine.
12
+ // message ("rebuild failed") rather than `String(err)` noise. Prefer
13
+ // passing a fallback at error-response boundaries — omit it for
14
+ // logging contexts where `String(err)` is fine.
11
15
 
12
16
  export function errorMessage(err: unknown, fallback?: string): string {
13
17
  if (err instanceof Error) return err.message;
18
+ if (err !== null && typeof err === "object") {
19
+ const obj = err as { details?: unknown; message?: unknown };
20
+ if (typeof obj.details === "string" && obj.details) return obj.details;
21
+ if (typeof obj.message === "string" && obj.message) return obj.message;
22
+ }
14
23
  if (fallback !== undefined) return fallback;
15
24
  return String(err);
16
25
  }