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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// LLM-driven clusterer that turns a flat list of legacy atomic
|
|
2
|
+
// entries into a `<type, topic> → bullets[]` mapping (#1070 PR-A).
|
|
3
|
+
// Library only — `topic-migrate` is the one that calls it and writes
|
|
4
|
+
// to staging. The clusterer is pure async function shape so tests
|
|
5
|
+
// can substitute a deterministic stub without touching the LLM.
|
|
6
|
+
//
|
|
7
|
+
// CLEANUP 2026-07-01: see `topic-run.ts` — this file is part of
|
|
8
|
+
// the one-shot atomic → topic migration chain and goes when the
|
|
9
|
+
// chain goes.
|
|
10
|
+
|
|
11
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
12
|
+
import { log } from "../../system/logger/index.js";
|
|
13
|
+
import type { Summarize } from "../journal/archivist-cli.js";
|
|
14
|
+
import type { MemoryEntry, MemoryType } from "./types.js";
|
|
15
|
+
import { isMemoryType } from "./types.js";
|
|
16
|
+
import { isSafeTopicSlug, slugifyTopicName } from "./topic-types.js";
|
|
17
|
+
|
|
18
|
+
export interface ClusterTopic {
|
|
19
|
+
/** Topic slug (filename without `.md`). Must pass `isSafeTopicSlug`. */
|
|
20
|
+
topic: string;
|
|
21
|
+
/** Optional H2 sub-categorisation. When empty, bullets sit under the
|
|
22
|
+
* H1 directly. */
|
|
23
|
+
sections?: ClusterSection[];
|
|
24
|
+
/** Bullets that don't fit any section. They land directly under H1
|
|
25
|
+
* ahead of any sectioned content. */
|
|
26
|
+
unsectionedBullets?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ClusterSection {
|
|
30
|
+
/** H2 heading text (e.g. "Rock / Metal"). */
|
|
31
|
+
heading: string;
|
|
32
|
+
/** Bullet bodies. Order is preserved. */
|
|
33
|
+
bullets: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ClusterMap = Record<MemoryType, ClusterTopic[]>;
|
|
37
|
+
|
|
38
|
+
export type MemoryClusterer = (entries: readonly MemoryEntry[]) => Promise<ClusterMap | null>;
|
|
39
|
+
|
|
40
|
+
const CLUSTER_SYSTEM_PROMPT = `You group personal-memory bullets into topic files for long-term storage.
|
|
41
|
+
|
|
42
|
+
Input: a JSON array of bullets. Each has \`type\` (preference / interest / fact / reference), \`name\` (one-line label), and \`body\` (the raw bullet text).
|
|
43
|
+
|
|
44
|
+
Task: cluster the bullets by type and topic.
|
|
45
|
+
|
|
46
|
+
- Each \`type\` (preference / interest / fact / reference) gets its own section in the output.
|
|
47
|
+
- Within a type, group bullets that share a subject into one topic. Topic names are short, lowercase ASCII slugs (e.g. "music", "art", "ai-research", "travel", "bootcamp", "dev", "food", "tasks", "paths"). Pick names that are descriptive of the cluster.
|
|
48
|
+
- Inside a topic, you MAY further sub-categorise via H2 sections (e.g. \`music\` → "Rock / Metal", "Punk / Melodic"). Sections are optional — small topics with a handful of bullets can skip them and leave \`unsectionedBullets\`.
|
|
49
|
+
- Keep bullets verbatim. Do NOT edit, paraphrase, summarise, or merge bullets. The output must be losslessly reconstructable.
|
|
50
|
+
- Place each bullet into exactly ONE topic. No duplication across topics.
|
|
51
|
+
- Aim for ~5–15 topic files per type. Avoid singletons unless the bullet has no peers.
|
|
52
|
+
|
|
53
|
+
Output: ONE JSON object only, no prose, no markdown fences. Schema:
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
"preference": [
|
|
57
|
+
{ "topic": "<slug>", "sections": [ { "heading": "<H2 text>", "bullets": ["...", "..."] } ], "unsectionedBullets": ["..."] }
|
|
58
|
+
],
|
|
59
|
+
"interest": [ ... ],
|
|
60
|
+
"fact": [ ... ],
|
|
61
|
+
"reference": [ ... ]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
\`sections\` and \`unsectionedBullets\` are both optional per topic. Empty arrays are also OK. Every type key must be present even if its array is empty.`;
|
|
65
|
+
|
|
66
|
+
export interface MakeClustererDeps {
|
|
67
|
+
summarize: Summarize;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function makeLlmMemoryClusterer(deps: MakeClustererDeps): MemoryClusterer {
|
|
71
|
+
return async (entries) => {
|
|
72
|
+
if (entries.length === 0) {
|
|
73
|
+
return { preference: [], interest: [], fact: [], reference: [] };
|
|
74
|
+
}
|
|
75
|
+
const userPrompt = buildUserPrompt(entries);
|
|
76
|
+
let raw: string;
|
|
77
|
+
try {
|
|
78
|
+
raw = await deps.summarize(CLUSTER_SYSTEM_PROMPT, userPrompt);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
log.warn("memory", "cluster: summarize threw", { error: errorMessage(err) });
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return parseClusterMap(raw);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildUserPrompt(entries: readonly MemoryEntry[]): string {
|
|
88
|
+
const payload = entries.map((entry) => ({ type: entry.type, name: entry.name, body: entry.body }));
|
|
89
|
+
return `${entries.length} bullets to cluster:\n\n${JSON.stringify(payload, null, 2)}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Tolerant parser: strips fences, picks the first balanced object,
|
|
93
|
+
// validates schema, normalises slugs. Bullets with unknown types or
|
|
94
|
+
// missing topics are dropped (logged so the migration's count of
|
|
95
|
+
// unaccounted-for entries is visible).
|
|
96
|
+
export function parseClusterMap(raw: string): ClusterMap | null {
|
|
97
|
+
const stripped = stripFenceAndWhitespace(raw);
|
|
98
|
+
const objectText = extractFirstObject(stripped);
|
|
99
|
+
if (!objectText) return null;
|
|
100
|
+
let parsed: unknown;
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(objectText);
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (!isPlainObject(parsed)) return null;
|
|
107
|
+
const out: ClusterMap = { preference: [], interest: [], fact: [], reference: [] };
|
|
108
|
+
for (const type of ["preference", "interest", "fact", "reference"] as const) {
|
|
109
|
+
const list = (parsed as Record<string, unknown>)[type];
|
|
110
|
+
if (!Array.isArray(list)) continue;
|
|
111
|
+
for (const candidate of list) {
|
|
112
|
+
const topic = normaliseTopic(candidate);
|
|
113
|
+
if (topic) out[type].push(topic);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normaliseTopic(value: unknown): ClusterTopic | null {
|
|
120
|
+
if (!isPlainObject(value)) return null;
|
|
121
|
+
const obj = value as Record<string, unknown>;
|
|
122
|
+
const slug = resolveTopicSlug(obj.topic);
|
|
123
|
+
if (!slug) return null;
|
|
124
|
+
const sections = normaliseSections(obj.sections);
|
|
125
|
+
const unsectionedBullets = normaliseBulletList(obj.unsectionedBullets);
|
|
126
|
+
if (sections.length === 0 && unsectionedBullets.length === 0) return null;
|
|
127
|
+
return {
|
|
128
|
+
topic: slug,
|
|
129
|
+
...(sections.length > 0 ? { sections } : {}),
|
|
130
|
+
...(unsectionedBullets.length > 0 ? { unsectionedBullets } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveTopicSlug(value: unknown): string | null {
|
|
135
|
+
if (typeof value !== "string") return null;
|
|
136
|
+
if (isSafeTopicSlug(value)) return value;
|
|
137
|
+
const slugified = slugifyTopicName(value);
|
|
138
|
+
return slugified && isSafeTopicSlug(slugified) ? slugified : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normaliseSections(value: unknown): ClusterSection[] {
|
|
142
|
+
if (!Array.isArray(value)) return [];
|
|
143
|
+
const out: ClusterSection[] = [];
|
|
144
|
+
for (const candidate of value) {
|
|
145
|
+
const section = normaliseSection(candidate);
|
|
146
|
+
if (section) out.push(section);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normaliseBulletList(value: unknown): string[] {
|
|
152
|
+
if (!Array.isArray(value)) return [];
|
|
153
|
+
return value.filter((bullet): bullet is string => typeof bullet === "string" && bullet.trim().length > 0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normaliseSection(value: unknown): ClusterSection | null {
|
|
157
|
+
if (!isPlainObject(value)) return null;
|
|
158
|
+
const obj = value as Record<string, unknown>;
|
|
159
|
+
const heading = typeof obj.heading === "string" ? obj.heading.trim() : "";
|
|
160
|
+
if (heading.length === 0) return null;
|
|
161
|
+
if (!Array.isArray(obj.bullets)) return null;
|
|
162
|
+
const bullets = obj.bullets.filter((bullet): bullet is string => typeof bullet === "string" && bullet.trim().length > 0);
|
|
163
|
+
if (bullets.length === 0) return null;
|
|
164
|
+
return { heading, bullets };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function stripFenceAndWhitespace(raw: string): string {
|
|
168
|
+
let text = raw.trim();
|
|
169
|
+
if (text.startsWith("```")) {
|
|
170
|
+
const firstNl = text.indexOf("\n");
|
|
171
|
+
if (firstNl >= 0) text = text.slice(firstNl + 1);
|
|
172
|
+
if (text.endsWith("```")) text = text.slice(0, -3);
|
|
173
|
+
}
|
|
174
|
+
return text.trim();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function extractFirstObject(text: string): string | null {
|
|
178
|
+
const start = text.indexOf("{");
|
|
179
|
+
if (start < 0) return null;
|
|
180
|
+
let depth = 0;
|
|
181
|
+
let index = start;
|
|
182
|
+
while (index < text.length) {
|
|
183
|
+
const char = text[index];
|
|
184
|
+
if (char === '"') {
|
|
185
|
+
index = skipStringBody(text, index + 1);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (char === "{") depth += 1;
|
|
189
|
+
else if (char === "}") {
|
|
190
|
+
depth -= 1;
|
|
191
|
+
if (depth === 0) return text.slice(start, index + 1);
|
|
192
|
+
}
|
|
193
|
+
index += 1;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function skipStringBody(text: string, fromIndex: number): number {
|
|
199
|
+
let index = fromIndex;
|
|
200
|
+
while (index < text.length) {
|
|
201
|
+
const char = text[index];
|
|
202
|
+
if (char === "\\") {
|
|
203
|
+
index += 2;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (char === '"') return index + 1;
|
|
207
|
+
index += 1;
|
|
208
|
+
}
|
|
209
|
+
return text.length;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
213
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Re-exported so migrate.ts can validate the clusterer's output
|
|
217
|
+
// shape before writing.
|
|
218
|
+
export { isMemoryType };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Format detection for the memory storage layer (#1070 PR-B).
|
|
2
|
+
//
|
|
3
|
+
// Two layouts can live at `<workspaceRoot>/conversations/memory/`:
|
|
4
|
+
//
|
|
5
|
+
// atomic (#1029): flat `<type>_<slug>.md` files at the memory
|
|
6
|
+
// dir root, one fact per file.
|
|
7
|
+
// topic (#1070): `<type>/<topic>.md` under per-type subdirs,
|
|
8
|
+
// one topic per file.
|
|
9
|
+
//
|
|
10
|
+
// Detection signal: topic format is active iff a canonical type
|
|
11
|
+
// subdir (`preference/` / `interest/` / `fact/` / `reference/`)
|
|
12
|
+
// exists under live `conversations/memory/`. The post-swap state.
|
|
13
|
+
//
|
|
14
|
+
// Special case for the swap window: `swapStagingIntoMemory` first
|
|
15
|
+
// renames `memory/` out of the way and then renames `memory.next/`
|
|
16
|
+
// into place. Inside that gap `memory/` does not exist at all, so
|
|
17
|
+
// requests would otherwise fall back to atomic format and write
|
|
18
|
+
// `<type>_<slug>.md` into the newly promoted topic tree (later
|
|
19
|
+
// topic-mode reads silently ignore those). To bridge the gap, we
|
|
20
|
+
// also accept `memory.next/<type>/` — but ONLY when `memory/` is
|
|
21
|
+
// entirely absent (the actual swap window). When `memory/` still
|
|
22
|
+
// exists with atomic files (staging-in-progress, before the swap),
|
|
23
|
+
// `memory.next/<type>/` is just the clusterer filling staging and
|
|
24
|
+
// must NOT flip detection to topic mode — atomic data is still
|
|
25
|
+
// authoritative and the prompt has to keep reading it.
|
|
26
|
+
// (#1076 / #1087 follow-up — review on prompt-routing regression.)
|
|
27
|
+
//
|
|
28
|
+
// The check is cheap (one stat per type plus a stat on `memory/`)
|
|
29
|
+
// and reflects on-disk truth, so a manual swap immediately changes
|
|
30
|
+
// behavior on the next request — no module-level cache.
|
|
31
|
+
|
|
32
|
+
import { statSync } from "node:fs";
|
|
33
|
+
import path from "node:path";
|
|
34
|
+
|
|
35
|
+
import { WORKSPACE_DIRS } from "../paths.js";
|
|
36
|
+
import { MEMORY_TYPES } from "./types.js";
|
|
37
|
+
|
|
38
|
+
function isDirectorySafe(absPath: string): boolean {
|
|
39
|
+
try {
|
|
40
|
+
return statSync(absPath).isDirectory();
|
|
41
|
+
} catch {
|
|
42
|
+
// ENOENT / EACCES → treat as missing.
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasAnyTypeSubdir(root: string): boolean {
|
|
48
|
+
for (const type of MEMORY_TYPES) {
|
|
49
|
+
if (isDirectorySafe(path.join(root, type))) return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function hasTopicFormat(workspaceRoot: string): boolean {
|
|
55
|
+
const memoryRoot = path.join(workspaceRoot, WORKSPACE_DIRS.memoryDir);
|
|
56
|
+
// Live tree wins: any `memory/<type>/` → post-swap topic mode.
|
|
57
|
+
if (hasAnyTypeSubdir(memoryRoot)) return true;
|
|
58
|
+
// If live `memory/` still exists (with atomic files at the root,
|
|
59
|
+
// or empty), the staging dir is just being filled — atomic format
|
|
60
|
+
// is still authoritative until the swap renames memory/ out of
|
|
61
|
+
// the way. Don't let `memory.next/<type>/` flip detection here.
|
|
62
|
+
if (isDirectorySafe(memoryRoot)) return false;
|
|
63
|
+
// Live tree absent → could be the swap window OR a fresh
|
|
64
|
+
// workspace. Consult staging.
|
|
65
|
+
const stagingRoot = path.join(workspaceRoot, WORKSPACE_DIRS.memoryStaging);
|
|
66
|
+
return hasAnyTypeSubdir(stagingRoot);
|
|
67
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Auto-regenerate `conversations/memory/MEMORY.md` whenever a topic
|
|
2
|
+
// file is written via an app route (#1032).
|
|
3
|
+
//
|
|
4
|
+
// Wired into `publishFileChange` — the single chokepoint every
|
|
5
|
+
// route hits after a successful write. When the changed path looks
|
|
6
|
+
// like a topic file, this kicks off `regenerateTopicIndex` async
|
|
7
|
+
// so the index stays in sync with the bullets the user just edited
|
|
8
|
+
// in the file explorer.
|
|
9
|
+
//
|
|
10
|
+
// Limitation: the agent's raw `Write` tool bypasses app routes, so
|
|
11
|
+
// agent-driven edits do NOT trigger this hook. The prompt context
|
|
12
|
+
// re-reads disk every turn (`loadAllTopicFilesSync`), so the agent
|
|
13
|
+
// itself stays fresh; only the on-disk `MEMORY.md` lags between
|
|
14
|
+
// agent writes. Acceptable today — revisit if a periodic refresh
|
|
15
|
+
// proves needed.
|
|
16
|
+
|
|
17
|
+
import { workspacePath } from "../workspace.js";
|
|
18
|
+
import { regenerateTopicIndex } from "./topic-io.js";
|
|
19
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
20
|
+
import { log } from "../../system/logger/index.js";
|
|
21
|
+
import { MEMORY_TYPES } from "./types.js";
|
|
22
|
+
import { isSafeTopicSlug } from "./topic-types.js";
|
|
23
|
+
|
|
24
|
+
const TOPIC_PATH_PREFIXES: readonly string[] = MEMORY_TYPES.map((type) => `conversations/memory/${type}/`);
|
|
25
|
+
|
|
26
|
+
// Returns true iff the relative path points at a topic file —
|
|
27
|
+
// `conversations/memory/<type>/<slug>.md` where `<slug>` passes
|
|
28
|
+
// `isSafeTopicSlug` (the same shape gate the writer uses). Files at
|
|
29
|
+
// the memory root itself (e.g. `conversations/memory/MEMORY.md`,
|
|
30
|
+
// the index this helper writes) are excluded so the regen doesn't
|
|
31
|
+
// recurse on its own writes.
|
|
32
|
+
//
|
|
33
|
+
// Path is expected POSIX-normalised (the caller in `file-change.ts`
|
|
34
|
+
// already does this) and workspace-relative. We reject:
|
|
35
|
+
// - absolute paths (`/foo` / `C:\\foo`)
|
|
36
|
+
// - backslash-using paths (raw Windows separators)
|
|
37
|
+
// - non-`.md` files
|
|
38
|
+
// - dotdir subtrees (`.atomic-backup/`, `.archived/`)
|
|
39
|
+
// - nested paths under a type subdir (the layout is flat)
|
|
40
|
+
// - basenames that fail `isSafeTopicSlug` — same contract the
|
|
41
|
+
// writer enforces, so a malformed file dropped manually under a
|
|
42
|
+
// type subdir won't trigger a regen for an entry the loader
|
|
43
|
+
// would later skip anyway.
|
|
44
|
+
export function isTopicFilePath(relativePath: string): boolean {
|
|
45
|
+
if (typeof relativePath !== "string" || relativePath.length === 0) return false;
|
|
46
|
+
if (relativePath.startsWith("/")) return false;
|
|
47
|
+
if (relativePath.includes("\\")) return false;
|
|
48
|
+
if (!relativePath.endsWith(".md")) return false;
|
|
49
|
+
if (relativePath.includes("/.atomic-backup/")) return false;
|
|
50
|
+
if (relativePath.includes("/.archived/")) return false;
|
|
51
|
+
for (const prefix of TOPIC_PATH_PREFIXES) {
|
|
52
|
+
if (!relativePath.startsWith(prefix)) continue;
|
|
53
|
+
const tail = relativePath.slice(prefix.length);
|
|
54
|
+
if (tail.includes("/")) return false;
|
|
55
|
+
const slug = tail.slice(0, -".md".length);
|
|
56
|
+
if (!isSafeTopicSlug(slug)) return false;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Workspace-relative path to the index file the hook regenerates. */
|
|
63
|
+
export const TOPIC_INDEX_RELATIVE_PATH = "conversations/memory/MEMORY.md";
|
|
64
|
+
|
|
65
|
+
// Per-workspace FIFO chain. `regenerateTopicIndex` reads the topic
|
|
66
|
+
// directory tree then writes `MEMORY.md` atomically — concurrent
|
|
67
|
+
// invocations would race on the readdir scan, and a fast burst of
|
|
68
|
+
// edits via the file explorer (saving 5 topic files in 200ms) could
|
|
69
|
+
// produce overlapping rebuilds where the second writer overwrites
|
|
70
|
+
// the first with a stale snapshot. The chain ensures one rebuild at
|
|
71
|
+
// a time per workspace; later calls coalesce naturally because the
|
|
72
|
+
// last finishing rebuild already saw every prior write to disk
|
|
73
|
+
// (#1109 review).
|
|
74
|
+
//
|
|
75
|
+
// Keyed by absolute workspace path so a future multi-workspace
|
|
76
|
+
// deployment doesn't cross-serialize unrelated work. In practice
|
|
77
|
+
// `workspacePath` is a process-level constant today, so the map has
|
|
78
|
+
// a single entry — the keying is forward-compatible insurance.
|
|
79
|
+
const regenChains = new Map<string, Promise<unknown>>();
|
|
80
|
+
|
|
81
|
+
function chainRegen(workspaceRoot: string): Promise<unknown> {
|
|
82
|
+
const previous = regenChains.get(workspaceRoot) ?? Promise.resolve();
|
|
83
|
+
const next = previous.then(
|
|
84
|
+
() => regenerateTopicIndex(workspaceRoot),
|
|
85
|
+
() => regenerateTopicIndex(workspaceRoot),
|
|
86
|
+
);
|
|
87
|
+
// Tail update is unconditional (then with both onFulfilled and
|
|
88
|
+
// onRejected) so a single failed rebuild doesn't permanently
|
|
89
|
+
// poison ordering for the next call.
|
|
90
|
+
regenChains.set(
|
|
91
|
+
workspaceRoot,
|
|
92
|
+
next.then(
|
|
93
|
+
() => undefined,
|
|
94
|
+
() => undefined,
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
return next;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Fire-and-forget index regeneration for a workspace-relative path.
|
|
101
|
+
// Returns `true` when a regen actually ran — callers (specifically
|
|
102
|
+
// `publishFileChange`) use this to decide whether to emit a follow-up
|
|
103
|
+
// change event for the index file itself, so an open `MEMORY.md` tab
|
|
104
|
+
// refreshes alongside the topic file the user just saved. Failures
|
|
105
|
+
// log and resolve `false`.
|
|
106
|
+
export async function maybeRegenerateTopicIndex(relativePath: string): Promise<boolean> {
|
|
107
|
+
if (!isTopicFilePath(relativePath)) return false;
|
|
108
|
+
try {
|
|
109
|
+
await chainRegen(workspacePath);
|
|
110
|
+
log.debug("memory", "topic-index-hook: regenerated", { trigger: relativePath });
|
|
111
|
+
return true;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
log.warn("memory", "topic-index-hook: regenerate failed", {
|
|
114
|
+
trigger: relativePath,
|
|
115
|
+
error: errorMessage(err),
|
|
116
|
+
});
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Test-only — drop the per-workspace regen chain so each test
|
|
122
|
+
* starts with a fresh queue. Without this, a chain rejection from
|
|
123
|
+
* one test could carry into the next (the .catch in `chainRegen`
|
|
124
|
+
* swallows it for the next caller, but the test runner sees the
|
|
125
|
+
* unhandled-rejection from the original promise). */
|
|
126
|
+
export function _resetTopicIndexRegenChainForTesting(): void {
|
|
127
|
+
regenChains.clear();
|
|
128
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Topic-based memory IO (#1070 PR-A).
|
|
2
|
+
//
|
|
3
|
+
// Reads and writes `<type>/<topic>.md` topic files. The directory
|
|
4
|
+
// layout is:
|
|
5
|
+
//
|
|
6
|
+
// conversations/memory/
|
|
7
|
+
// preference/dev.md
|
|
8
|
+
// interest/music.md
|
|
9
|
+
// ...
|
|
10
|
+
// MEMORY.md # auto-generated index (sibling of the type subdirs)
|
|
11
|
+
//
|
|
12
|
+
// The async loader is for migration / batch use; the sync loader is
|
|
13
|
+
// for the agent prompt builder, which runs in a sync code path.
|
|
14
|
+
// Both loaders share the same parse helper so a malformed file
|
|
15
|
+
// produces the same warning regardless of caller.
|
|
16
|
+
|
|
17
|
+
import { mkdir } from "node:fs/promises";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
import { parseFrontmatter, serializeWithFrontmatter } from "../../utils/markdown/frontmatter.js";
|
|
21
|
+
import { writeFileAtomic } from "../../utils/files/atomic.js";
|
|
22
|
+
import { readDirSafe, readDirSafeAsync, readTextSafe, readTextSafeSync } from "../../utils/files/safe.js";
|
|
23
|
+
import { log } from "../../system/logger/index.js";
|
|
24
|
+
import { WORKSPACE_DIRS, WORKSPACE_FILES } from "../paths.js";
|
|
25
|
+
import { isMemoryType, MEMORY_TYPES, type MemoryType } from "./types.js";
|
|
26
|
+
import { extractH2Sections, isSafeTopicSlug, type TopicMemoryFile } from "./topic-types.js";
|
|
27
|
+
|
|
28
|
+
export function topicMemoryRoot(workspaceRoot: string): string {
|
|
29
|
+
return path.join(workspaceRoot, WORKSPACE_DIRS.memoryDir);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function topicMemoryIndexPath(workspaceRoot: string): string {
|
|
33
|
+
return path.join(workspaceRoot, WORKSPACE_FILES.memoryIndex);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function topicFilePath(workspaceRoot: string, type: MemoryType, topic: string): string {
|
|
37
|
+
return path.join(topicMemoryRoot(workspaceRoot), type, `${topic}.md`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Both loaders walk types in `MEMORY_TYPES` order (a stable
|
|
41
|
+
// constant) and sort filenames within each type by name. This pins
|
|
42
|
+
// the order of entries in the agent's system prompt so a `readdir`
|
|
43
|
+
// reshuffle on a different filesystem or after a restart can't
|
|
44
|
+
// destabilise the prompt content (and trash prompt cache hit rates).
|
|
45
|
+
export async function loadAllTopicFiles(workspaceRoot: string): Promise<TopicMemoryFile[]> {
|
|
46
|
+
const root = topicMemoryRoot(workspaceRoot);
|
|
47
|
+
const collected: TopicMemoryFile[] = [];
|
|
48
|
+
for (const type of MEMORY_TYPES) {
|
|
49
|
+
const typeDir = path.join(root, type);
|
|
50
|
+
const dirents = await readDirSafeAsync(typeDir);
|
|
51
|
+
const filenames = candidateFilenamesSorted(dirents);
|
|
52
|
+
for (const name of filenames) {
|
|
53
|
+
const absPath = path.join(typeDir, name);
|
|
54
|
+
const raw = await readTextSafe(absPath);
|
|
55
|
+
const file = parseTopicFile(absPath, raw, type);
|
|
56
|
+
if (file) collected.push(file);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return collected;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function loadAllTopicFilesSync(workspaceRoot: string): TopicMemoryFile[] {
|
|
63
|
+
const root = topicMemoryRoot(workspaceRoot);
|
|
64
|
+
const collected: TopicMemoryFile[] = [];
|
|
65
|
+
for (const type of MEMORY_TYPES) {
|
|
66
|
+
const typeDir = path.join(root, type);
|
|
67
|
+
const dirents = readDirSafe(typeDir);
|
|
68
|
+
const filenames = candidateFilenamesSorted(dirents);
|
|
69
|
+
for (const name of filenames) {
|
|
70
|
+
const absPath = path.join(typeDir, name);
|
|
71
|
+
const raw = readTextSafeSync(absPath);
|
|
72
|
+
const file = parseTopicFile(absPath, raw, type);
|
|
73
|
+
if (file) collected.push(file);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return collected;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function candidateFilenamesSorted(dirents: readonly { name: string }[]): string[] {
|
|
80
|
+
const names: string[] = [];
|
|
81
|
+
for (const dirent of dirents) {
|
|
82
|
+
if (isCandidateFilename(dirent.name)) names.push(dirent.name);
|
|
83
|
+
}
|
|
84
|
+
return names.sort();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function writeTopicFile(workspaceRoot: string, file: TopicMemoryFile): Promise<string> {
|
|
88
|
+
if (!isSafeTopicSlug(file.topic)) {
|
|
89
|
+
throw new Error(`refusing to write topic file with unsafe topic slug: ${JSON.stringify(file.topic)}`);
|
|
90
|
+
}
|
|
91
|
+
const dir = path.join(topicMemoryRoot(workspaceRoot), file.type);
|
|
92
|
+
await mkdir(dir, { recursive: true });
|
|
93
|
+
const absPath = path.join(dir, `${file.topic}.md`);
|
|
94
|
+
const content = serializeWithFrontmatter({ type: file.type, topic: file.topic }, file.body);
|
|
95
|
+
await writeFileAtomic(absPath, content, { uniqueTmp: true });
|
|
96
|
+
return path.posix.join(WORKSPACE_DIRS.memoryDir, file.type, `${file.topic}.md`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Rebuild `MEMORY.md` from the live topic files. Sorted by type
|
|
100
|
+
// (preference / interest / fact / reference) then by topic name.
|
|
101
|
+
// Each file renders as `- <type>/<topic>.md — <H2 csv>` (or just
|
|
102
|
+
// the path when there are no H2 sections yet).
|
|
103
|
+
export async function regenerateTopicIndex(workspaceRoot: string): Promise<void> {
|
|
104
|
+
await mkdir(topicMemoryRoot(workspaceRoot), { recursive: true });
|
|
105
|
+
const files = await loadAllTopicFiles(workspaceRoot);
|
|
106
|
+
const sorted = [...files].sort(compareFiles);
|
|
107
|
+
const lines: string[] = ["# Memory Index", ""];
|
|
108
|
+
for (const type of MEMORY_TYPES) {
|
|
109
|
+
const inType = sorted.filter((file) => file.type === type);
|
|
110
|
+
if (inType.length === 0) continue;
|
|
111
|
+
lines.push(`## ${type}`);
|
|
112
|
+
lines.push("");
|
|
113
|
+
for (const file of inType) {
|
|
114
|
+
lines.push(formatIndexLine(file));
|
|
115
|
+
}
|
|
116
|
+
lines.push("");
|
|
117
|
+
}
|
|
118
|
+
if (sorted.length === 0) lines.push("_(no entries yet)_", "");
|
|
119
|
+
await writeFileAtomic(topicMemoryIndexPath(workspaceRoot), lines.join("\n"), { uniqueTmp: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function formatIndexLine(file: TopicMemoryFile): string {
|
|
123
|
+
const link = `${file.type}/${file.topic}.md`;
|
|
124
|
+
if (file.sections.length === 0) return `- ${link}`;
|
|
125
|
+
return `- ${link} — ${file.sections.join(", ")}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function compareFiles(left: TopicMemoryFile, right: TopicMemoryFile): number {
|
|
129
|
+
const typeDelta = MEMORY_TYPES.indexOf(left.type) - MEMORY_TYPES.indexOf(right.type);
|
|
130
|
+
if (typeDelta !== 0) return typeDelta;
|
|
131
|
+
return left.topic.localeCompare(right.topic);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isCandidateFilename(name: string): boolean {
|
|
135
|
+
if (!name.endsWith(".md")) return false;
|
|
136
|
+
if (name.startsWith(".")) return false;
|
|
137
|
+
if (name === "MEMORY.md") return false;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseTopicFile(absPath: string, raw: string | null, expectedType: MemoryType): TopicMemoryFile | null {
|
|
142
|
+
if (raw === null) {
|
|
143
|
+
log.warn("memory", "topic-io: failed to read file", { path: absPath });
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const parsed = parseFrontmatter(raw);
|
|
147
|
+
if (!parsed.hasHeader) {
|
|
148
|
+
log.warn("memory", "topic-io: missing frontmatter", { path: absPath });
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const { type, topic } = parsed.meta;
|
|
152
|
+
if (!isMemoryType(type)) {
|
|
153
|
+
log.warn("memory", "topic-io: unknown type", { path: absPath, type: String(type) });
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (type !== expectedType) {
|
|
157
|
+
log.warn("memory", "topic-io: type / directory mismatch", { path: absPath, type, expectedType });
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (typeof topic !== "string" || topic.trim().length === 0) {
|
|
161
|
+
log.warn("memory", "topic-io: missing topic", { path: absPath });
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const topicTrimmed = topic.trim();
|
|
165
|
+
if (!isSafeTopicSlug(topicTrimmed)) {
|
|
166
|
+
log.warn("memory", "topic-io: unsafe topic slug", { path: absPath, topic });
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
// Filename is the source of truth — the index links to it.
|
|
170
|
+
// A frontmatter `topic` that disagrees with the basename produces
|
|
171
|
+
// dangling index entries (`type/topic.md` doesn't exist on disk),
|
|
172
|
+
// which the swap promotes verbatim.
|
|
173
|
+
const fileTopic = path.basename(absPath, ".md");
|
|
174
|
+
if (topicTrimmed !== fileTopic) {
|
|
175
|
+
log.warn("memory", "topic-io: topic / filename mismatch", { path: absPath, topic: topicTrimmed, fileTopic });
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const sections = extractH2Sections(parsed.body);
|
|
179
|
+
return { type, topic: topicTrimmed, body: parsed.body, sections };
|
|
180
|
+
}
|