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
|
@@ -1,25 +1,13 @@
|
|
|
1
|
-
// Pure builder for summaries/_index.md. Takes in-memory listings of
|
|
2
|
-
// the journal's current topic / daily files and returns the full
|
|
3
|
-
// markdown for the index. All filesystem walking happens in the
|
|
4
|
-
// caller; this function is deterministic and easy to snapshot-test.
|
|
5
|
-
|
|
6
1
|
import { isoDateOnly } from "../../utils/date.js";
|
|
7
2
|
|
|
8
3
|
export interface IndexTopicEntry {
|
|
9
|
-
// Filesystem slug (matches topics/<slug>.md).
|
|
10
4
|
slug: string;
|
|
11
|
-
// Optional human-readable title extracted from the topic file's
|
|
12
|
-
// first H1 heading. Falls back to `slug` if absent so the index
|
|
13
|
-
// row always reads sensibly.
|
|
14
5
|
title?: string;
|
|
15
|
-
// ISO timestamp of the last write to the topic file. Rendered
|
|
16
|
-
// for "stale topic" visibility.
|
|
17
6
|
lastUpdatedIso?: string;
|
|
18
7
|
}
|
|
19
8
|
|
|
20
9
|
export interface IndexDailyEntry {
|
|
21
|
-
// YYYY-MM-DD
|
|
22
|
-
date: string;
|
|
10
|
+
date: string; // YYYY-MM-DD local
|
|
23
11
|
}
|
|
24
12
|
|
|
25
13
|
export interface IndexInputs {
|
|
@@ -27,8 +15,6 @@ export interface IndexInputs {
|
|
|
27
15
|
days: readonly IndexDailyEntry[];
|
|
28
16
|
archivedTopicCount: number;
|
|
29
17
|
builtAtIso: string;
|
|
30
|
-
// How many "Recent days" rows to list before collapsing the
|
|
31
|
-
// remainder. The full listing still lives under daily/ on disk.
|
|
32
18
|
maxRecentDays?: number;
|
|
33
19
|
}
|
|
34
20
|
|
|
@@ -56,8 +42,7 @@ export function renderTopicsSection(topics: readonly IndexTopicEntry[]): string[
|
|
|
56
42
|
lines.push("_No topics yet._");
|
|
57
43
|
return lines;
|
|
58
44
|
}
|
|
59
|
-
// Newest-first
|
|
60
|
-
// last, ordered alphabetically among themselves for stability).
|
|
45
|
+
// Newest-first; topics with no timestamp sort last, alphabetical among themselves for stability.
|
|
61
46
|
const sorted = [...topics].sort(compareTopicsNewestFirst);
|
|
62
47
|
for (const topicEntry of sorted) {
|
|
63
48
|
lines.push(renderTopicRow(topicEntry));
|
|
@@ -71,7 +56,6 @@ export function renderRecentDaysSection(days: readonly IndexDailyEntry[], maxRec
|
|
|
71
56
|
lines.push("_No daily entries yet._");
|
|
72
57
|
return lines;
|
|
73
58
|
}
|
|
74
|
-
// Newest-first by date string (YYYY-MM-DD sorts lexically).
|
|
75
59
|
const sorted = [...days].sort((leftDay, rightDay) => {
|
|
76
60
|
if (leftDay.date < rightDay.date) return 1;
|
|
77
61
|
if (leftDay.date > rightDay.date) return -1;
|
|
@@ -100,8 +84,7 @@ export function renderArchiveSection(archivedTopicCount: number): string[] {
|
|
|
100
84
|
return lines;
|
|
101
85
|
}
|
|
102
86
|
|
|
103
|
-
//
|
|
104
|
-
// timestamps get -Infinity so they sort to the bottom (oldest).
|
|
87
|
+
// Invalid/missing ISO → -Infinity so they sort to the bottom.
|
|
105
88
|
function topicSortKey(entry: IndexTopicEntry): number {
|
|
106
89
|
if (!entry.lastUpdatedIso) return -Infinity;
|
|
107
90
|
const timestampMs = Date.parse(entry.lastUpdatedIso);
|
|
@@ -117,14 +100,10 @@ function compareBySlug(leftEntry: IndexTopicEntry, rightEntry: IndexTopicEntry):
|
|
|
117
100
|
function compareTopicsNewestFirst(leftEntry: IndexTopicEntry, rightEntry: IndexTopicEntry): number {
|
|
118
101
|
const leftKey = topicSortKey(leftEntry);
|
|
119
102
|
const rightKey = topicSortKey(rightEntry);
|
|
120
|
-
// Both valid timestamps → compare numerically.
|
|
121
|
-
// One or both invalid (-Infinity) → valid wins; if both invalid,
|
|
122
|
-
// fall through to the slug tie-breaker.
|
|
123
103
|
const leftIsValid = Number.isFinite(leftKey);
|
|
124
104
|
const rightIsValid = Number.isFinite(rightKey);
|
|
125
105
|
if (leftIsValid && rightIsValid && rightKey !== leftKey) return rightKey - leftKey;
|
|
126
106
|
if (leftIsValid !== rightIsValid) return leftIsValid ? -1 : 1;
|
|
127
|
-
// Tie-break on slug for determinism.
|
|
128
107
|
return compareBySlug(leftEntry, rightEntry);
|
|
129
108
|
}
|
|
130
109
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// #876 top-bar "today's journal" shortcut falls back to the most recent existing day when today's summary is missing.
|
|
2
|
+
// Backtracks YYYY → MM → DD.md so an empty 2026/05/ still finds 2026/04. Bounded I/O: O(years × months) worst case.
|
|
3
|
+
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import fsp from "node:fs/promises";
|
|
6
|
+
import { isEnoent } from "../../utils/files/safe.js";
|
|
7
|
+
import { summariesRoot, DAILY_DIR } from "./paths.js";
|
|
8
|
+
|
|
9
|
+
export interface LatestDailyResult {
|
|
10
|
+
/** Workspace-relative path, e.g. "conversations/summaries/daily/2026/04/26.md" */
|
|
11
|
+
path: string;
|
|
12
|
+
/** ISO-ish date "YYYY-MM-DD" matching the file's location. */
|
|
13
|
+
isoDate: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const YEAR_RE = /^\d{4}$/;
|
|
17
|
+
const MONTH_RE = /^\d{2}$/;
|
|
18
|
+
const DAY_FILE_RE = /^(\d{2})\.md$/;
|
|
19
|
+
|
|
20
|
+
async function listSorted(dir: string, pattern: RegExp): Promise<string[]> {
|
|
21
|
+
let entries: string[];
|
|
22
|
+
try {
|
|
23
|
+
entries = await fsp.readdir(dir);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (isEnoent(err)) return [];
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
return entries
|
|
29
|
+
.filter((name) => pattern.test(name))
|
|
30
|
+
.sort()
|
|
31
|
+
.reverse();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function findLatestDaily(workspaceRoot: string): Promise<LatestDailyResult | null> {
|
|
35
|
+
const dailyRoot = path.join(summariesRoot(workspaceRoot), DAILY_DIR);
|
|
36
|
+
const years = await listSorted(dailyRoot, YEAR_RE);
|
|
37
|
+
for (const year of years) {
|
|
38
|
+
const months = await listSorted(path.join(dailyRoot, year), MONTH_RE);
|
|
39
|
+
for (const month of months) {
|
|
40
|
+
const days = await listSorted(path.join(dailyRoot, year, month), DAY_FILE_RE);
|
|
41
|
+
if (days.length === 0) continue;
|
|
42
|
+
const [dayFile] = days;
|
|
43
|
+
const dayMatch = DAY_FILE_RE.exec(dayFile);
|
|
44
|
+
if (!dayMatch) continue;
|
|
45
|
+
const [, day] = dayMatch;
|
|
46
|
+
const relPath = path.posix.join("conversations", "summaries", DAILY_DIR, year, month, dayFile);
|
|
47
|
+
return { path: relPath, isoDate: `${year}-${month}-${day}` };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// daily pass so new facts are picked up even if the agent didn't
|
|
4
|
-
// write them during the conversation.
|
|
5
|
-
//
|
|
6
|
-
// Only appends facts that aren't already in memory.md — the LLM
|
|
7
|
-
// receives the current memory content as context and is instructed
|
|
8
|
-
// to return ONLY new facts.
|
|
1
|
+
// Run at end of the journal daily pass so durable user facts are picked up even if the agent didn't memo them in-conversation.
|
|
2
|
+
// LLM receives existing memory.md as context and is instructed to return ONLY new facts (we still de-dupe defensively).
|
|
9
3
|
|
|
10
4
|
import { readFileSync, existsSync } from "fs";
|
|
11
5
|
import path from "path";
|
|
@@ -40,16 +34,10 @@ Rules:
|
|
|
40
34
|
|
|
41
35
|
export interface MemoryExtractionDeps {
|
|
42
36
|
workspaceRoot: string;
|
|
43
|
-
/** The excerpts from today's dirty sessions, concatenated. */
|
|
44
37
|
excerpts: string;
|
|
45
|
-
/** Spawns claude CLI and returns stdout. Injectable for tests. */
|
|
46
38
|
summarize: (systemPrompt: string, userPrompt: string) => Promise<string>;
|
|
47
39
|
}
|
|
48
40
|
|
|
49
|
-
/**
|
|
50
|
-
* Extract new user facts from chat excerpts and append to memory.md.
|
|
51
|
-
* Returns the number of facts appended (0 if none found or on error).
|
|
52
|
-
*/
|
|
53
41
|
export async function extractAndAppendMemory(deps: MemoryExtractionDeps): Promise<number> {
|
|
54
42
|
const memoryPath = path.join(deps.workspaceRoot, WORKSPACE_FILES.memory);
|
|
55
43
|
const existingMemory = existsSync(memoryPath) ? readFileSync(memoryPath, "utf-8") : "";
|
|
@@ -80,18 +68,16 @@ export async function extractAndAppendMemory(deps: MemoryExtractionDeps): Promis
|
|
|
80
68
|
return factsToAppend.length;
|
|
81
69
|
}
|
|
82
70
|
|
|
83
|
-
/** Build the user prompt with existing memory + new excerpts. */
|
|
84
71
|
export function buildUserPrompt(existingMemory: string, excerpts: string): string {
|
|
85
72
|
const parts: string[] = [];
|
|
86
73
|
if (existingMemory.trim()) {
|
|
87
|
-
parts.push(
|
|
74
|
+
parts.push(`## Already known (do NOT repeat these):\n\n${existingMemory}`);
|
|
88
75
|
}
|
|
89
|
-
parts.push(
|
|
76
|
+
parts.push(`## New chat excerpts:\n\n${excerpts}`);
|
|
90
77
|
parts.push("\nExtract any NEW user facts not already in the 'Already known' section above. If none, output: NONE");
|
|
91
78
|
return parts.join("\n\n");
|
|
92
79
|
}
|
|
93
80
|
|
|
94
|
-
/** Parse LLM output into a list of fact strings. */
|
|
95
81
|
export function parseExtractedFacts(raw: string): string[] {
|
|
96
82
|
const trimmed = raw.trim();
|
|
97
83
|
if (trimmed === "NONE" || trimmed === "") return [];
|
|
@@ -106,7 +92,6 @@ function normalizeFact(fact: string): string {
|
|
|
106
92
|
return fact.replace(/^- /, "").trim().toLowerCase();
|
|
107
93
|
}
|
|
108
94
|
|
|
109
|
-
/** Remove facts that already exist in the current memory content. */
|
|
110
95
|
export function filterNewFacts(existingMemory: string, facts: readonly string[]): string[] {
|
|
111
96
|
const seen = new Set(parseExtractedFacts(existingMemory).map(normalizeFact));
|
|
112
97
|
const out: string[] = [];
|
|
@@ -119,7 +104,6 @@ export function filterNewFacts(existingMemory: string, facts: readonly string[])
|
|
|
119
104
|
return out;
|
|
120
105
|
}
|
|
121
106
|
|
|
122
|
-
/** Append facts to existing memory content. */
|
|
123
107
|
export function appendFacts(existing: string, facts: string[]): string {
|
|
124
108
|
const trimmed = existing.trimEnd();
|
|
125
109
|
const factsBlock = facts.join("\n");
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
// Weekly-ish topic optimization pass: merge near-duplicates, move
|
|
2
|
-
// stale topics into archive/. Separate file from dailyPass so the
|
|
3
|
-
// two can evolve independently and so the optimizer stays opt-in
|
|
4
|
-
// from the top-level runner.
|
|
5
|
-
|
|
6
1
|
import { workspacePath as defaultWorkspacePath } from "../workspace.js";
|
|
7
2
|
import { writeTopicFile, readAllTopicFiles, archiveTopic } from "../../utils/files/journal-io.js";
|
|
8
3
|
import {
|
|
@@ -17,7 +12,6 @@ import { slugify } from "./paths.js";
|
|
|
17
12
|
import type { JournalState } from "./state.js";
|
|
18
13
|
import { log } from "../../system/logger/index.js";
|
|
19
14
|
|
|
20
|
-
// How many characters of each topic file we hand to the optimizer.
|
|
21
15
|
// Enough to judge duplication without blowing up the prompt.
|
|
22
16
|
const OPTIMIZER_HEAD_CHARS = 500;
|
|
23
17
|
|
|
@@ -33,10 +27,7 @@ export interface OptimizationPassResult {
|
|
|
33
27
|
skippedReason?: string;
|
|
34
28
|
}
|
|
35
29
|
|
|
36
|
-
//
|
|
37
|
-
// concrete list of slug-level operations. Empty merges (where every
|
|
38
|
-
// source resolves to the merge target itself) are dropped, and slugs
|
|
39
|
-
// are normalized via slugify so the I/O layer never has to.
|
|
30
|
+
// Drops empty merges (every source resolves to the target itself); normalizes via slugify so the I/O layer never has to.
|
|
40
31
|
export interface MergePlanItem {
|
|
41
32
|
intoSlug: string;
|
|
42
33
|
fromSlugs: string[];
|
|
@@ -60,8 +51,6 @@ export function planMerges(merges: readonly RawMerge[]): MergePlanItem[] {
|
|
|
60
51
|
return plans;
|
|
61
52
|
}
|
|
62
53
|
|
|
63
|
-
// Pure transform: returns the next JournalState with any slug in
|
|
64
|
-
// `removed` filtered out of knownTopics.
|
|
65
54
|
export function applyRemovedTopics(state: JournalState, removed: ReadonlySet<string>): JournalState {
|
|
66
55
|
return {
|
|
67
56
|
...state,
|
|
@@ -73,10 +62,7 @@ async function executeMergePlans(workspaceRoot: string, plans: MergePlanItem[],
|
|
|
73
62
|
for (const plan of plans) {
|
|
74
63
|
await writeTopicFile(plan.intoSlug, plan.newContent, workspaceRoot);
|
|
75
64
|
for (const src of plan.fromSlugs) {
|
|
76
|
-
//
|
|
77
|
-
// actually moved. If archiveTopic fails (missing file, IO
|
|
78
|
-
// error) we leave the source out of the removed set so the
|
|
79
|
-
// in-memory knownTopics state stays accurate.
|
|
65
|
+
// Skip on archive failure so in-memory knownTopics stays accurate (the source file didn't actually move).
|
|
80
66
|
if (!(await archiveTopic(src, workspaceRoot))) continue;
|
|
81
67
|
removed.add(src);
|
|
82
68
|
mergedSlugs.push(src);
|
|
@@ -107,9 +93,7 @@ export async function runOptimizationPass(
|
|
|
107
93
|
|
|
108
94
|
const topics = await loadTopicHeads(workspaceRoot);
|
|
109
95
|
if (topics.length < 2) {
|
|
110
|
-
//
|
|
111
|
-
// be meaningful, and archiving a single topic would leave an
|
|
112
|
-
// empty journal which feels wrong.
|
|
96
|
+
// Need at least 2 topics for a merge to be meaningful; archiving a sole topic would leave an empty journal.
|
|
113
97
|
result.skipped = true;
|
|
114
98
|
result.skippedReason = "fewer than 2 topics";
|
|
115
99
|
return { nextState: { ...state }, result };
|
|
@@ -138,8 +122,7 @@ export async function runOptimizationPass(
|
|
|
138
122
|
|
|
139
123
|
const removed = new Set<string>();
|
|
140
124
|
|
|
141
|
-
//
|
|
142
|
-
// removed by a merge).
|
|
125
|
+
// Merges first, then archives (which skip slugs already removed by a merge).
|
|
143
126
|
await executeMergePlans(workspaceRoot, planMerges(parsed.merges), removed, result.mergedSlugs);
|
|
144
127
|
await executeArchives(workspaceRoot, parsed.archives, removed, result.archivedSlugs);
|
|
145
128
|
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
// Pure path / slug helpers for the workspace journal. Nothing here
|
|
2
|
-
// touches the filesystem — every function is a straightforward
|
|
3
|
-
// string transformation so it can be exhaustively unit-tested.
|
|
4
|
-
|
|
5
1
|
import path from "node:path";
|
|
6
2
|
import { WORKSPACE_DIRS } from "../paths.js";
|
|
7
3
|
import { isValidIsoDate } from "../../utils/date.js";
|
|
8
4
|
import { slugify as slugifyCanonical } from "../../utils/slug.js";
|
|
9
5
|
|
|
10
|
-
// Directory layout under workspace/conversations/summaries/ is an
|
|
11
|
-
// implementation detail of the journal module; keep it centralised
|
|
12
|
-
// here so tests and callers all agree on the structure.
|
|
13
6
|
export const SUMMARIES_DIR = WORKSPACE_DIRS.summaries;
|
|
14
7
|
export const STATE_FILE = "_state.json";
|
|
15
8
|
export const INDEX_FILE = "_index.md";
|
|
@@ -17,16 +10,12 @@ export const DAILY_DIR = "daily";
|
|
|
17
10
|
export const TOPICS_DIR = "topics";
|
|
18
11
|
export const ARCHIVE_DIR = "archive";
|
|
19
12
|
|
|
20
|
-
// Absolute path to the summaries root inside a workspace.
|
|
21
13
|
export function summariesRoot(workspaceRoot: string): string {
|
|
22
14
|
return path.join(workspaceRoot, SUMMARIES_DIR);
|
|
23
15
|
}
|
|
24
16
|
|
|
25
|
-
// summaries/daily/YYYY/MM/DD.md for a given ISO-ish date ("YYYY-MM-DD").
|
|
26
|
-
// Throws if `isoDate` is not exactly YYYY-MM-DD — catches typos at
|
|
27
|
-
// the boundary instead of producing "undefined/undefined.md" paths
|
|
28
|
-
// downstream.
|
|
29
17
|
export function dailyPathFor(workspaceRoot: string, isoDate: string): string {
|
|
18
|
+
// Throw at the boundary so a typo doesn't produce "undefined/undefined.md".
|
|
30
19
|
if (!isValidIsoDate(isoDate)) {
|
|
31
20
|
throw new Error(`[journal] dailyPathFor: expected YYYY-MM-DD, got "${isoDate}"`);
|
|
32
21
|
}
|
|
@@ -34,27 +23,19 @@ export function dailyPathFor(workspaceRoot: string, isoDate: string): string {
|
|
|
34
23
|
return path.join(summariesRoot(workspaceRoot), DAILY_DIR, year, month, `${day}.md`);
|
|
35
24
|
}
|
|
36
25
|
|
|
37
|
-
// summaries/topics/<slug>.md
|
|
38
26
|
export function topicPathFor(workspaceRoot: string, slug: string): string {
|
|
39
27
|
return path.join(summariesRoot(workspaceRoot), TOPICS_DIR, `${slug}.md`);
|
|
40
28
|
}
|
|
41
29
|
|
|
42
|
-
// summaries/archive/topics/<slug>.md — where the optimizer moves
|
|
43
|
-
// merged or stale topic files.
|
|
44
30
|
export function archivedTopicPathFor(workspaceRoot: string, slug: string): string {
|
|
45
31
|
return path.join(summariesRoot(workspaceRoot), ARCHIVE_DIR, TOPICS_DIR, `${slug}.md`);
|
|
46
32
|
}
|
|
47
33
|
|
|
48
|
-
// Re-export for backwards compatibility — callers that import
|
|
49
|
-
// toIsoDate from journal/paths keep working.
|
|
50
34
|
export { toLocalIsoDate as toIsoDate } from "../../utils/date.js";
|
|
51
35
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// stopped using its own ASCII-only impl: pure-non-ASCII topic names
|
|
56
|
-
// (e.g. "プロジェクトA" / "プロジェクトB") all collapsed to "topic"
|
|
57
|
-
// and silently overwrote each other's summary files.
|
|
36
|
+
// Falls through to slugifyCanonical with a journal-specific "topic" fallback.
|
|
37
|
+
// #732 retired the journal's own ASCII-only impl that collapsed every
|
|
38
|
+
// non-ASCII topic name onto a single overwriting "topic".
|
|
58
39
|
export function slugify(raw: string): string {
|
|
59
40
|
return slugifyCanonical(raw, "topic");
|
|
60
41
|
}
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
// Journal state file schema + persistence. The state file tracks
|
|
2
|
-
// what the archivist has already done so we only re-process new or
|
|
3
|
-
// changed sessions on each run.
|
|
4
|
-
//
|
|
5
|
-
// The pure bits (default creation, schema validation, interval
|
|
6
|
-
// arithmetic) live at the top of the file so tests can exercise
|
|
7
|
-
// them without touching disk. Filesystem helpers at the bottom wrap
|
|
8
|
-
// those pure functions with atomic read/write.
|
|
9
|
-
|
|
10
1
|
import {
|
|
11
2
|
readJournalState as readJournalStateRaw,
|
|
12
3
|
writeJournalState as writeJournalStateRaw,
|
|
@@ -16,16 +7,12 @@ import { ONE_HOUR_MS, ONE_DAY_MS } from "../../utils/time.js";
|
|
|
16
7
|
import { log } from "../../system/logger/index.js";
|
|
17
8
|
import { isRecord } from "../../utils/types.js";
|
|
18
9
|
|
|
19
|
-
// Bump
|
|
20
|
-
//
|
|
21
|
-
// fresh default (ingest everything from scratch) — cheap because it
|
|
22
|
-
// only costs one extra archivist pass.
|
|
10
|
+
// Bump on backwards-incompatible schema changes — older state files
|
|
11
|
+
// are discarded and rebuilt (one extra archivist pass).
|
|
23
12
|
export const JOURNAL_STATE_VERSION = 1;
|
|
24
13
|
|
|
25
14
|
export interface ProcessedSessionRecord {
|
|
26
|
-
// mtime
|
|
27
|
-
// last ingested it. If mtime advances on the next run, the session
|
|
28
|
-
// has appended events and needs re-ingest.
|
|
15
|
+
// mtime at last ingest; advance triggers re-ingest of appended events.
|
|
29
16
|
lastMtimeMs: number;
|
|
30
17
|
}
|
|
31
18
|
|
|
@@ -42,8 +29,6 @@ export interface JournalState {
|
|
|
42
29
|
export const DEFAULT_DAILY_INTERVAL_HOURS = 1;
|
|
43
30
|
export const DEFAULT_OPTIMIZATION_INTERVAL_DAYS = 7;
|
|
44
31
|
|
|
45
|
-
// --- Pure helpers (unit-testable without disk) ---------------------
|
|
46
|
-
|
|
47
32
|
export function defaultState(): JournalState {
|
|
48
33
|
return {
|
|
49
34
|
version: JOURNAL_STATE_VERSION,
|
|
@@ -56,17 +41,13 @@ export function defaultState(): JournalState {
|
|
|
56
41
|
};
|
|
57
42
|
}
|
|
58
43
|
|
|
59
|
-
//
|
|
60
|
-
// fields and fills defaults — users can hand-edit the file to change
|
|
61
|
-
// intervals and we want to be forgiving.
|
|
44
|
+
// Forgiving toward partial / hand-edited input — fill defaults instead of throwing.
|
|
62
45
|
export function parseState(raw: unknown): JournalState {
|
|
63
46
|
if (!isRecord(raw)) return defaultState();
|
|
64
47
|
const obj = raw as Record<string, unknown>;
|
|
65
48
|
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// forced re-ingest from "first run after install" / a deleted
|
|
69
|
-
// state file. (#799 PR1)
|
|
49
|
+
// Log the version-mismatch reset (#799 PR1) so postmortems can tell
|
|
50
|
+
// it apart from a missing-file first run.
|
|
70
51
|
if (obj.version !== JOURNAL_STATE_VERSION) {
|
|
71
52
|
log.info("journal", "state schema version mismatch — resetting", {
|
|
72
53
|
from: obj.version,
|
|
@@ -101,8 +82,6 @@ function parseProcessedSessions(raw: unknown): Record<string, ProcessedSessionRe
|
|
|
101
82
|
return out;
|
|
102
83
|
}
|
|
103
84
|
|
|
104
|
-
// Has the configured daily interval elapsed since the last run? A
|
|
105
|
-
// null lastDailyRunAt means "never run" → always due.
|
|
106
85
|
export function isDailyDue(state: JournalState, nowMs: number): boolean {
|
|
107
86
|
if (state.lastDailyRunAt === null) return true;
|
|
108
87
|
const last = Date.parse(state.lastDailyRunAt);
|
|
@@ -119,8 +98,6 @@ export function isOptimizationDue(state: JournalState, nowMs: number): boolean {
|
|
|
119
98
|
return nowMs - last >= intervalMs;
|
|
120
99
|
}
|
|
121
100
|
|
|
122
|
-
// --- Filesystem helpers (delegated to journal-io) --------------------
|
|
123
|
-
|
|
124
101
|
export async function readState(workspaceRoot: string): Promise<JournalState> {
|
|
125
102
|
const raw = await readJournalStateRaw<unknown>(null, workspaceRoot);
|
|
126
103
|
return parseState(raw);
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Memory storage IO (#1029 PR-A).
|
|
2
|
+
//
|
|
3
|
+
// Read all entries, write a single entry, regenerate the index file.
|
|
4
|
+
// Reader is forgiving: a corrupt frontmatter on one entry logs and
|
|
5
|
+
// is skipped, the rest still load. Writer is atomic.
|
|
6
|
+
//
|
|
7
|
+
// `MEMORY.md` is rebuilt from the live frontmatters whenever
|
|
8
|
+
// `regenerateIndex` is called — so every write that changes name /
|
|
9
|
+
// description / type should be followed by a regenerate. The legacy
|
|
10
|
+
// `memory.md` is intentionally NOT touched here; migration handles it.
|
|
11
|
+
|
|
12
|
+
import { mkdir } from "node:fs/promises";
|
|
13
|
+
import type { Stats } from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
import { parseFrontmatter, serializeWithFrontmatter } from "../../utils/markdown/frontmatter.js";
|
|
17
|
+
import { writeFileAtomic } from "../../utils/files/atomic.js";
|
|
18
|
+
import { WORKSPACE_DIRS, WORKSPACE_FILES } from "../paths.js";
|
|
19
|
+
import { readDirSafe, readDirSafeAsync, readTextSafe, readTextSafeSync, statSafe, statSafeAsync } from "../../utils/files/safe.js";
|
|
20
|
+
import { log } from "../../system/logger/index.js";
|
|
21
|
+
import { isMemoryType, type MemoryEntry, type MemoryType } from "./types.js";
|
|
22
|
+
|
|
23
|
+
// On-disk directory and index file for typed memory entries.
|
|
24
|
+
// Callers pass the workspace root; this returns the absolute paths.
|
|
25
|
+
export function memoryDirOf(workspaceRoot: string): string {
|
|
26
|
+
return path.join(workspaceRoot, WORKSPACE_DIRS.memoryDir);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function memoryIndexOf(workspaceRoot: string): string {
|
|
30
|
+
return path.join(workspaceRoot, WORKSPACE_FILES.memoryIndex);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Load every entry from `<workspaceRoot>/conversations/memory/`. The
|
|
34
|
+
// index file (`MEMORY.md`) and any sub-directory (e.g. future
|
|
35
|
+
// `archived/` from #1033) are skipped. Files that fail frontmatter
|
|
36
|
+
// parsing or carry an unknown `type` are logged and excluded.
|
|
37
|
+
export async function loadAllMemoryEntries(workspaceRoot: string): Promise<MemoryEntry[]> {
|
|
38
|
+
const dir = memoryDirOf(workspaceRoot);
|
|
39
|
+
const filenames = await listEntryFiles(dir);
|
|
40
|
+
const loaded: MemoryEntry[] = [];
|
|
41
|
+
for (const filename of filenames) {
|
|
42
|
+
const parsed = await readMemoryFile(path.join(dir, filename));
|
|
43
|
+
if (parsed) loaded.push(parsed);
|
|
44
|
+
}
|
|
45
|
+
return loaded;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Persist a single entry. Filename is `<slug>.md`. Returns the
|
|
49
|
+
// workspace-relative path that was written so callers can log /
|
|
50
|
+
// reference the destination. Slugs are validated against
|
|
51
|
+
// `isSafeMemorySlug` — `..` segments, separators, dotfiles, and
|
|
52
|
+
// reserved filenames are rejected so a caller-supplied slug cannot
|
|
53
|
+
// escape the memory directory or collide with `MEMORY.md`. PR-B
|
|
54
|
+
// will surface this same check at the agent-write boundary.
|
|
55
|
+
export async function writeMemoryEntry(workspaceRoot: string, entry: MemoryEntry): Promise<string> {
|
|
56
|
+
if (!isSafeMemorySlug(entry.slug)) {
|
|
57
|
+
throw new Error(`refusing to write memory entry with unsafe slug: ${JSON.stringify(entry.slug)}`);
|
|
58
|
+
}
|
|
59
|
+
const dir = memoryDirOf(workspaceRoot);
|
|
60
|
+
await mkdir(dir, { recursive: true });
|
|
61
|
+
const filename = `${entry.slug}.md`;
|
|
62
|
+
const absPath = path.join(dir, filename);
|
|
63
|
+
const content = serializeWithFrontmatter({ name: entry.name, description: entry.description, type: entry.type }, entry.body);
|
|
64
|
+
await writeFileAtomic(absPath, content, { uniqueTmp: true });
|
|
65
|
+
return path.posix.join(WORKSPACE_DIRS.memoryDir, filename);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Slug shape gate. Allows arbitrary unicode (so non-ASCII names slug
|
|
69
|
+
// fine via `slugifyMemoryName`'s hash fallback) but rejects anything
|
|
70
|
+
// that would let a caller escape the memory directory or shadow
|
|
71
|
+
// reserved filenames.
|
|
72
|
+
export function isSafeMemorySlug(slug: string): boolean {
|
|
73
|
+
if (typeof slug !== "string") return false;
|
|
74
|
+
if (slug.length === 0) return false;
|
|
75
|
+
if (slug.length > 200) return false;
|
|
76
|
+
if (slug.includes("/") || slug.includes("\\")) return false;
|
|
77
|
+
if (slug.includes("\0")) return false;
|
|
78
|
+
// `.`, `..`, leading `.` (dotfiles), or any case-fold of `MEMORY`
|
|
79
|
+
// would conflict with how the reader scans the directory. The
|
|
80
|
+
// case-fold covers `Memory` / `memory` collisions on macOS /
|
|
81
|
+
// Windows where the filesystem is case-insensitive — a case-
|
|
82
|
+
// sensitive comparison would let `slug = "Memory"` produce
|
|
83
|
+
// `Memory.md`, which aliases `MEMORY.md` and shadows the index.
|
|
84
|
+
if (slug.startsWith(".")) return false;
|
|
85
|
+
if (slug.toUpperCase() === "MEMORY") return false;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Rebuild `MEMORY.md` from current entry frontmatters. Sorted by type
|
|
90
|
+
// (preference / interest / fact / reference order) then by name so
|
|
91
|
+
// the index reads consistently. Empty memory directory writes a
|
|
92
|
+
// placeholder index (still useful as the marker that the new layout
|
|
93
|
+
// is in effect).
|
|
94
|
+
export async function regenerateIndex(workspaceRoot: string): Promise<void> {
|
|
95
|
+
const dir = memoryDirOf(workspaceRoot);
|
|
96
|
+
await mkdir(dir, { recursive: true });
|
|
97
|
+
const all = await loadAllMemoryEntries(workspaceRoot);
|
|
98
|
+
const sorted = [...all].sort(compareEntries);
|
|
99
|
+
const lines: string[] = ["# Memory", ""];
|
|
100
|
+
for (const entry of sorted) {
|
|
101
|
+
lines.push(`- [${entry.name}](${entry.slug}.md) — ${entry.description}`);
|
|
102
|
+
}
|
|
103
|
+
if (sorted.length === 0) lines.push("_(no entries yet)_");
|
|
104
|
+
lines.push("");
|
|
105
|
+
await writeFileAtomic(memoryIndexOf(workspaceRoot), lines.join("\n"), { uniqueTmp: true });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const TYPE_ORDER: readonly MemoryType[] = ["preference", "interest", "fact", "reference"];
|
|
109
|
+
|
|
110
|
+
function typeOrderKey(type: MemoryType): number {
|
|
111
|
+
const idx = TYPE_ORDER.indexOf(type);
|
|
112
|
+
return idx < 0 ? TYPE_ORDER.length : idx;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function compareEntries(left: MemoryEntry, right: MemoryEntry): number {
|
|
116
|
+
const typeDelta = typeOrderKey(left.type) - typeOrderKey(right.type);
|
|
117
|
+
if (typeDelta !== 0) return typeDelta;
|
|
118
|
+
return left.name.localeCompare(right.name);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function listEntryFiles(dir: string): Promise<string[]> {
|
|
122
|
+
// Missing memory dir → treat as empty. `readDirSafeAsync` already
|
|
123
|
+
// returns `[]` on ENOENT, so no explicit branch needed.
|
|
124
|
+
const dirents = await readDirSafeAsync(dir);
|
|
125
|
+
const candidates: string[] = [];
|
|
126
|
+
for (const dirent of dirents) {
|
|
127
|
+
if (!isCandidateName(dirent.name)) continue;
|
|
128
|
+
if (dirent.isFile()) {
|
|
129
|
+
candidates.push(dirent.name);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// Symlinks / non-regular entries: stat to resolve, skip on
|
|
133
|
+
// failure or non-file. A corrupt entry should not poison the
|
|
134
|
+
// read path.
|
|
135
|
+
const stat = await statSafeAsync(path.join(dir, dirent.name));
|
|
136
|
+
if (stat?.isFile()) candidates.push(dirent.name);
|
|
137
|
+
}
|
|
138
|
+
return candidates.sort();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Synchronous mirror of `loadAllMemoryEntries` for the agent prompt
|
|
142
|
+
// builder, which is sync all the way up. Same skip rules and
|
|
143
|
+
// frontmatter validation — corrupt entries log once and are excluded
|
|
144
|
+
// instead of leaking raw markdown into the system prompt.
|
|
145
|
+
export function loadAllMemoryEntriesSync(workspaceRoot: string): MemoryEntry[] {
|
|
146
|
+
const dir = memoryDirOf(workspaceRoot);
|
|
147
|
+
const dirents = readDirSafe(dir);
|
|
148
|
+
const filenames: string[] = [];
|
|
149
|
+
for (const dirent of dirents) {
|
|
150
|
+
if (!isCandidateName(dirent.name)) continue;
|
|
151
|
+
if (dirent.isFile()) {
|
|
152
|
+
filenames.push(dirent.name);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const stat: Stats | null = statSafe(path.join(dir, dirent.name));
|
|
156
|
+
if (stat?.isFile()) filenames.push(dirent.name);
|
|
157
|
+
}
|
|
158
|
+
filenames.sort();
|
|
159
|
+
const loaded: MemoryEntry[] = [];
|
|
160
|
+
for (const filename of filenames) {
|
|
161
|
+
const absPath = path.join(dir, filename);
|
|
162
|
+
const raw = readTextSafeSync(absPath);
|
|
163
|
+
if (raw === null) {
|
|
164
|
+
log.warn("memory", "failed to read entry", { path: absPath });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const entry = parseMemoryFile(absPath, raw);
|
|
168
|
+
if (entry) loaded.push(entry);
|
|
169
|
+
}
|
|
170
|
+
return loaded;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isCandidateName(name: string): boolean {
|
|
174
|
+
if (name === "MEMORY.md") return false;
|
|
175
|
+
if (!name.endsWith(".md")) return false;
|
|
176
|
+
if (name.startsWith(".")) return false;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function readMemoryFile(absPath: string): Promise<MemoryEntry | null> {
|
|
181
|
+
const raw = await readTextSafe(absPath);
|
|
182
|
+
if (raw === null) {
|
|
183
|
+
log.warn("memory", "failed to read entry", { path: absPath });
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return parseMemoryFile(absPath, raw);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Shared frontmatter validator used by both async and sync loaders.
|
|
190
|
+
// Returns null on missing envelope / unknown type / blank required
|
|
191
|
+
// fields; callers log + skip.
|
|
192
|
+
function parseMemoryFile(absPath: string, raw: string): MemoryEntry | null {
|
|
193
|
+
const parsed = parseFrontmatter(raw);
|
|
194
|
+
if (!parsed.hasHeader) {
|
|
195
|
+
log.warn("memory", "entry missing frontmatter", { path: absPath });
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const name = stringField(parsed.meta.name);
|
|
199
|
+
const description = stringField(parsed.meta.description);
|
|
200
|
+
const { type } = parsed.meta;
|
|
201
|
+
if (!name || !description || !isMemoryType(type)) {
|
|
202
|
+
log.warn("memory", "entry frontmatter incomplete", { path: absPath });
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const slug = path.basename(absPath, ".md");
|
|
206
|
+
return { name, description, type, body: parsed.body, slug };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function stringField(value: unknown): string | null {
|
|
210
|
+
if (typeof value !== "string") return null;
|
|
211
|
+
const trimmed = value.trim();
|
|
212
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
213
|
+
}
|