mulmoclaude 0.5.3 → 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/README.md +17 -4
- package/bin/mulmoclaude.js +46 -15
- 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 +17 -17
- 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
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Atomic-to-topic migration (#1070 PR-A).
|
|
2
|
+
//
|
|
3
|
+
// Reads the existing #1029-style atomic entries from
|
|
4
|
+
// `conversations/memory/`, runs a clusterer, and writes the
|
|
5
|
+
// proposed topic layout to a STAGING dir
|
|
6
|
+
// `conversations/memory.next/`. Does NOT swap. The user runs
|
|
7
|
+
// `topic-swap.ts` after reviewing.
|
|
8
|
+
//
|
|
9
|
+
// Library only — `runTopicMigrationOnce` (in PR-B) decides when
|
|
10
|
+
// to call this from server startup.
|
|
11
|
+
//
|
|
12
|
+
// CLEANUP 2026-07-01: see `topic-run.ts` — this file is part of
|
|
13
|
+
// the one-shot atomic → topic migration chain and goes when the
|
|
14
|
+
// chain goes.
|
|
15
|
+
|
|
16
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
import { writeFileAtomic } from "../../utils/files/atomic.js";
|
|
20
|
+
import { log } from "../../system/logger/index.js";
|
|
21
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
22
|
+
import { WORKSPACE_DIRS } from "../paths.js";
|
|
23
|
+
import { loadAllMemoryEntries } from "./io.js";
|
|
24
|
+
import { MEMORY_TYPES, type MemoryEntry, type MemoryType } from "./types.js";
|
|
25
|
+
import type { ClusterMap, ClusterTopic, MemoryClusterer } from "./topic-cluster.js";
|
|
26
|
+
import { MAX_TOPIC_SLUG_LENGTH } from "./topic-types.js";
|
|
27
|
+
|
|
28
|
+
export interface TopicMigrationResult {
|
|
29
|
+
/** Whether anything was emitted to the staging dir. */
|
|
30
|
+
noop: boolean;
|
|
31
|
+
/** Atomic entries that fed the cluster call. */
|
|
32
|
+
inputCount: number;
|
|
33
|
+
/** Topic files written to the staging dir, per type. */
|
|
34
|
+
topicCounts: Record<MemoryType, number>;
|
|
35
|
+
/** Bullets the clusterer omitted (sum across types). */
|
|
36
|
+
bulletsLost: number;
|
|
37
|
+
/** Where the staging dir lives. */
|
|
38
|
+
stagingPath: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function topicStagingPath(workspaceRoot: string): string {
|
|
42
|
+
return path.join(workspaceRoot, WORKSPACE_DIRS.memoryStaging);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface WrittenTopic {
|
|
46
|
+
type: MemoryType;
|
|
47
|
+
topic: ClusterTopic;
|
|
48
|
+
/** Slug actually written. May differ from `topic.topic` when a
|
|
49
|
+
* collision was resolved with a `-2` / `-3` suffix. */
|
|
50
|
+
writtenSlug: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function clusterAtomicIntoStaging(workspaceRoot: string, clusterer: MemoryClusterer): Promise<TopicMigrationResult> {
|
|
54
|
+
const entries = await loadAllMemoryEntries(workspaceRoot);
|
|
55
|
+
const stagingPath = topicStagingPath(workspaceRoot);
|
|
56
|
+
if (entries.length === 0) {
|
|
57
|
+
return emptyResult(stagingPath);
|
|
58
|
+
}
|
|
59
|
+
log.info("memory", "topic-migrate: clustering", { entryCount: entries.length });
|
|
60
|
+
// Wipe stale staging BEFORE the cluster call. If the clusterer
|
|
61
|
+
// returns null or throws, we leave the workspace with no staging
|
|
62
|
+
// dir at all — that's the correct "migration didn't complete"
|
|
63
|
+
// signal. The earlier flow only cleared on success and could
|
|
64
|
+
// leave a stale tree in place after a failed run, which a later
|
|
65
|
+
// swap would happily promote.
|
|
66
|
+
await resetStaging(stagingPath);
|
|
67
|
+
let map: ClusterMap | null = null;
|
|
68
|
+
let clustererThrew = false;
|
|
69
|
+
try {
|
|
70
|
+
map = await clusterer(entries);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
clustererThrew = true;
|
|
73
|
+
log.error("memory", "topic-migrate: clusterer threw", { error: errorMessage(err) });
|
|
74
|
+
}
|
|
75
|
+
if (!map) {
|
|
76
|
+
// Only warn about a graceful null return when the clusterer
|
|
77
|
+
// didn't already log a throw. Without this, a hard failure
|
|
78
|
+
// (claude CLI missing, schema mismatch, etc.) showed up as
|
|
79
|
+
// BOTH `clusterer threw` AND `clusterer returned null`, which
|
|
80
|
+
// misleads readers into thinking there were two distinct
|
|
81
|
+
// failure modes (#1072 review).
|
|
82
|
+
if (!clustererThrew) log.warn("memory", "topic-migrate: clusterer returned null");
|
|
83
|
+
await rm(stagingPath, { recursive: true, force: true });
|
|
84
|
+
return { ...emptyResult(stagingPath), inputCount: entries.length };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result: TopicMigrationResult = {
|
|
88
|
+
noop: false,
|
|
89
|
+
inputCount: entries.length,
|
|
90
|
+
topicCounts: { preference: 0, interest: 0, fact: 0, reference: 0 },
|
|
91
|
+
bulletsLost: countBulletsLost(entries, map),
|
|
92
|
+
stagingPath,
|
|
93
|
+
};
|
|
94
|
+
const written: WrittenTopic[] = [];
|
|
95
|
+
for (const type of MEMORY_TYPES) {
|
|
96
|
+
const usedSlugs = new Set<string>();
|
|
97
|
+
for (const topic of map[type]) {
|
|
98
|
+
const writtenSlug = pickUniqueSlug(topic.topic, usedSlugs);
|
|
99
|
+
try {
|
|
100
|
+
await writeTopicFileToStaging(stagingPath, type, topic, writtenSlug);
|
|
101
|
+
usedSlugs.add(writtenSlug);
|
|
102
|
+
written.push({ type, topic, writtenSlug });
|
|
103
|
+
result.topicCounts[type] += 1;
|
|
104
|
+
} catch (err) {
|
|
105
|
+
log.warn("memory", "topic-migrate: write failed", { type, topic: topic.topic, writtenSlug, error: errorMessage(err) });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
await writeStagingIndex(stagingPath, written);
|
|
110
|
+
log.info("memory", "topic-migrate: staging ready", {
|
|
111
|
+
stagingPath,
|
|
112
|
+
topicCounts: result.topicCounts,
|
|
113
|
+
bulletsLost: result.bulletsLost,
|
|
114
|
+
});
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Pick a slug that hasn't been used yet within this type. The
|
|
119
|
+
// clusterer may return two topics that would normalise to the same
|
|
120
|
+
// slug (e.g. "Music" and "music"); without this guard the second
|
|
121
|
+
// would silently overwrite the first.
|
|
122
|
+
//
|
|
123
|
+
// The base slug is trimmed if needed so `base + "-N"` still fits the
|
|
124
|
+
// `MAX_TOPIC_SLUG_LENGTH` cap that `isSafeTopicSlug` enforces. A
|
|
125
|
+
// 60-char slug colliding with a prior write would otherwise produce a
|
|
126
|
+
// 62-char filename that the writer rejects (and the reader would
|
|
127
|
+
// then refuse to load on the next session). After trimming we strip
|
|
128
|
+
// any trailing `-` so the suffix ("-N") is the only separator at the
|
|
129
|
+
// boundary.
|
|
130
|
+
function pickUniqueSlug(base: string, used: Set<string>): string {
|
|
131
|
+
if (!used.has(base)) return base;
|
|
132
|
+
let counter = 2;
|
|
133
|
+
while (true) {
|
|
134
|
+
const suffix = `-${counter}`;
|
|
135
|
+
const room = MAX_TOPIC_SLUG_LENGTH - suffix.length;
|
|
136
|
+
const trimmedBase = trimTrailingDash(base.slice(0, room));
|
|
137
|
+
const candidate = trimmedBase.length > 0 ? `${trimmedBase}${suffix}` : `topic${suffix}`;
|
|
138
|
+
if (!used.has(candidate)) return candidate;
|
|
139
|
+
counter += 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function trimTrailingDash(text: string): string {
|
|
144
|
+
let end = text.length;
|
|
145
|
+
while (end > 0 && text[end - 1] === "-") end -= 1;
|
|
146
|
+
return text.slice(0, end);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function emptyResult(stagingPath: string): TopicMigrationResult {
|
|
150
|
+
return {
|
|
151
|
+
noop: true,
|
|
152
|
+
inputCount: 0,
|
|
153
|
+
topicCounts: { preference: 0, interest: 0, fact: 0, reference: 0 },
|
|
154
|
+
bulletsLost: 0,
|
|
155
|
+
stagingPath,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function resetStaging(stagingPath: string): Promise<void> {
|
|
160
|
+
// Stale staging from a prior run is wiped — the user's review
|
|
161
|
+
// signal is the diff, so we always emit a fresh tree.
|
|
162
|
+
await rm(stagingPath, { recursive: true, force: true });
|
|
163
|
+
await mkdir(stagingPath, { recursive: true });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function writeTopicFileToStaging(stagingPath: string, type: MemoryType, topic: ClusterTopic, writtenSlug: string): Promise<void> {
|
|
167
|
+
const dir = path.join(stagingPath, type);
|
|
168
|
+
await mkdir(dir, { recursive: true });
|
|
169
|
+
const absPath = path.join(dir, `${writtenSlug}.md`);
|
|
170
|
+
const body = renderTopicBody(topic, writtenSlug);
|
|
171
|
+
const content = `---\ntype: ${type}\ntopic: ${writtenSlug}\n---\n\n${body}`;
|
|
172
|
+
await writeFileAtomic(absPath, content, { uniqueTmp: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderTopicBody(topic: ClusterTopic, writtenSlug: string): string {
|
|
176
|
+
const heading = humaniseTopic(writtenSlug);
|
|
177
|
+
const lines: string[] = [`# ${heading}`, ""];
|
|
178
|
+
if (topic.unsectionedBullets && topic.unsectionedBullets.length > 0) {
|
|
179
|
+
for (const bullet of topic.unsectionedBullets) {
|
|
180
|
+
lines.push(`- ${bullet}`);
|
|
181
|
+
}
|
|
182
|
+
lines.push("");
|
|
183
|
+
}
|
|
184
|
+
if (topic.sections) {
|
|
185
|
+
for (const section of topic.sections) {
|
|
186
|
+
lines.push(`## ${section.heading}`, "");
|
|
187
|
+
for (const bullet of section.bullets) {
|
|
188
|
+
lines.push(`- ${bullet}`);
|
|
189
|
+
}
|
|
190
|
+
lines.push("");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return lines.join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function humaniseTopic(slug: string): string {
|
|
197
|
+
// ASCII-friendly humaniser: split on `-`, capitalise each word.
|
|
198
|
+
// Non-ASCII slugs (which fall back to a hash) render as the slug
|
|
199
|
+
// itself; the user can rename later in the file explorer.
|
|
200
|
+
return slug
|
|
201
|
+
.split("-")
|
|
202
|
+
.map((part) => (part.length > 0 ? part[0].toUpperCase() + part.slice(1) : part))
|
|
203
|
+
.join(" ");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Build the staging-side `MEMORY.md` from the topics we successfully
|
|
207
|
+
// wrote to disk. Using the cluster map directly would link to files
|
|
208
|
+
// that failed to write (e.g. one bad slug would leave a broken index
|
|
209
|
+
// entry that swap promotes); this list is the disk-truth.
|
|
210
|
+
async function writeStagingIndex(stagingPath: string, written: WrittenTopic[]): Promise<void> {
|
|
211
|
+
const lines: string[] = ["# Memory Index", ""];
|
|
212
|
+
for (const type of MEMORY_TYPES) {
|
|
213
|
+
const inType = written.filter((entry) => entry.type === type);
|
|
214
|
+
if (inType.length === 0) continue;
|
|
215
|
+
lines.push(`## ${type}`, "");
|
|
216
|
+
const sorted = [...inType].sort((left, right) => left.writtenSlug.localeCompare(right.writtenSlug));
|
|
217
|
+
for (const entry of sorted) {
|
|
218
|
+
lines.push(formatStagingIndexLine(entry));
|
|
219
|
+
}
|
|
220
|
+
lines.push("");
|
|
221
|
+
}
|
|
222
|
+
if (written.length === 0) {
|
|
223
|
+
lines.push("_(no entries yet)_", "");
|
|
224
|
+
}
|
|
225
|
+
await writeFileAtomic(path.join(stagingPath, "MEMORY.md"), lines.join("\n"), { uniqueTmp: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function formatStagingIndexLine(entry: WrittenTopic): string {
|
|
229
|
+
const link = `${entry.type}/${entry.writtenSlug}.md`;
|
|
230
|
+
const headings = (entry.topic.sections ?? []).map((section) => section.heading);
|
|
231
|
+
if (headings.length === 0) return `- ${link}`;
|
|
232
|
+
return `- ${link} — ${headings.join(", ")}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function countBulletsLost(entries: readonly MemoryEntry[], map: ClusterMap): number {
|
|
236
|
+
let placed = 0;
|
|
237
|
+
for (const type of MEMORY_TYPES) {
|
|
238
|
+
for (const topic of map[type]) {
|
|
239
|
+
if (topic.unsectionedBullets) placed += topic.unsectionedBullets.length;
|
|
240
|
+
if (topic.sections) {
|
|
241
|
+
for (const section of topic.sections) {
|
|
242
|
+
placed += section.bullets.length;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return Math.max(0, entries.length - placed);
|
|
248
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// One-shot topic-based migration entry point used by server startup
|
|
2
|
+
// (#1070 PR-B). Mirrors `runMemoryMigrationOnce` from #1029 PR-B
|
|
3
|
+
// but targets the topic-format restructure instead of the legacy
|
|
4
|
+
// memory.md flow.
|
|
5
|
+
//
|
|
6
|
+
// Idempotent: returns immediately when there is nothing to do —
|
|
7
|
+
// the workspace is already topic-format, there are no atomic
|
|
8
|
+
// entries to migrate, or the legacy `memory.md` is still in
|
|
9
|
+
// flight. When staging is already present from a prior crash mid-
|
|
10
|
+
// swap, this runner retries the swap rather than burning another
|
|
11
|
+
// LLM cluster call. Failures are logged and swallowed so the
|
|
12
|
+
// server can continue serving traffic.
|
|
13
|
+
//
|
|
14
|
+
// Concurrency: cluster runs in the background while the agent
|
|
15
|
+
// continues serving requests. Atomic-format reads / writes stay in
|
|
16
|
+
// effect right up until the swap completes; the next request after
|
|
17
|
+
// the swap sees the new topic layout.
|
|
18
|
+
//
|
|
19
|
+
// CLEANUP 2026-07-01: this is one-shot migration code for the
|
|
20
|
+
// atomic → topic transition (#1070). After every active workspace
|
|
21
|
+
// has been swapped to the topic format, this file plus
|
|
22
|
+
// `topic-migrate.ts`, `topic-cluster.ts`, `topic-swap.ts`, the CLI
|
|
23
|
+
// helper at `scripts/memory-swap-topic-staging.ts`, the
|
|
24
|
+
// `yarn memory:swap` script, and the migration call in
|
|
25
|
+
// `server/index.ts` can be deleted in one sweep. Topic-format
|
|
26
|
+
// reading / writing (`topic-types.ts`, `topic-io.ts`,
|
|
27
|
+
// `topic-detect.ts` — minus the atomic-format branch) stays.
|
|
28
|
+
|
|
29
|
+
import { existsSync, statSync } from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
|
|
32
|
+
import { runClaudeCli, ClaudeCliNotFoundError, type Summarize } from "../journal/archivist-cli.js";
|
|
33
|
+
import { WORKSPACE_DIRS, WORKSPACE_FILES } from "../paths.js";
|
|
34
|
+
import { loadAllMemoryEntries } from "./io.js";
|
|
35
|
+
import { makeLlmMemoryClusterer } from "./topic-cluster.js";
|
|
36
|
+
import { clusterAtomicIntoStaging, topicStagingPath } from "./topic-migrate.js";
|
|
37
|
+
import { swapStagingIntoMemory } from "./topic-swap.js";
|
|
38
|
+
import { MEMORY_TYPES } from "./types.js";
|
|
39
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
40
|
+
import { log } from "../../system/logger/index.js";
|
|
41
|
+
|
|
42
|
+
export interface RunTopicMigrationDeps {
|
|
43
|
+
/** Override the summarize callback (useful for tests). Defaults to
|
|
44
|
+
* the production `runClaudeCli` which spawns the Claude CLI. */
|
|
45
|
+
summarize?: Summarize;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Strict variant of `hasTopicFormat` that only looks at `memory/`,
|
|
49
|
+
// not `memory.next/`. The shared `hasTopicFormat` is intentionally
|
|
50
|
+
// swap-tolerant so prompt routing doesn't fall back to atomic-format
|
|
51
|
+
// rules during the rename window — but the runner's idempotency
|
|
52
|
+
// guard needs the OPPOSITE: when only `memory.next/` exists (a
|
|
53
|
+
// swap-in-progress or a crash mid-swap), the runner must drop into
|
|
54
|
+
// the "existing staging detected" retry-swap branch below, not exit.
|
|
55
|
+
function memoryTreeIsTopicFormat(workspaceRoot: string): boolean {
|
|
56
|
+
const memoryRoot = path.join(workspaceRoot, WORKSPACE_DIRS.memoryDir);
|
|
57
|
+
for (const type of MEMORY_TYPES) {
|
|
58
|
+
try {
|
|
59
|
+
if (statSync(path.join(memoryRoot, type)).isDirectory()) return true;
|
|
60
|
+
} catch {
|
|
61
|
+
// ENOENT / EACCES → keep looking; only the actual presence of
|
|
62
|
+
// a `memory/<type>` dir signals migration completed.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function runTopicMigrationOnce(workspaceRoot: string, deps: RunTopicMigrationDeps = {}): Promise<void> {
|
|
69
|
+
if (memoryTreeIsTopicFormat(workspaceRoot)) {
|
|
70
|
+
log.debug("memory", "topic-run: workspace already uses topic format, skipping");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const stagingPath = topicStagingPath(workspaceRoot);
|
|
74
|
+
// If staging is left over from a prior run that crashed between
|
|
75
|
+
// cluster and swap, just retry the swap. Re-clustering would burn
|
|
76
|
+
// another LLM call and (because clusterAtomicIntoStaging wipes
|
|
77
|
+
// staging up front) discard the prior cluster result.
|
|
78
|
+
if (existsSync(stagingPath)) {
|
|
79
|
+
log.info("memory", "topic-run: existing staging detected, retrying swap", { stagingPath });
|
|
80
|
+
await runSwap(workspaceRoot);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Don't trip over an in-progress legacy `memory.md` migration from
|
|
84
|
+
// #1029 PR-B. We mirror the conditions under which
|
|
85
|
+
// `runMemoryMigrationOnce` would actually run — legacy file
|
|
86
|
+
// present, past the placeholder threshold, AND `.backup` absent.
|
|
87
|
+
// The `.backup` check is load-bearing: when both `memory.md` and
|
|
88
|
+
// `.backup` exist, the legacy runner refuses to re-process (the
|
|
89
|
+
// backup signals "already done; user re-introduced the file"),
|
|
90
|
+
// and without this clause the topic runner would defer
|
|
91
|
+
// indefinitely waiting for a migration that's never going to
|
|
92
|
+
// happen.
|
|
93
|
+
//
|
|
94
|
+
// One guarded `statSync` (no `existsSync` first): the legacy
|
|
95
|
+
// migration runs in parallel and can rename / delete `memory.md`
|
|
96
|
+
// between an `existsSync` check and a follow-up `statSync`,
|
|
97
|
+
// turning the race into an unhandled rejection because this whole
|
|
98
|
+
// function is invoked as a floating promise on startup. ENOENT
|
|
99
|
+
// means the legacy file isn't there (or just got renamed away),
|
|
100
|
+
// so there's nothing to defer for; any other error is swallowed
|
|
101
|
+
// and the runner proceeds — a permission glitch should never block
|
|
102
|
+
// the topic restructure.
|
|
103
|
+
const legacyPath = path.join(workspaceRoot, WORKSPACE_FILES.memory);
|
|
104
|
+
let legacyStat: ReturnType<typeof statSync> | null;
|
|
105
|
+
try {
|
|
106
|
+
legacyStat = statSync(legacyPath);
|
|
107
|
+
} catch {
|
|
108
|
+
legacyStat = null;
|
|
109
|
+
}
|
|
110
|
+
if (legacyStat && legacyStat.size >= 64) {
|
|
111
|
+
const backupPath = `${legacyPath}.backup`;
|
|
112
|
+
if (!existsSync(backupPath)) {
|
|
113
|
+
log.debug("memory", "topic-run: legacy memory.md still in flight, deferring", { legacyPath });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const entries = await loadAllMemoryEntries(workspaceRoot);
|
|
118
|
+
if (entries.length === 0) {
|
|
119
|
+
log.debug("memory", "topic-run: no atomic entries to migrate, skipping");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const summarize = deps.summarize ?? runClaudeCli;
|
|
123
|
+
const clusterer = makeLlmMemoryClusterer({ summarize });
|
|
124
|
+
log.info("memory", "topic-run: starting", { entryCount: entries.length });
|
|
125
|
+
try {
|
|
126
|
+
const result = await clusterAtomicIntoStaging(workspaceRoot, clusterer);
|
|
127
|
+
if (result.noop) {
|
|
128
|
+
// `clusterAtomicIntoStaging` already logged the failure cause
|
|
129
|
+
// (`clusterer threw` or `clusterer returned null`) and rm'd
|
|
130
|
+
// the staging dir. Logging "staged" here would tell the user
|
|
131
|
+
// to `diff` a directory that no longer exists (#1076 review).
|
|
132
|
+
log.warn("memory", "topic-run: cluster did not produce staging — see prior log entry");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
log.info("memory", "topic-run: staged", {
|
|
136
|
+
stagingPath: result.stagingPath,
|
|
137
|
+
topicCounts: result.topicCounts,
|
|
138
|
+
bulletsLost: result.bulletsLost,
|
|
139
|
+
});
|
|
140
|
+
await runSwap(workspaceRoot);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
// Defensive: `makeLlmMemoryClusterer` swallows summarize errors
|
|
143
|
+
// and returns null today, and `clusterAtomicIntoStaging` doesn't
|
|
144
|
+
// re-throw, so this branch is currently unreachable. Kept so a
|
|
145
|
+
// future change in the clusterer error contract surfaces a
|
|
146
|
+
// visible log instead of an unhandled rejection (the runner is
|
|
147
|
+
// invoked as a floating promise on startup).
|
|
148
|
+
if (err instanceof ClaudeCliNotFoundError) {
|
|
149
|
+
log.warn("memory", "topic-run: claude CLI not on PATH; topic restructure deferred");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
log.error("memory", "topic-run: cluster threw", { error: errorMessage(err) });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Swap staging into the live memory dir. The atomic format is
|
|
157
|
+
// parked under `memory/.atomic-backup/<ts>/` so misclassified
|
|
158
|
+
// migrations can be rolled back by hand without losing data.
|
|
159
|
+
// Failures leave staging in place; the next server start hits the
|
|
160
|
+
// "existing staging detected" branch above and retries.
|
|
161
|
+
async function runSwap(workspaceRoot: string): Promise<void> {
|
|
162
|
+
const result = await swapStagingIntoMemory(workspaceRoot);
|
|
163
|
+
if (result.swapped) {
|
|
164
|
+
log.info("memory", "topic-run: swap complete — workspace now uses topic format", {
|
|
165
|
+
backupPath: result.backupPath,
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
log.warn("memory", "topic-run: swap did not complete, leaving staging in place for retry", {
|
|
169
|
+
reason: result.reason ?? "unknown",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Swap `conversations/memory/` ↔ `conversations/memory.next/`
|
|
2
|
+
// after the user has approved the staging diff (#1070 PR-A).
|
|
3
|
+
//
|
|
4
|
+
// Library only — invoked from a CLI helper or the agent (a tool
|
|
5
|
+
// surface for the agent will land later). The swap is intentionally
|
|
6
|
+
// NOT auto-run: the whole point of staging is to give the user a
|
|
7
|
+
// chance to inspect.
|
|
8
|
+
//
|
|
9
|
+
// CLEANUP 2026-07-01: see `topic-run.ts` — this file is part of
|
|
10
|
+
// the one-shot atomic → topic migration chain and goes when the
|
|
11
|
+
// chain goes.
|
|
12
|
+
//
|
|
13
|
+
// Swap mechanics:
|
|
14
|
+
// memory/ → memory/.atomic-backup-<ts>/
|
|
15
|
+
// memory.next/ → memory/
|
|
16
|
+
//
|
|
17
|
+
// The backup name carries a timestamp so re-runs (after a follow-up
|
|
18
|
+
// migration on a richer workspace) don't clobber prior backups.
|
|
19
|
+
|
|
20
|
+
import { mkdir, rename, stat } from "node:fs/promises";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
|
|
23
|
+
import { log } from "../../system/logger/index.js";
|
|
24
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
25
|
+
import { WORKSPACE_DIRS } from "../paths.js";
|
|
26
|
+
import { topicStagingPath } from "./topic-migrate.js";
|
|
27
|
+
|
|
28
|
+
export interface SwapResult {
|
|
29
|
+
/** True when a swap actually happened. */
|
|
30
|
+
swapped: boolean;
|
|
31
|
+
/** Where the prior atomic layout was moved. Null if no prior data. */
|
|
32
|
+
backupPath: string | null;
|
|
33
|
+
/** Reason when `swapped: false`. */
|
|
34
|
+
reason?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function swapStagingIntoMemory(workspaceRoot: string): Promise<SwapResult> {
|
|
38
|
+
const stagingPath = topicStagingPath(workspaceRoot);
|
|
39
|
+
const memoryPath = path.join(workspaceRoot, WORKSPACE_DIRS.memoryDir);
|
|
40
|
+
const stagingExists = await pathExists(stagingPath);
|
|
41
|
+
if (!stagingExists) {
|
|
42
|
+
return { swapped: false, backupPath: null, reason: "staging dir not found" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let backupPath: string | null = null;
|
|
46
|
+
if (await pathExists(memoryPath)) {
|
|
47
|
+
backupPath = await pickBackupPath(memoryPath);
|
|
48
|
+
try {
|
|
49
|
+
await rename(memoryPath, backupPath);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
log.error("memory", "topic-swap: backup rename failed", { from: memoryPath, to: backupPath, error: errorMessage(err) });
|
|
52
|
+
return { swapped: false, backupPath: null, reason: "backup rename failed" };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await rename(stagingPath, memoryPath);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
log.error("memory", "topic-swap: staging rename failed", { from: stagingPath, to: memoryPath, error: errorMessage(err) });
|
|
60
|
+
// Try to put the backup back so the workspace isn't left empty.
|
|
61
|
+
let rollbackFailed = false;
|
|
62
|
+
if (backupPath) {
|
|
63
|
+
try {
|
|
64
|
+
await rename(backupPath, memoryPath);
|
|
65
|
+
} catch (rollbackErr) {
|
|
66
|
+
rollbackFailed = true;
|
|
67
|
+
log.error("memory", "topic-swap: rollback failed; manual intervention needed", {
|
|
68
|
+
backupPath,
|
|
69
|
+
memoryPath,
|
|
70
|
+
error: errorMessage(rollbackErr),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// When rollback ALSO fails, the prior data still lives at
|
|
75
|
+
// `backupPath`. Returning `backupPath: null` would tell callers
|
|
76
|
+
// "no recovery point exists" and they'd give up — surface the
|
|
77
|
+
// path so a human (or a retry loop) can move it back manually
|
|
78
|
+
// (#1072 review).
|
|
79
|
+
return { swapped: false, backupPath: rollbackFailed ? backupPath : null, reason: "staging rename failed" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Park the backup INSIDE the new memory dir so it travels with
|
|
83
|
+
// the workspace. A flat sibling backup (`memory.atomic-backup`)
|
|
84
|
+
// is also fine but clutters `conversations/`.
|
|
85
|
+
if (backupPath) {
|
|
86
|
+
const inside = path.join(memoryPath, ".atomic-backup");
|
|
87
|
+
await mkdir(inside, { recursive: true });
|
|
88
|
+
const finalLocation = path.join(inside, path.basename(backupPath));
|
|
89
|
+
try {
|
|
90
|
+
await rename(backupPath, finalLocation);
|
|
91
|
+
backupPath = finalLocation;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
log.warn("memory", "topic-swap: failed to park backup inside memory/, leaving at sibling location", {
|
|
94
|
+
backupPath,
|
|
95
|
+
error: errorMessage(err),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
log.info("memory", "topic-swap: done", { backupPath, memoryPath });
|
|
101
|
+
return { swapped: true, backupPath };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function pathExists(target: string): Promise<boolean> {
|
|
105
|
+
try {
|
|
106
|
+
await stat(target);
|
|
107
|
+
return true;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Builds an unused backup path. We use a coarse timestamp suffix so
|
|
114
|
+
// re-runs sort chronologically and don't collide.
|
|
115
|
+
async function pickBackupPath(memoryPath: string): Promise<string> {
|
|
116
|
+
const parent = path.dirname(memoryPath);
|
|
117
|
+
const stamp = formatTimestamp(new Date());
|
|
118
|
+
const base = `memory.atomic-backup-${stamp}`;
|
|
119
|
+
let candidate = path.join(parent, base);
|
|
120
|
+
let counter = 2;
|
|
121
|
+
while (await pathExists(candidate)) {
|
|
122
|
+
candidate = path.join(parent, `${base}-${counter}`);
|
|
123
|
+
counter += 1;
|
|
124
|
+
}
|
|
125
|
+
return candidate;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatTimestamp(date: Date): string {
|
|
129
|
+
const year = date.getUTCFullYear();
|
|
130
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
131
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
132
|
+
const hour = String(date.getUTCHours()).padStart(2, "0");
|
|
133
|
+
const minute = String(date.getUTCMinutes()).padStart(2, "0");
|
|
134
|
+
return `${year}${month}${day}-${hour}${minute}`;
|
|
135
|
+
}
|