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.
- package/Dockerfile.sandbox +100 -0
- package/README.md +17 -4
- package/bin/mulmoclaude.js +46 -15
- package/bin/prepare-dist.js +18 -2
- package/client/assets/chunk-CernVdwh.js +1 -0
- package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
- package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
- package/client/assets/index-BbgSjFQ8.js +4968 -0
- package/client/assets/index-ECD0lgIv.css +2 -0
- package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
- package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
- package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
- package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
- package/client/assets/vue-1e_vz2LW.js +1 -0
- package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
- package/client/index.html +33 -2
- package/package.json +20 -18
- package/sandbox-entrypoint.sh +106 -0
- package/server/accounting/accountNormalize.ts +32 -0
- package/server/accounting/defaultAccounts.ts +87 -0
- package/server/accounting/eventPublisher.ts +51 -0
- package/server/accounting/journal.ts +252 -0
- package/server/accounting/openingBalances.ts +114 -0
- package/server/accounting/report.ts +237 -0
- package/server/accounting/service.ts +718 -0
- package/server/accounting/snapshotCache.ts +333 -0
- package/server/accounting/timeSeries.ts +265 -0
- package/server/accounting/types.ts +148 -0
- package/server/agent/activeTools.ts +128 -0
- package/server/agent/attachmentConverter.ts +10 -5
- package/server/agent/backend/claude-code.ts +8 -2
- package/server/agent/backend/types.ts +1 -1
- package/server/agent/config.ts +101 -31
- package/server/agent/index.ts +45 -33
- package/server/agent/mcp-server.ts +146 -69
- package/server/agent/mcp-tools/index.ts +1 -5
- package/server/agent/mcp-tools/notify.ts +2 -22
- package/server/agent/mcp-tools/x.ts +0 -4
- package/server/agent/mcpHealth.ts +168 -0
- package/server/agent/plugin-names.ts +20 -77
- package/server/agent/prompt.ts +259 -51
- package/server/agent/resumeFailover.ts +1 -1
- package/server/agent/stream.ts +0 -1
- package/server/api/auth/bearerAuth.ts +5 -5
- package/server/api/csrfGuard.ts +1 -1
- package/server/api/routes/accounting.ts +366 -0
- package/server/api/routes/agent.ts +509 -46
- package/server/api/routes/attachment.ts +104 -0
- package/server/api/routes/chart.ts +2 -1
- package/server/api/routes/config.ts +12 -12
- package/server/api/routes/files.ts +105 -48
- package/server/api/routes/image.ts +70 -25
- package/server/api/routes/journal.ts +35 -0
- package/server/api/routes/mulmo-script.ts +358 -118
- package/server/api/routes/mulmoScriptValidate.ts +1 -1
- package/server/api/routes/news.ts +1 -1
- package/server/api/routes/notifications.ts +92 -22
- package/server/api/routes/notifier.ts +98 -0
- package/server/api/routes/pdf.ts +188 -48
- package/server/api/routes/plugins.ts +34 -14
- package/server/api/routes/presentHtml.ts +58 -3
- package/server/api/routes/roles.ts +1 -8
- package/server/api/routes/runtime-plugin.ts +224 -0
- package/server/api/routes/scheduler.ts +7 -5
- package/server/api/routes/schedulerHandlers.ts +1 -1
- package/server/api/routes/schedulerTasks.ts +8 -7
- package/server/api/routes/sessions.ts +234 -121
- package/server/api/routes/skills.ts +56 -51
- package/server/api/routes/sources.ts +52 -45
- package/server/api/routes/translation.ts +44 -0
- package/server/api/routes/wiki/frontmatter.ts +13 -65
- package/server/api/routes/wiki/history.ts +261 -0
- package/server/api/routes/wiki/pageIndex.ts +1 -1
- package/server/api/routes/wiki.ts +50 -26
- package/server/events/file-change.ts +83 -0
- package/server/events/notifications.ts +247 -91
- package/server/events/pub-sub/index.ts +1 -1
- package/server/events/relay-client.ts +5 -5
- package/server/events/scheduler-adapter.ts +2 -2
- package/server/events/session-store/index.ts +110 -22
- package/server/events/task-manager/index.ts +10 -9
- package/server/index.ts +509 -33
- package/server/notifier/engine.ts +419 -0
- package/server/notifier/legacy-adapters.ts +76 -0
- package/server/notifier/runtime-api.ts +74 -0
- package/server/notifier/store.ts +70 -0
- package/server/notifier/types.ts +121 -0
- package/server/plugins/dev-loader.ts +171 -0
- package/server/plugins/dev-watcher.ts +150 -0
- package/server/plugins/diagnostics.ts +188 -0
- package/server/plugins/preset-list.ts +52 -0
- package/server/plugins/preset-loader.ts +112 -0
- package/server/plugins/runtime-chat-api.ts +38 -0
- package/server/plugins/runtime-loader.ts +430 -0
- package/server/plugins/runtime-registry.ts +112 -0
- package/server/plugins/runtime-tasks-api.ts +50 -0
- package/server/plugins/runtime.ts +378 -0
- package/server/services/translation/cache.ts +72 -0
- package/server/services/translation/index.ts +106 -0
- package/server/services/translation/llm.ts +140 -0
- package/server/services/translation/types.ts +35 -0
- package/server/system/credentials.ts +13 -2
- package/server/system/env.ts +6 -1
- package/server/system/logger/formatters.ts +46 -4
- package/server/system/logger/index.ts +4 -4
- package/server/system/logger/sinks.ts +26 -5
- package/server/system/logger/types.ts +2 -2
- package/server/utils/dev-plugin-args.d.mts +11 -0
- package/server/utils/dev-plugin-args.mjs +43 -0
- package/server/utils/errors.ts +13 -4
- package/server/utils/files/accounting-io.ts +295 -0
- package/server/utils/files/atomic.ts +17 -49
- package/server/utils/files/attachment-store.ts +182 -0
- package/server/utils/files/html-io.ts +1 -7
- package/server/utils/files/html-store.ts +19 -0
- package/server/utils/files/image-store.ts +20 -22
- package/server/utils/files/index.ts +5 -15
- package/server/utils/files/journal-io.ts +7 -35
- package/server/utils/files/json.ts +2 -29
- package/server/utils/files/markdown-image-fill.ts +6 -37
- package/server/utils/files/markdown-store.ts +6 -21
- package/server/utils/files/naming.ts +3 -39
- package/server/utils/files/plugins-io.ts +100 -0
- package/server/utils/files/reference-dirs-io.ts +1 -9
- package/server/utils/files/roles-io.ts +2 -10
- package/server/utils/files/safe.ts +17 -19
- package/server/utils/files/scheduler-io.ts +1 -7
- package/server/utils/files/scheduler-overrides-io.ts +3 -12
- package/server/utils/files/session-io.ts +21 -30
- package/server/utils/files/spreadsheet-store.ts +9 -22
- package/server/utils/files/translation-io.ts +46 -0
- package/server/utils/files/user-tasks-io.ts +1 -7
- package/server/utils/files/workspace-io.ts +3 -79
- package/server/utils/gemini.ts +33 -11
- package/server/utils/html/htmlArtifactSplicer.ts +41 -0
- package/server/utils/markdown/frontmatter.ts +112 -0
- package/server/utils/regex.ts +56 -0
- package/server/utils/router.ts +41 -0
- package/server/utils/slug.ts +5 -3
- package/server/utils/time.ts +12 -0
- package/server/workspace/chat-index/indexer.ts +15 -2
- package/server/workspace/chat-index/summarizer.ts +1 -1
- package/server/workspace/custom-dirs.ts +1 -1
- package/server/workspace/helps/gemini.md +1 -1
- package/server/workspace/helps/guide.md +61 -0
- package/server/workspace/helps/index.md +4 -0
- package/server/workspace/helps/presenthtml.md +80 -0
- package/server/workspace/helps/sandbox.md +7 -0
- package/server/workspace/helps/storyteller.md +101 -0
- package/server/workspace/helps/telegram.md +1 -0
- package/server/workspace/helps/wiki.md +9 -7
- package/server/workspace/journal/archivist-cli.ts +7 -33
- package/server/workspace/journal/archivist-schemas.ts +5 -43
- package/server/workspace/journal/dailyPass.ts +34 -187
- package/server/workspace/journal/diff.ts +3 -28
- package/server/workspace/journal/index.ts +10 -81
- package/server/workspace/journal/indexFile.ts +3 -24
- package/server/workspace/journal/latestDaily.ts +51 -0
- package/server/workspace/journal/memoryExtractor.ts +4 -20
- package/server/workspace/journal/optimizationPass.ts +4 -21
- package/server/workspace/journal/paths.ts +4 -23
- package/server/workspace/journal/state.ts +6 -29
- package/server/workspace/memory/io.ts +213 -0
- package/server/workspace/memory/llm-classifier.ts +158 -0
- package/server/workspace/memory/migrate.ts +263 -0
- package/server/workspace/memory/run.ts +84 -0
- package/server/workspace/memory/topic-cluster.ts +218 -0
- package/server/workspace/memory/topic-detect.ts +67 -0
- package/server/workspace/memory/topic-index-hook.ts +128 -0
- package/server/workspace/memory/topic-io.ts +180 -0
- package/server/workspace/memory/topic-migrate.ts +248 -0
- package/server/workspace/memory/topic-run.ts +172 -0
- package/server/workspace/memory/topic-swap.ts +135 -0
- package/server/workspace/memory/topic-types.ts +142 -0
- package/server/workspace/memory/types.ts +83 -0
- package/server/workspace/news/reader.ts +4 -5
- package/server/workspace/paths.ts +124 -47
- package/server/workspace/roles.ts +2 -11
- package/server/workspace/skills/parser.ts +38 -55
- package/server/workspace/skills/user-tasks.ts +1 -2
- package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
- package/server/workspace/skills-preset.ts +196 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
- package/server/workspace/sources/fetchers/index.ts +1 -1
- package/server/workspace/sources/fetchers/rssParser.ts +1 -1
- package/server/workspace/sources/pipeline/index.ts +2 -2
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/write.ts +2 -2
- package/server/workspace/sources/registry.ts +39 -61
- package/server/workspace/sources/robots.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +2 -1
- package/server/workspace/tool-trace/index.ts +1 -1
- package/server/workspace/tool-trace/writeSearch.ts +6 -1
- package/server/workspace/wiki-backlinks/index.ts +19 -7
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
- package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
- package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
- package/server/workspace/wiki-history/provision.ts +181 -0
- package/server/workspace/wiki-pages/io.ts +217 -0
- package/server/workspace/wiki-pages/snapshot.ts +380 -0
- package/server/workspace/workspace.ts +75 -13
- package/src/App.vue +115 -40
- package/src/_runtime/protocol-vue.ts +21 -0
- package/src/_runtime/vue.ts +22 -0
- package/src/components/ChatInput.vue +14 -10
- package/src/components/CopyChatButton.vue +76 -0
- package/src/components/FileContentRenderer.vue +67 -14
- package/src/components/FileTree.vue +2 -2
- package/src/components/FilesView.vue +17 -1
- package/src/components/NewsView.vue +16 -2
- package/src/components/NotificationBell.vue +320 -93
- package/src/components/PageChatComposer.vue +5 -4
- package/src/components/PluginLauncher.vue +42 -6
- package/src/components/PluginScopedRoot.vue +87 -0
- package/src/components/RoleSelector.vue +12 -1
- package/src/components/RolesView.vue +562 -0
- package/src/components/SentAttachmentChip.vue +102 -0
- package/src/components/SessionHistoryPanel.vue +109 -20
- package/src/components/SessionRoleIcon.vue +7 -4
- package/src/components/SessionSidebar.vue +20 -7
- package/src/components/SessionTabBar.vue +1 -1
- package/src/components/SettingsMcpTab.vue +4 -4
- package/src/components/SettingsModal.vue +2 -0
- package/src/components/SidebarHeader.vue +16 -5
- package/src/components/SourcesManager.vue +23 -9
- package/src/components/SourcesView.vue +1 -1
- package/src/components/StackView.vue +102 -6
- package/src/components/SuggestionsPanel.vue +105 -16
- package/src/components/SystemFileBanner.vue +1 -1
- package/src/components/TodoExplorer.vue +4 -5
- package/src/components/todo/TodoAddDialog.vue +2 -3
- package/src/components/todo/TodoEditDialog.vue +1 -2
- package/src/components/todo/TodoEditPanel.vue +2 -3
- package/src/components/todo/TodoKanbanView.vue +8 -5
- package/src/components/todo/TodoListView.vue +3 -5
- package/src/components/todo/TodoTableView.vue +7 -5
- package/src/composables/useAccountingChannel.ts +58 -0
- package/src/composables/useActiveSession.ts +4 -25
- package/src/composables/useAppApi.ts +6 -44
- package/src/composables/useClipboardCopy.ts +3 -20
- package/src/composables/useContentDisplay.ts +33 -2
- package/src/composables/useDevPluginReload.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +5 -31
- package/src/composables/useEventListeners.ts +0 -20
- package/src/composables/useExpandedDirs.ts +4 -15
- package/src/composables/useFaviconState.ts +12 -46
- package/src/composables/useFileChange.ts +53 -0
- package/src/composables/useFreshPluginData.ts +6 -43
- package/src/composables/useHealth.ts +14 -43
- package/src/composables/useImageErrorRepair.ts +104 -0
- package/src/composables/useLatestDaily.ts +40 -0
- package/src/composables/useMarkdownDoc.ts +39 -0
- package/src/composables/useMarkdownLinkHandler.ts +1 -1
- package/src/composables/useMcpTools.ts +3 -16
- package/src/composables/useNotifications.ts +138 -112
- package/src/composables/usePdfDownload.ts +17 -3
- package/src/composables/usePendingCalls.ts +8 -26
- package/src/composables/usePluginErrorBoundary.ts +68 -0
- package/src/composables/usePubSub.ts +9 -17
- package/src/composables/useRunElapsed.ts +5 -22
- package/src/composables/useSandboxStatus.ts +4 -20
- package/src/composables/useSessionDerived.ts +7 -15
- package/src/composables/useSessionHistory.ts +70 -29
- package/src/composables/useSessionSync.ts +25 -3
- package/src/composables/useSkillsList.ts +59 -0
- package/src/composables/useTranslatedQueries.ts +109 -0
- package/src/config/apiRoutes.ts +181 -80
- package/src/config/historyFilters.ts +5 -3
- package/src/config/hostEvents.ts +17 -0
- package/src/config/mcpCatalog.ts +277 -5
- package/src/config/pubsubChannels.ts +134 -12
- package/src/config/roles.ts +212 -147
- package/src/config/systemFileDescriptors.ts +5 -5
- package/src/config/toolNames.ts +52 -30
- package/src/config/workspacePaths.ts +26 -2
- package/src/lang/de.ts +483 -27
- package/src/lang/en.ts +448 -27
- package/src/lang/es.ts +474 -27
- package/src/lang/fr.ts +476 -27
- package/src/lang/ja.ts +465 -27
- package/src/lang/ko.ts +466 -27
- package/src/lang/pt-BR.ts +473 -27
- package/src/lang/zh.ts +463 -27
- package/src/lib/vue-i18n.ts +1 -1
- package/src/lib/wiki-page/slug.ts +66 -0
- package/src/main.ts +85 -0
- package/src/plugins/_extras.ts +58 -0
- package/src/plugins/_generated/metas.ts +42 -0
- package/src/plugins/_generated/registrations.ts +44 -0
- package/src/plugins/_generated/server-bindings.ts +47 -0
- package/src/plugins/accounting/Preview.vue +106 -0
- package/src/plugins/accounting/View.vue +632 -0
- package/src/plugins/accounting/actions.ts +34 -0
- package/src/plugins/accounting/api.ts +301 -0
- package/src/plugins/accounting/components/AccountEditor.vue +250 -0
- package/src/plugins/accounting/components/AccountRow.vue +50 -0
- package/src/plugins/accounting/components/AccountsList.vue +102 -0
- package/src/plugins/accounting/components/AccountsModal.vue +300 -0
- package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
- package/src/plugins/accounting/components/BookSettings.vue +284 -0
- package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
- package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
- package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
- package/src/plugins/accounting/components/JournalList.vue +553 -0
- package/src/plugins/accounting/components/Ledger.vue +206 -0
- package/src/plugins/accounting/components/NewBookForm.vue +211 -0
- package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
- package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
- package/src/plugins/accounting/components/accountDraft.ts +13 -0
- package/src/plugins/accounting/components/accountNumbering.ts +103 -0
- package/src/plugins/accounting/components/accountValidation.ts +75 -0
- package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
- package/src/plugins/accounting/countries.ts +158 -0
- package/src/plugins/accounting/currencies.ts +64 -0
- package/src/plugins/accounting/dates.ts +51 -0
- package/src/plugins/accounting/definition.ts +199 -0
- package/src/plugins/accounting/fiscalYear.ts +136 -0
- package/src/plugins/accounting/index.ts +49 -0
- package/src/plugins/accounting/meta.ts +91 -0
- package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
- package/src/plugins/api.ts +125 -0
- package/src/plugins/canvas/View.vue +38 -28
- package/src/plugins/canvas/definition.ts +10 -8
- package/src/plugins/canvas/index.ts +15 -8
- package/src/plugins/canvas/meta.ts +12 -0
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +2 -2
- package/src/plugins/chart/definition.ts +12 -2
- package/src/plugins/chart/index.ts +15 -7
- package/src/plugins/chart/meta.ts +18 -0
- package/src/plugins/editImages/definition.ts +44 -0
- package/src/plugins/editImages/index.ts +43 -0
- package/src/plugins/editImages/meta.ts +5 -0
- package/src/plugins/generateImage/View.vue +3 -1
- package/src/plugins/generateImage/definition.ts +2 -0
- package/src/plugins/generateImage/index.ts +13 -5
- package/src/plugins/generateImage/meta.ts +5 -0
- package/src/plugins/index.ts +35 -0
- package/src/plugins/manageRoles/Preview.vue +7 -4
- package/src/plugins/manageRoles/View.vue +12 -8
- package/src/plugins/manageRoles/definition.ts +6 -0
- package/src/plugins/manageRoles/index.ts +7 -6
- package/src/plugins/manageSkills/View.vue +11 -7
- package/src/plugins/manageSkills/definition.ts +4 -1
- package/src/plugins/manageSkills/index.ts +14 -7
- package/src/plugins/manageSkills/meta.ts +21 -0
- package/src/plugins/manageSource/definition.ts +4 -1
- package/src/plugins/manageSource/index.ts +15 -7
- package/src/plugins/manageSource/meta.ts +21 -0
- package/src/plugins/markdown/Preview.vue +10 -8
- package/src/plugins/markdown/View.vue +84 -17
- package/src/plugins/markdown/definition.ts +7 -1
- package/src/plugins/markdown/index.ts +15 -8
- package/src/plugins/markdown/meta.ts +16 -0
- package/src/plugins/meta-types.ts +97 -0
- package/src/plugins/metas.ts +224 -0
- package/src/plugins/presentForm/Preview.vue +4 -15
- package/src/plugins/presentForm/View.vue +35 -78
- package/src/plugins/presentForm/definition.ts +7 -6
- package/src/plugins/presentForm/index.ts +12 -5
- package/src/plugins/presentForm/meta.ts +11 -0
- package/src/plugins/presentForm/plugin.ts +8 -9
- package/src/plugins/presentForm/types.ts +0 -24
- package/src/plugins/presentHtml/Preview.vue +1 -8
- package/src/plugins/presentHtml/View.vue +401 -30
- package/src/plugins/presentHtml/definition.ts +8 -5
- package/src/plugins/presentHtml/index.ts +15 -8
- package/src/plugins/presentHtml/meta.ts +14 -0
- package/src/plugins/presentMulmoScript/View.vue +327 -107
- package/src/plugins/presentMulmoScript/definition.ts +34 -7
- package/src/plugins/presentMulmoScript/helpers.ts +4 -5
- package/src/plugins/presentMulmoScript/index.ts +20 -7
- package/src/plugins/presentMulmoScript/meta.ts +52 -0
- package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
- package/src/plugins/scheduler/Preview.vue +5 -2
- package/src/plugins/scheduler/TasksTab.vue +16 -36
- package/src/plugins/scheduler/View.vue +22 -54
- package/src/plugins/scheduler/automationsDefinition.ts +14 -9
- package/src/plugins/scheduler/automationsMeta.ts +5 -0
- package/src/plugins/scheduler/calendarDefinition.ts +4 -7
- package/src/plugins/scheduler/calendarMeta.ts +28 -0
- package/src/plugins/scheduler/formatSchedule.ts +6 -24
- package/src/plugins/scheduler/index.ts +26 -52
- package/src/plugins/scope.ts +57 -0
- package/src/plugins/server-bindings-types.ts +38 -0
- package/src/plugins/server.ts +32 -0
- package/src/plugins/skill/Preview.vue +25 -0
- package/src/plugins/skill/View.vue +125 -0
- package/src/plugins/skill/definition.ts +23 -0
- package/src/plugins/skill/index.ts +36 -0
- package/src/plugins/skill/plugin.ts +31 -0
- package/src/plugins/skill/types.ts +21 -0
- package/src/plugins/spreadsheet/Preview.vue +1 -3
- package/src/plugins/spreadsheet/View.vue +29 -49
- package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
- package/src/plugins/spreadsheet/definition.ts +5 -2
- package/src/plugins/spreadsheet/index.ts +15 -8
- package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
- package/src/plugins/spreadsheet/meta.ts +14 -0
- package/src/plugins/textResponse/Preview.vue +9 -1
- package/src/plugins/textResponse/View.vue +59 -8
- package/src/plugins/textResponse/index.ts +11 -3
- package/src/plugins/textResponse/plugin.ts +8 -10
- package/src/plugins/textResponse/types.ts +28 -0
- package/src/plugins/wiki/Preview.vue +6 -4
- package/src/plugins/wiki/View.vue +463 -254
- package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
- package/src/plugins/wiki/helpers.ts +17 -0
- package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
- package/src/plugins/wiki/history/HistoryTab.vue +167 -0
- package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
- package/src/plugins/wiki/history/api.ts +52 -0
- package/src/plugins/wiki/history/diff.ts +145 -0
- package/src/plugins/wiki/index.ts +42 -32
- package/src/plugins/wiki/meta.ts +10 -0
- package/src/plugins/wiki/pageEditLoader.ts +53 -0
- package/src/plugins/wiki/route.ts +8 -0
- package/src/router/guards.ts +2 -1
- package/src/router/index.ts +19 -0
- package/src/router/pageRoutes.ts +1 -0
- package/src/tools/index.ts +50 -51
- package/src/tools/runtimeLoader.ts +141 -0
- package/src/tools/types.ts +44 -1
- package/src/types/notification.ts +23 -0
- package/src/types/pastedFile.ts +10 -0
- package/src/types/session.ts +61 -3
- package/src/types/sse.ts +21 -6
- package/src/utils/agent/eventDispatch.ts +12 -9
- package/src/utils/agent/pastedAttachment.ts +35 -0
- package/src/utils/agent/request.ts +32 -3
- package/src/utils/agent/toolCalls.ts +7 -1
- package/src/utils/api.ts +1 -1
- package/src/utils/chat/exportMarkdown.ts +243 -0
- package/src/utils/errors.ts +10 -2
- package/src/utils/files/expandedDirs.ts +1 -1
- package/src/utils/filesPreview/todoPreview.ts +13 -2
- package/src/utils/format/date.ts +1 -3
- package/src/utils/format/jsonSyntax.ts +5 -0
- package/src/utils/html/iframeHeightReporterScript.ts +62 -0
- package/src/utils/html/previewCsp.ts +29 -2
- package/src/utils/image/htmlSrcAttrs.ts +122 -0
- package/src/utils/image/imageRepairInlineScript.ts +115 -0
- package/src/utils/image/resolve.ts +17 -3
- package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
- package/src/utils/markdown/frontmatter.ts +125 -0
- package/src/utils/markdown/taskList.ts +7 -2
- package/src/utils/plugin/runtime.ts +132 -0
- package/src/utils/session/mergeSessions.ts +40 -37
- package/src/utils/session/sessionEntries.ts +74 -18
- package/src/utils/session/sessionHelpers.ts +54 -10
- package/src/utils/tools/result.ts +76 -14
- package/src/vite-env.d.ts +6 -0
- package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
- package/client/assets/index-CY-WpQUm.css +0 -2
- package/client/assets/index-DbTz2Mfs.js +0 -4911
- package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
- package/server/api/routes/html.ts +0 -114
- package/server/api/routes/todos.ts +0 -293
- package/server/api/routes/todosColumnsHandlers.ts +0 -333
- package/server/api/routes/todosHandlers.ts +0 -274
- package/server/api/routes/todosItemsHandlers.ts +0 -386
- package/server/utils/files/todos-io.ts +0 -29
- package/src/components/NotificationToast.vue +0 -75
- package/src/plugins/editImage/definition.ts +0 -27
- package/src/plugins/editImage/index.ts +0 -37
- package/src/plugins/presentHtml/helpers.ts +0 -72
- package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
- package/src/plugins/scheduler/legacyShape.ts +0 -34
- package/src/plugins/todo/Preview.vue +0 -68
- package/src/plugins/todo/View.vue +0 -378
- package/src/plugins/todo/composables/useTodos.ts +0 -179
- package/src/plugins/todo/definition.ts +0 -45
- package/src/plugins/todo/index.ts +0 -62
- package/src/plugins/todo/labels.ts +0 -163
- package/src/plugins/todo/priority.ts +0 -98
- package/src/plugins/todo/viewModes.ts +0 -19
- package/src/plugins/wiki/definition.ts +0 -25
- package/src/tools/legacyPluginNames.ts +0 -13
- package/src/utils/format/frontmatter.ts +0 -80
- package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
- package/src/utils/notification/dispatch.ts +0 -58
- /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
- /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
- /package/src/plugins/{editImage → editImages}/View.vue +0 -0
- /package/src/{config/schedulerActions.ts → plugins/scheduler/actions.ts} +0 -0
|
@@ -35,6 +35,7 @@ import { normalizeCategories, type CategorySlug } from "../../workspace/sources/
|
|
|
35
35
|
import { badRequest, conflict, sendError, serverError } from "../../utils/httpError.js";
|
|
36
36
|
import { errorMessage } from "../../utils/errors.js";
|
|
37
37
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
38
|
+
import { bindRoute } from "../../utils/router.js";
|
|
38
39
|
import { isNonEmptyString, isRecord } from "../../utils/types.js";
|
|
39
40
|
|
|
40
41
|
const router = Router();
|
|
@@ -55,7 +56,7 @@ interface ErrorResponse {
|
|
|
55
56
|
error: string;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
router
|
|
59
|
+
bindRoute(router, API_ROUTES.sources.list, async (_req: Request, res: Response<ListSourcesResponse | ErrorResponse>) => {
|
|
59
60
|
try {
|
|
60
61
|
const sources = await listSources(workspacePath);
|
|
61
62
|
res.json({ sources });
|
|
@@ -88,45 +89,49 @@ interface RegisterSourceResponse {
|
|
|
88
89
|
classifyRationale?: string;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
92
|
+
bindRoute(
|
|
93
|
+
router,
|
|
94
|
+
API_ROUTES.sources.create,
|
|
95
|
+
async (req: Request<object, unknown, RegisterSourceBody>, res: Response<RegisterSourceResponse | ErrorResponse>) => {
|
|
96
|
+
const parsed = parseRegisterBody(req.body ?? {});
|
|
97
|
+
if ("error" in parsed) {
|
|
98
|
+
sendError(res, parsed.status, parsed.error);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const existing = await readSource(workspacePath, parsed.slug);
|
|
102
|
+
if (existing) {
|
|
103
|
+
conflict(res, `source "${parsed.slug}" already exists`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const { categories, rationale } = await resolveCategories(parsed);
|
|
107
|
+
const source: Source = {
|
|
108
|
+
slug: parsed.slug,
|
|
109
|
+
title: parsed.title,
|
|
110
|
+
url: parsed.url,
|
|
111
|
+
fetcherKind: parsed.fetcherKind,
|
|
112
|
+
fetcherParams: parsed.fetcherParams,
|
|
113
|
+
schedule: parsed.schedule,
|
|
114
|
+
categories,
|
|
115
|
+
maxItemsPerFetch: parsed.maxItemsPerFetch,
|
|
116
|
+
addedAt: new Date().toISOString(),
|
|
117
|
+
notes: parsed.notes,
|
|
118
|
+
};
|
|
119
|
+
try {
|
|
120
|
+
await writeSource(workspacePath, source);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
serverError(res, errorMessage(err, "failed to write source"));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
log.info("sources", "source registered", {
|
|
126
|
+
slug: parsed.slug,
|
|
127
|
+
fetcherKind: parsed.fetcherKind,
|
|
128
|
+
});
|
|
129
|
+
res.status(201).json({
|
|
130
|
+
source,
|
|
131
|
+
...(rationale !== undefined && { classifyRationale: rationale }),
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
);
|
|
130
135
|
|
|
131
136
|
// --- DELETE /api/sources/:slug ------------------------------------------
|
|
132
137
|
|
|
@@ -139,7 +144,7 @@ interface DeleteSourceResponse {
|
|
|
139
144
|
stateRemoved: boolean;
|
|
140
145
|
}
|
|
141
146
|
|
|
142
|
-
router
|
|
147
|
+
bindRoute(router, API_ROUTES.sources.remove, async (req: Request<DeleteSourceParams>, res: Response<DeleteSourceResponse | ErrorResponse>) => {
|
|
143
148
|
const { slug } = req.params;
|
|
144
149
|
if (!isValidSlug(slug)) {
|
|
145
150
|
badRequest(res, "invalid slug");
|
|
@@ -159,7 +164,7 @@ interface RebuildBody {
|
|
|
159
164
|
scheduleType?: unknown;
|
|
160
165
|
}
|
|
161
166
|
|
|
162
|
-
router
|
|
167
|
+
bindRoute(router, API_ROUTES.sources.rebuild, async (req: Request<object, unknown, RebuildBody>, res: Response<ErrorResponse | Record<string, unknown>>) => {
|
|
163
168
|
const scheduleType = validateSchedule(req.body?.scheduleType, "daily");
|
|
164
169
|
if (!scheduleType) {
|
|
165
170
|
badRequest(res, `scheduleType must be one of: ${[...SOURCE_SCHEDULES].join(", ")}`);
|
|
@@ -239,7 +244,7 @@ interface ManageSourceSuccess {
|
|
|
239
244
|
|
|
240
245
|
const MANAGE_ACTIONS = new Set(["list", "register", "remove", "rebuild"]);
|
|
241
246
|
|
|
242
|
-
router
|
|
247
|
+
bindRoute(router, API_ROUTES.sources.manage, async (req: Request<object, unknown, ManageSourceBody>, res: Response<ManageSourceSuccess | ErrorResponse>) => {
|
|
243
248
|
const action = req.body?.action;
|
|
244
249
|
if (typeof action !== "string" || !MANAGE_ACTIONS.has(action)) {
|
|
245
250
|
badRequest(res, `action must be one of: ${[...MANAGE_ACTIONS].join(", ")}`);
|
|
@@ -258,7 +263,6 @@ router.post(API_ROUTES.sources.manage, async (req: Request<object, unknown, Mana
|
|
|
258
263
|
return;
|
|
259
264
|
case "rebuild":
|
|
260
265
|
await handleRebuild(res);
|
|
261
|
-
return;
|
|
262
266
|
}
|
|
263
267
|
} catch (err) {
|
|
264
268
|
log.warn("sources", "manage failed", { action, error: String(err) });
|
|
@@ -433,7 +437,10 @@ interface ParsedRegisterBody {
|
|
|
433
437
|
skipClassify: boolean;
|
|
434
438
|
}
|
|
435
439
|
|
|
436
|
-
|
|
440
|
+
interface ParseError {
|
|
441
|
+
status: number;
|
|
442
|
+
error: string;
|
|
443
|
+
}
|
|
437
444
|
|
|
438
445
|
function parseRegisterBody(body: RegisterSourceBody): ParsedRegisterBody | ParseError {
|
|
439
446
|
const title = typeof body.title === "string" ? body.title.trim() : "";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Translation HTTP route — POST /api/translation. Thin handler that
|
|
2
|
+
// delegates to the translation service; validation lives there.
|
|
3
|
+
|
|
4
|
+
import { Router, type Request, type Response } from "express";
|
|
5
|
+
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
6
|
+
import { createTranslationService, TranslationInputError } from "../../services/translation/index.js";
|
|
7
|
+
import { defaultTranslateBatch } from "../../services/translation/llm.js";
|
|
8
|
+
import { log } from "../../system/logger/index.js";
|
|
9
|
+
import type { TranslateBatchFn, TranslateRequest, TranslateResponse } from "../../services/translation/types.js";
|
|
10
|
+
|
|
11
|
+
export interface TranslationRouteDeps {
|
|
12
|
+
/** Override for tests — defaults to the live workspace root. */
|
|
13
|
+
workspaceRoot?: string;
|
|
14
|
+
/** Override for tests — defaults to the production claude-CLI backend. */
|
|
15
|
+
translateBatch?: TranslateBatchFn;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface TranslateErrorBody {
|
|
19
|
+
error: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createTranslationRouter(deps: TranslationRouteDeps = {}): Router {
|
|
23
|
+
const router = Router();
|
|
24
|
+
const service = createTranslationService({
|
|
25
|
+
translateBatch: deps.translateBatch ?? defaultTranslateBatch,
|
|
26
|
+
workspaceRoot: deps.workspaceRoot,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
router.post(API_ROUTES.translation.translate, async (req: Request, res: Response<TranslateResponse | TranslateErrorBody>) => {
|
|
30
|
+
try {
|
|
31
|
+
const result = await service.translate(req.body as TranslateRequest);
|
|
32
|
+
res.json(result);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err instanceof TranslationInputError) {
|
|
35
|
+
res.status(400).json({ error: err.message });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
log.error("translation-route", "translate failed", { error: String(err) });
|
|
39
|
+
res.status(500).json({ error: "translation failed" });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return router;
|
|
44
|
+
}
|
|
@@ -1,47 +1,18 @@
|
|
|
1
|
-
// Narrow
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// and block-list style:
|
|
1
|
+
// Narrow `tags:` field reader for wiki page files. Built on the
|
|
2
|
+
// shared `parseFrontmatter` util (#895 PR C) — js-yaml handles
|
|
3
|
+
// both flow style (`tags: [a, b, c]`) and block-list style:
|
|
5
4
|
//
|
|
6
5
|
// tags:
|
|
7
6
|
// - a
|
|
8
7
|
// - b
|
|
9
8
|
//
|
|
9
|
+
// out of the box, so we just normalise the resulting strings.
|
|
10
|
+
//
|
|
10
11
|
// Anything unparseable returns `[]` — callers use this for a
|
|
11
12
|
// best-effort comparison against index.md, so a noisy file should
|
|
12
13
|
// degrade silently, not throw.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
// matching (no lazy quantifier) so sonarjs/slow-regex stays happy.
|
|
16
|
-
const BLOCK_LIST_ITEM_PATTERN = /^\s*-\s+(\S.*)$/;
|
|
17
|
-
|
|
18
|
-
// Pull the inner list from a line that starts with `tags:` and
|
|
19
|
-
// contains a `[...]` flow list. Returns null when the line isn't a
|
|
20
|
-
// flow-style tags line. Bracket matching is done with `indexOf` so
|
|
21
|
-
// we don't need a lazy-quantified regex.
|
|
22
|
-
function extractFlowTagsCell(line: string): string | null {
|
|
23
|
-
const trimmed = line.trimStart();
|
|
24
|
-
if (!trimmed.startsWith("tags:")) return null;
|
|
25
|
-
const open = trimmed.indexOf("[");
|
|
26
|
-
if (open === -1) return null;
|
|
27
|
-
const close = trimmed.indexOf("]", open + 1);
|
|
28
|
-
if (close === -1) return null;
|
|
29
|
-
return trimmed.slice(open + 1, close);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Extract the YAML frontmatter body with plain string scanning so
|
|
33
|
-
// we don't rely on a lazy-quantified regex (which ESLint's slow-regex
|
|
34
|
-
// rule flags for super-linear backtracking when the closing fence is
|
|
35
|
-
// missing). Returns null when no well-formed `---\n…\n---` block is
|
|
36
|
-
// present at the top of the file.
|
|
37
|
-
function extractFrontmatterBody(content: string): string | null {
|
|
38
|
-
if (!content.startsWith("---")) return null;
|
|
39
|
-
const after = content.indexOf("\n");
|
|
40
|
-
if (after === -1) return null;
|
|
41
|
-
const close = content.indexOf("\n---", after);
|
|
42
|
-
if (close === -1) return null;
|
|
43
|
-
return content.slice(after + 1, close);
|
|
44
|
-
}
|
|
15
|
+
import { parseFrontmatter } from "../../../utils/markdown/frontmatter.js";
|
|
45
16
|
|
|
46
17
|
function cleanTagToken(token: string): string {
|
|
47
18
|
return token
|
|
@@ -51,36 +22,13 @@ function cleanTagToken(token: string): string {
|
|
|
51
22
|
.toLowerCase();
|
|
52
23
|
}
|
|
53
24
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
25
|
+
export function parseFrontmatterTags(content: string): string[] {
|
|
26
|
+
const parsed = parseFrontmatter(content);
|
|
27
|
+
if (!parsed.hasHeader) return [];
|
|
28
|
+
const tagsValue = parsed.meta.tags;
|
|
29
|
+
if (!Array.isArray(tagsValue)) return [];
|
|
30
|
+
return tagsValue
|
|
31
|
+
.filter((item): item is string => typeof item === "string")
|
|
57
32
|
.map(cleanTagToken)
|
|
58
33
|
.filter((token) => token.length > 0);
|
|
59
34
|
}
|
|
60
|
-
|
|
61
|
-
function parseBlockList(lines: string[], startIndex: number): string[] {
|
|
62
|
-
const tags: string[] = [];
|
|
63
|
-
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
64
|
-
const line = lines[i];
|
|
65
|
-
// Block list ends at the first line that isn't a list item —
|
|
66
|
-
// blank line, next key, or unindented text.
|
|
67
|
-
if (/^\S/.test(line) || line.trim() === "") break;
|
|
68
|
-
const match = BLOCK_LIST_ITEM_PATTERN.exec(line);
|
|
69
|
-
if (!match) break;
|
|
70
|
-
const token = cleanTagToken(match[1].trimEnd());
|
|
71
|
-
if (token.length > 0) tags.push(token);
|
|
72
|
-
}
|
|
73
|
-
return tags;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function parseFrontmatterTags(content: string): string[] {
|
|
77
|
-
const body = extractFrontmatterBody(content);
|
|
78
|
-
if (body === null) return [];
|
|
79
|
-
const lines = body.split(/\r?\n/);
|
|
80
|
-
for (let i = 0; i < lines.length; i++) {
|
|
81
|
-
const flow = extractFlowTagsCell(lines[i]);
|
|
82
|
-
if (flow !== null) return parseFlowList(flow);
|
|
83
|
-
if (/^tags:\s*$/.test(lines[i])) return parseBlockList(lines, i);
|
|
84
|
-
}
|
|
85
|
-
return [];
|
|
86
|
-
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Wiki page edit-history routes (#763 PR 2). Three endpoints:
|
|
2
|
+
//
|
|
3
|
+
// GET /api/wiki/pages/:slug/history — list snapshots (meta-only)
|
|
4
|
+
// GET /api/wiki/pages/:slug/history/:stamp — read one snapshot
|
|
5
|
+
// POST /api/wiki/pages/:slug/history/:stamp/restore — round-trip the
|
|
6
|
+
// snapshot through `writeWikiPage` (which snapshots the restore
|
|
7
|
+
// itself, so undo stays cheap).
|
|
8
|
+
//
|
|
9
|
+
// Path safety: both `:slug` and `:stamp` are validated *before*
|
|
10
|
+
// they are joined with the workspace root. The slug check matches
|
|
11
|
+
// `wiki-pages/io.ts`'s `isSafeSlug`; the stamp check is the
|
|
12
|
+
// `FILENAME_RE` shape exposed via `isSafeStamp`.
|
|
13
|
+
|
|
14
|
+
import { Router, type Request, type Response } from "express";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { TOOL_NAMES } from "../../../../src/config/toolNames.js";
|
|
18
|
+
import { hasMeaningfulChange, writeWikiPage } from "../../../workspace/wiki-pages/io.js";
|
|
19
|
+
import { WORKSPACE_DIRS } from "../../../workspace/paths.js";
|
|
20
|
+
import { isSafeStamp, listSnapshots, readSnapshot, stripSnapshotMeta } from "../../../workspace/wiki-pages/snapshot.js";
|
|
21
|
+
import { mergeFrontmatter, serializeWithFrontmatter } from "../../../utils/markdown/frontmatter.js";
|
|
22
|
+
import { badRequest, notFound } from "../../../utils/httpError.js";
|
|
23
|
+
import { readTextOrNull } from "../../../utils/files/safe.js";
|
|
24
|
+
import { workspacePath } from "../../../workspace/workspace.js";
|
|
25
|
+
import { pushToolResult } from "../../../events/session-store/index.js";
|
|
26
|
+
import { log } from "../../../system/logger/index.js";
|
|
27
|
+
|
|
28
|
+
const router = Router();
|
|
29
|
+
|
|
30
|
+
// Mirrors `isSafeSlug` from wiki-pages/io.ts (kept independent so
|
|
31
|
+
// the route layer doesn't import the helper through a circular
|
|
32
|
+
// dependency — io.ts already imports snapshot.ts).
|
|
33
|
+
function isSafeSlug(slug: string): boolean {
|
|
34
|
+
if (slug.length === 0) return false;
|
|
35
|
+
if (slug === "." || slug === "..") return false;
|
|
36
|
+
if (slug.includes("/") || slug.includes("\\")) return false;
|
|
37
|
+
if (slug.includes("\0")) return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Restore is a write under the user's workspace; record a short
|
|
42
|
+
// reason on the new snapshot so the history reads "Restored from
|
|
43
|
+
// 2026-04-28T01-23-45-789Z" rather than an empty cell. Editor stays
|
|
44
|
+
// `user` because the human triggered the restore — same shape as
|
|
45
|
+
// every other UI-driven save today.
|
|
46
|
+
function restoreReason(stamp: string): string {
|
|
47
|
+
return `Restored from ${stamp}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Re-build the previous snapshot's content as a single
|
|
51
|
+
// frontmatter+body string for `hasMeaningfulChange` to compare
|
|
52
|
+
// against. `_snapshot_*` keys are stripped first — those are
|
|
53
|
+
// snapshot-event metadata, not part of the page itself, and
|
|
54
|
+
// keeping them in the diff would always flag a difference.
|
|
55
|
+
async function loadPreviousSnapshotContent(slug: string): Promise<string | null> {
|
|
56
|
+
const recent = await listSnapshots(slug, { workspaceRoot: workspacePath });
|
|
57
|
+
if (recent.length === 0) return null;
|
|
58
|
+
const latest = await readSnapshot(slug, recent[0].stamp, { workspaceRoot: workspacePath });
|
|
59
|
+
if (latest === null) return null;
|
|
60
|
+
return serializeWithFrontmatter(stripSnapshotMeta(latest.meta), latest.body);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
router.get("/pages/:slug/history", async (req: Request<{ slug: string }>, res: Response) => {
|
|
64
|
+
const { slug } = req.params;
|
|
65
|
+
if (!isSafeSlug(slug)) {
|
|
66
|
+
badRequest(res, "Unsafe slug");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Don't gate on the live page existing. Snapshots are non-destructive
|
|
70
|
+
// and outlive their page — gating here would make history disappear
|
|
71
|
+
// exactly when the user needs it (deleted/renamed page → can't see
|
|
72
|
+
// history → can't restore). An empty list still answers "no history"
|
|
73
|
+
// unambiguously (codex review iter-2 #917).
|
|
74
|
+
const snapshots = await listSnapshots(slug);
|
|
75
|
+
res.json({ slug, snapshots });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
router.get("/pages/:slug/history/:stamp", async (req: Request<{ slug: string; stamp: string }>, res: Response) => {
|
|
79
|
+
const { slug, stamp } = req.params;
|
|
80
|
+
if (!isSafeSlug(slug)) {
|
|
81
|
+
badRequest(res, "Unsafe slug");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!isSafeStamp(stamp)) {
|
|
85
|
+
badRequest(res, "Unsafe stamp");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const snapshot = await readSnapshot(slug, stamp);
|
|
89
|
+
if (snapshot === null) {
|
|
90
|
+
notFound(res, `snapshot not found: ${slug}/${stamp}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
res.json({ slug, snapshot });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
router.post("/pages/:slug/history/:stamp/restore", async (req: Request<{ slug: string; stamp: string }>, res: Response) => {
|
|
97
|
+
const { slug, stamp } = req.params;
|
|
98
|
+
if (!isSafeSlug(slug)) {
|
|
99
|
+
badRequest(res, "Unsafe slug");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!isSafeStamp(stamp)) {
|
|
103
|
+
badRequest(res, "Unsafe stamp");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const snapshot = await readSnapshot(slug, stamp);
|
|
107
|
+
if (snapshot === null) {
|
|
108
|
+
notFound(res, `snapshot not found: ${slug}/${stamp}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Strip `_snapshot_*` keys before writing — they describe the
|
|
113
|
+
// *original* save event and would be misleading on the restored
|
|
114
|
+
// page. `writeWikiPage` will re-stamp `updated` and the new
|
|
115
|
+
// snapshot will get a fresh `_snapshot_ts` for the restore event.
|
|
116
|
+
const liveMeta = stripSnapshotMeta(snapshot.meta);
|
|
117
|
+
const restoredContent = serializeWithFrontmatter(mergeFrontmatter({}, liveMeta), snapshot.body);
|
|
118
|
+
|
|
119
|
+
// forceSnapshot=true so a "restore to identical content" still
|
|
120
|
+
// produces an audit entry — without it the no-op gate in
|
|
121
|
+
// writeWikiPage would swallow the restore silently.
|
|
122
|
+
await writeWikiPage(slug, restoredContent, {
|
|
123
|
+
editor: "user",
|
|
124
|
+
reason: restoreReason(stamp),
|
|
125
|
+
forceSnapshot: true,
|
|
126
|
+
});
|
|
127
|
+
log.info("wiki", "history restore", { slug, stamp });
|
|
128
|
+
res.json({ slug, restored: { fromStamp: stamp } });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── Internal endpoint (LLM write hook callback) ────────────────
|
|
132
|
+
//
|
|
133
|
+
// Hit by `<workspace>/.claude/hooks/wiki-snapshot.mjs` after the
|
|
134
|
+
// claude CLI completes a `Write` / `Edit` tool call. The hook
|
|
135
|
+
// computes the slug from the file path it just touched and
|
|
136
|
+
// passes it here; the server resolves the slug to its OWN
|
|
137
|
+
// `data/wiki/pages/` filesystem location, reads disk state, and
|
|
138
|
+
// drops a snapshot through the same `appendSnapshot` path the
|
|
139
|
+
// in-process writers use. Always tagged `editor: "llm"` —
|
|
140
|
+
// user-driven writes go through the regular `writeWikiPage`
|
|
141
|
+
// path with their own editor identity.
|
|
142
|
+
//
|
|
143
|
+
// Why slug-not-absPath: in Docker mode the hook runs inside the
|
|
144
|
+
// container where the workspace lives at `/home/node/mulmoclaude/`
|
|
145
|
+
// while the server (running on the host) sees the same files at
|
|
146
|
+
// `/Users/<user>/mulmoclaude/`. Sending the absolute path forces
|
|
147
|
+
// either side to translate; sending the slug lets each side keep
|
|
148
|
+
// its own filesystem view.
|
|
149
|
+
//
|
|
150
|
+
// `sessionId` lets the snapshot carry the chat-session identifier
|
|
151
|
+
// that drove the write, surfaced from Claude CLI's `session_id`
|
|
152
|
+
// hook payload field. There is no `reason` — the LLM doesn't
|
|
153
|
+
// supply one, and in-process callers (writeWikiPage) attach
|
|
154
|
+
// their own reasons through `WikiWriteMeta` directly.
|
|
155
|
+
//
|
|
156
|
+
// Bearer auth applies via the global `app.use("/api", bearerAuth)`
|
|
157
|
+
// in server/index.ts; no extra check needed here.
|
|
158
|
+
|
|
159
|
+
interface InternalSnapshotBody {
|
|
160
|
+
slug?: string;
|
|
161
|
+
sessionId?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
router.post("/internal/snapshot", async (req: Request<object, unknown, InternalSnapshotBody>, res: Response) => {
|
|
165
|
+
const { slug, sessionId } = req.body ?? {};
|
|
166
|
+
if (typeof slug !== "string" || slug.length === 0) {
|
|
167
|
+
badRequest(res, "slug required");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!isSafeSlug(slug)) {
|
|
171
|
+
badRequest(res, "slug is not safe");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const pagePath = path.join(workspacePath, WORKSPACE_DIRS.wikiPages, `${slug}.md`);
|
|
176
|
+
const content = await readTextOrNull(pagePath);
|
|
177
|
+
if (content === null) {
|
|
178
|
+
notFound(res, "wiki page not found on disk");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Dedupe against the most recent snapshot — Write/Edit hooks
|
|
183
|
+
// fire for every tool call, including ones that only re-stamp
|
|
184
|
+
// `updated` / `editor` without touching the body. Without this
|
|
185
|
+
// guard the history page accumulates duplicate entries (user
|
|
186
|
+
// report 2026-04-30: two identical bodies snapped 2.6s apart).
|
|
187
|
+
// `hasMeaningfulChange` already drives the in-process
|
|
188
|
+
// `writeWikiPage` path; reusing it keeps both paths aligned.
|
|
189
|
+
const previousContent = await loadPreviousSnapshotContent(slug);
|
|
190
|
+
if (previousContent !== null && !hasMeaningfulChange(previousContent, content)) {
|
|
191
|
+
log.info("wiki", "internal snapshot skipped — no meaningful change since previous snapshot", { slug });
|
|
192
|
+
res.json({ slug, ok: true, skipped: "no-meaningful-change" });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// The hook only fires for claude-CLI-driven writes — by
|
|
197
|
+
// construction the agent is the actor. User-driven manual saves
|
|
198
|
+
// go through writeWikiPage in-process and never reach here.
|
|
199
|
+
const { appendSnapshot } = await import("../../../workspace/wiki-pages/snapshot.js");
|
|
200
|
+
const stamp = await appendSnapshot(
|
|
201
|
+
slug,
|
|
202
|
+
null,
|
|
203
|
+
content,
|
|
204
|
+
{
|
|
205
|
+
editor: "llm",
|
|
206
|
+
...(typeof sessionId === "string" && sessionId.length > 0 && { sessionId }),
|
|
207
|
+
},
|
|
208
|
+
{ workspaceRoot: workspacePath },
|
|
209
|
+
);
|
|
210
|
+
log.info("wiki", "internal snapshot recorded", { slug });
|
|
211
|
+
|
|
212
|
+
// Stage 3a (#963): publish a synthetic `manageWiki` toolResult
|
|
213
|
+
// into the session timeline so the canvas shows what the LLM
|
|
214
|
+
// just wrote. The View dispatch (existing manageWiki plugin)
|
|
215
|
+
// picks up the new `page-edit` action and fetches the snapshot
|
|
216
|
+
// body via /api/wiki/pages/:slug/history/:stamp on render —
|
|
217
|
+
// JSONL stays small (~150 bytes per write) because we store
|
|
218
|
+
// the snapshot reference, not the body. `pagePath` is a GC
|
|
219
|
+
// fallback: if the snapshot is gc'd before render, the View
|
|
220
|
+
// falls back to reading the live page file.
|
|
221
|
+
// Wrapped in try/catch so a publish failure (e.g. JSONL append
|
|
222
|
+
// throws) doesn't fail the whole route — the snapshot was
|
|
223
|
+
// already written, and the hook is fire-and-forget. Without
|
|
224
|
+
// this guard the route would 500 even though the wiki write
|
|
225
|
+
// itself succeeded; the next save would still snapshot fine,
|
|
226
|
+
// but the canvas would silently lose this one preview
|
|
227
|
+
// (CodeRabbit review).
|
|
228
|
+
if (typeof sessionId === "string" && sessionId.length > 0) {
|
|
229
|
+
try {
|
|
230
|
+
const outcome = await pushToolResult(sessionId, {
|
|
231
|
+
uuid: randomUUID(),
|
|
232
|
+
toolName: TOOL_NAMES.manageWiki,
|
|
233
|
+
data: {
|
|
234
|
+
// `"page-edit"` is the action discriminator the wiki
|
|
235
|
+
// plugin's `View.vue` switches on. It's repeated in the
|
|
236
|
+
// plugin and in `src/plugins/wiki/pageEditLoader.ts`; a
|
|
237
|
+
// shared `WIKI_ACTIONS` const would be the cleaner home
|
|
238
|
+
// but that's a multi-file refactor — out of scope for
|
|
239
|
+
// this CR follow-up.
|
|
240
|
+
action: "page-edit",
|
|
241
|
+
title: slug,
|
|
242
|
+
slug,
|
|
243
|
+
stamp,
|
|
244
|
+
pagePath: path.posix.join(WORKSPACE_DIRS.wikiPages, `${slug}.md`),
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
if (outcome.kind === "skipped") {
|
|
248
|
+
log.warn("wiki", "page-edit toolResult publish skipped", { slug, reason: outcome.reason });
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
log.warn("wiki", "page-edit toolResult publish failed", {
|
|
252
|
+
slug,
|
|
253
|
+
error: err instanceof Error ? err.message : String(err),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
res.json({ slug, ok: true });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
export default router;
|
|
@@ -39,7 +39,7 @@ export async function getPageIndex(pagesDir: string): Promise<PageIndex> {
|
|
|
39
39
|
const slugs = new Map<string, string>();
|
|
40
40
|
for (const entry of entries) {
|
|
41
41
|
if (!entry.isFile()) continue;
|
|
42
|
-
const name = entry
|
|
42
|
+
const { name } = entry;
|
|
43
43
|
if (!name.endsWith(".md")) continue;
|
|
44
44
|
slugs.set(name.slice(0, -".md".length), name);
|
|
45
45
|
}
|