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
|
@@ -11,6 +11,7 @@ import path from "node:path";
|
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { slugify } from "../../utils/slug.js";
|
|
13
13
|
import { toUtcIsoDate } from "../../utils/date.js";
|
|
14
|
+
import { writeFileAtomic } from "../../utils/files/atomic.js";
|
|
14
15
|
import { WORKSPACE_DIRS } from "../paths.js";
|
|
15
16
|
|
|
16
17
|
export const SEARCHES_DIR = WORKSPACE_DIRS.searches;
|
|
@@ -87,7 +88,11 @@ const defaultDeps: WriteSearchDeps = {
|
|
|
87
88
|
mkdir: async (dir) => {
|
|
88
89
|
await fsp.mkdir(dir, { recursive: true });
|
|
89
90
|
},
|
|
90
|
-
|
|
91
|
+
// Atomic write so a crash mid-flight can't leave a half-written
|
|
92
|
+
// search markdown that the jsonl `contentRef` then points at
|
|
93
|
+
// (#881 v2). Tests inject their own adapter so behaviour stays
|
|
94
|
+
// mockable.
|
|
95
|
+
writeFile: (filePath, content) => writeFileAtomic(filePath, content),
|
|
91
96
|
};
|
|
92
97
|
|
|
93
98
|
/**
|
|
@@ -21,6 +21,7 @@ import { workspacePath as defaultWorkspacePath } from "../workspace.js";
|
|
|
21
21
|
import { WORKSPACE_DIRS } from "../paths.js";
|
|
22
22
|
import { log } from "../../system/logger/index.js";
|
|
23
23
|
import { updateSessionBacklinks } from "./sessionBacklinks.js";
|
|
24
|
+
import { writeWikiPage } from "../wiki-pages/io.js";
|
|
24
25
|
import { ONE_SECOND_MS } from "../../utils/time.js";
|
|
25
26
|
|
|
26
27
|
// Small tolerance for filesystem mtime granularity (some filesystems
|
|
@@ -35,12 +36,23 @@ export interface WikiBacklinksDeps {
|
|
|
35
36
|
writeFile: (filePath: string, content: string) => Promise<void>;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
// Default writers funnel through `writeWikiPage` so the backlink
|
|
40
|
+
// append participates in the same atomic-write + history pipeline
|
|
41
|
+
// as user / LLM edits. Pre-#763 this was a non-atomic
|
|
42
|
+
// `fsp.writeFile` and the call could leave a half-written page on
|
|
43
|
+
// crash. Editor identity is `"system"` so PR 2's snapshot view can
|
|
44
|
+
// distinguish automated edits from human / LLM ones.
|
|
45
|
+
function buildDefaultDeps(workspaceRoot: string, sessionId: string): WikiBacklinksDeps {
|
|
46
|
+
return {
|
|
47
|
+
readdir: (dir) => fsp.readdir(dir),
|
|
48
|
+
stat: (filePath) => fsp.stat(filePath),
|
|
49
|
+
readFile: (filePath) => fsp.readFile(filePath, "utf-8"),
|
|
50
|
+
writeFile: async (filePath, content) => {
|
|
51
|
+
const slug = path.basename(filePath, ".md");
|
|
52
|
+
await writeWikiPage(slug, content, { editor: "system", sessionId }, { workspaceRoot });
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
44
56
|
|
|
45
57
|
export interface MaybeAppendWikiBacklinksOptions {
|
|
46
58
|
chatSessionId: string;
|
|
@@ -52,7 +64,7 @@ export interface MaybeAppendWikiBacklinksOptions {
|
|
|
52
64
|
export async function maybeAppendWikiBacklinks(opts: MaybeAppendWikiBacklinksOptions): Promise<void> {
|
|
53
65
|
if (!opts.chatSessionId) return;
|
|
54
66
|
const workspaceRoot = opts.workspaceRoot ?? defaultWorkspacePath;
|
|
55
|
-
const deps: WikiBacklinksDeps = { ...
|
|
67
|
+
const deps: WikiBacklinksDeps = { ...buildDefaultDeps(workspaceRoot, opts.chatSessionId), ...(opts.deps ?? {}) };
|
|
56
68
|
const pagesDir = path.join(workspaceRoot, WORKSPACE_DIRS.wikiPages);
|
|
57
69
|
|
|
58
70
|
const files = await listPageFiles(pagesDir, deps);
|
|
@@ -113,6 +113,7 @@ function extractSessionIdFromHref(href: string): string | null {
|
|
|
113
113
|
function stripFragmentAndQuery(href: string): string {
|
|
114
114
|
let end = href.length;
|
|
115
115
|
const hash = href.indexOf("#");
|
|
116
|
+
// eslint-disable-next-line security/detect-possible-timing-attacks -- numeric indexOf check, not a credential compare
|
|
116
117
|
if (hash !== -1) end = hash;
|
|
117
118
|
const query = href.indexOf("?");
|
|
118
119
|
if (query !== -1 && query < end) end = query;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// server/workspace/wiki-history/hook/snapshot.ts
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import path2 from "node:path";
|
|
7
|
+
|
|
8
|
+
// src/lib/wiki-page/slug.ts
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
function isSafeSlug(slug) {
|
|
11
|
+
if (slug.length === 0) return false;
|
|
12
|
+
if (slug === "." || slug === "..") return false;
|
|
13
|
+
if (slug.includes("/") || slug.includes("\\")) return false;
|
|
14
|
+
if (slug.includes("\0")) return false;
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
function wikiSlugFromAbsPath(absPath, pagesDir) {
|
|
18
|
+
const rel = path.relative(pagesDir, absPath);
|
|
19
|
+
if (rel.length === 0) return null;
|
|
20
|
+
if (path.isAbsolute(rel)) return null;
|
|
21
|
+
if (rel.includes(path.sep)) return null;
|
|
22
|
+
if (!rel.endsWith(".md")) return null;
|
|
23
|
+
const slug = rel.slice(0, -".md".length);
|
|
24
|
+
if (!isSafeSlug(slug)) return null;
|
|
25
|
+
return slug;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// server/workspace/wiki-history/hook/snapshot.ts
|
|
29
|
+
var WORKSPACE_ROOT = path2.join(homedir(), "mulmoclaude");
|
|
30
|
+
var WIKI_PAGES_DIR = path2.join(WORKSPACE_ROOT, "data", "wiki", "pages");
|
|
31
|
+
var TOKEN_PATH = path2.join(WORKSPACE_ROOT, ".session-token");
|
|
32
|
+
var PORT_PATH = path2.join(WORKSPACE_ROOT, ".server-port");
|
|
33
|
+
var SERVER_HOST = process.env.MULMOCLAUDE_HOST ?? "127.0.0.1";
|
|
34
|
+
async function readStdin() {
|
|
35
|
+
const chunks = [];
|
|
36
|
+
for await (const chunk of process.stdin) {
|
|
37
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
38
|
+
}
|
|
39
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
40
|
+
if (!raw.trim()) return null;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(raw);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function extractFilePath(payload) {
|
|
48
|
+
const fromInput = payload.tool_input?.file_path;
|
|
49
|
+
if (typeof fromInput === "string") return fromInput;
|
|
50
|
+
const fromResponse = payload.tool_response?.filePath;
|
|
51
|
+
if (typeof fromResponse === "string") return fromResponse;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function readTokenSafe() {
|
|
55
|
+
try {
|
|
56
|
+
return readFileSync(TOKEN_PATH, "utf-8").trim();
|
|
57
|
+
} catch {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function readPortSafe() {
|
|
62
|
+
try {
|
|
63
|
+
const raw = readFileSync(PORT_PATH, "utf-8").trim();
|
|
64
|
+
const port = Number.parseInt(raw, 10);
|
|
65
|
+
return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function main() {
|
|
71
|
+
const payload = await readStdin();
|
|
72
|
+
if (!payload) return;
|
|
73
|
+
const filePath = extractFilePath(payload);
|
|
74
|
+
if (!filePath) return;
|
|
75
|
+
const slug = wikiSlugFromAbsPath(filePath, WIKI_PAGES_DIR);
|
|
76
|
+
if (slug === null) return;
|
|
77
|
+
const token = readTokenSafe();
|
|
78
|
+
const port = readPortSafe();
|
|
79
|
+
if (!token || port === null) return;
|
|
80
|
+
const envChatSessionId = process.env.MULMOCLAUDE_CHAT_SESSION_ID;
|
|
81
|
+
const payloadSessionId = typeof payload.session_id === "string" && payload.session_id.length > 0 ? payload.session_id : void 0;
|
|
82
|
+
const sessionId = envChatSessionId && envChatSessionId.length > 0 ? envChatSessionId : payloadSessionId;
|
|
83
|
+
const body = sessionId === void 0 ? { slug } : { slug, sessionId };
|
|
84
|
+
try {
|
|
85
|
+
await fetch(`http://${SERVER_HOST}:${port}/api/wiki/internal/snapshot`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
Authorization: `Bearer ${token}`
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify(body)
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
main().catch(() => {
|
|
97
|
+
});
|
|
98
|
+
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic25hcHNob3QudHMiLCAiLi4vLi4vLi4vLi4vc3JjL2xpYi93aWtpLXBhZ2Uvc2x1Zy50cyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiLy8gUG9zdFRvb2xVc2UgaG9vayBzb3VyY2UgXHUyMDE0IHJ1bnMgYWZ0ZXIgZXZlcnkgQ2xhdWRlIENMSSBXcml0ZSAvXG4vLyBFZGl0IHRvb2wgY2FsbC4gRGV0ZWN0cyB3cml0ZXMgdGhhdCB0YXJnZXQgYSB3aWtpIHBhZ2UgZmlsZVxuLy8gYW5kIFBPU1RzIGB7IHNsdWcsIHNlc3Npb25JZD8gfWAgdG8gdGhlIHBhcmVudCBzZXJ2ZXInc1xuLy8gYC9hcGkvd2lraS9pbnRlcm5hbC9zbmFwc2hvdGAgZW5kcG9pbnQgc28gdGhlIHNuYXBzaG90IHBpcGVsaW5lXG4vLyAoIzc2MyBQUiAyKSByZWNvcmRzIHRoZSBuZXcgc3RhdGUuXG4vL1xuLy8gVEhJUyBGSUxFIElTIFRIRSBTT1VSQ0UgT0YgVFJVVEguIEVkaXRzIGhlcmUsIHRoZW4gcnVuXG4vLyBgeWFybiBidWlsZDpob29rc2AgKG9yIGB5YXJuIGJ1aWxkYCkgdG8gcmVnZW5lcmF0ZVxuLy8gYC4vc25hcHNob3QubWpzYC4gVGhlIGJ1bmRsZWQgYC5tanNgIGlzIGNvbW1pdHRlZCB0byBnaXQgc29cbi8vIGBwcm92aXNpb24udHNgIGNhbiByZWFkIGl0IGF0IHNlcnZlciBzdGFydHVwIHdpdGhvdXQgaW52b2tpbmdcbi8vIGVzYnVpbGQgb24gdGhlIHJ1bnRpbWUgcGF0aC4gQ0kgcnVucyBgeWFybiBidWlsZDpob29rcyAmJlxuLy8gZ2l0IGRpZmYgLS1leGl0LWNvZGVgIHRvIGNhdGNoIGEgc3RhbGUgYnVuZGxlLlxuLy9cbi8vIFRoZSBob29rIGV4ZWN1dGVzIGluc2lkZSBDbGF1ZGUgQ0xJJ3MgcHJvY2VzcyBzcGFjZSBcdTIwMTQgbXVzdCBiZVxuLy8gc2VsZi1jb250YWluZWQgRVNNLiBlc2J1aWxkIGJ1bmRsZXMgaW1wb3J0cyBmcm9tIHRoZSBzaGFyZWRcbi8vIGBzcmMvbGliL3dpa2ktcGFnZS9zbHVnLnRzYCBtb2R1bGUgc28gdGhlIGhvb2sgYW5kIHRoZVxuLy8gc2VydmVyLXNpZGUgYGNsYXNzaWZ5QXNXaWtpUGFnZWAgYWdyZWUgb24gd2hhdCBjb3VudHMgYXMgYVxuLy8gd2lraSBwYWdlIChubyBjb3B5LXBhc3RlIGRyaWZ0KS5cblxuaW1wb3J0IHsgcmVhZEZpbGVTeW5jIH0gZnJvbSBcIm5vZGU6ZnNcIjtcbmltcG9ydCB7IGhvbWVkaXIgfSBmcm9tIFwibm9kZTpvc1wiO1xuaW1wb3J0IHBhdGggZnJvbSBcIm5vZGU6cGF0aFwiO1xuaW1wb3J0IHsgd2lraVNsdWdGcm9tQWJzUGF0aCB9IGZyb20gXCIuLi8uLi8uLi8uLi9zcmMvbGliL3dpa2ktcGFnZS9zbHVnLmpzXCI7XG5cbi8vIFdvcmtzcGFjZSBsYXlvdXQgbWF0Y2hlcyBgV09SS1NQQUNFX0RJUlMud2lraVBhZ2VzYC4gV2UgaGFyZC1cbi8vIGNvZGUgXCJkYXRhL3dpa2kvcGFnZXNcIiByYXRoZXIgdGhhbiBpbXBvcnRpbmcgdGhlIGNvbnN0YW50c1xuLy8gYmVjYXVzZSB0aGUgaG9vayBuZWVkcyB0byBzdGF5IGluZGVwZW5kZW50IG9mIHRoZSBzZXJ2ZXInc1xuLy8gcnVudGltZSBjb25maWcgXHUyMDE0IHRoZSBidW5kbGUgbXVzdCB3b3JrIGV2ZW4gd2hlbiB0aGUgc2VydmVyXG4vLyBwcm9jZXNzIGlzbid0IHJ1bm5pbmcgeWV0IChlLmcuIGR1cmluZyBhIHN0YXJ0dXAgcmFjZSkuXG5jb25zdCBXT1JLU1BBQ0VfUk9PVCA9IHBhdGguam9pbihob21lZGlyKCksIFwibXVsbW9jbGF1ZGVcIik7XG5jb25zdCBXSUtJX1BBR0VTX0RJUiA9IHBhdGguam9pbihXT1JLU1BBQ0VfUk9PVCwgXCJkYXRhXCIsIFwid2lraVwiLCBcInBhZ2VzXCIpO1xuY29uc3QgVE9LRU5fUEFUSCA9IHBhdGguam9pbihXT1JLU1BBQ0VfUk9PVCwgXCIuc2Vzc2lvbi10b2tlblwiKTtcbmNvbnN0IFBPUlRfUEFUSCA9IHBhdGguam9pbihXT1JLU1BBQ0VfUk9PVCwgXCIuc2VydmVyLXBvcnRcIik7XG4vLyBJbiBEb2NrZXIgbW9kZSB0aGUgcGFyZW50IHNlcnZlciBydW5zIG9uIHRoZSBob3N0J3MgMTI3LjAuMC4xXG4vLyB3aGljaCB0aGUgY29udGFpbmVyIGNhbid0IHJlYWNoIHZpYSBwbGFpbiBsb29wYmFjay4gVGhlIERvY2tlclxuLy8gc3Bhd24gcGx1bWJpbmcgc2V0cyBNVUxNT0NMQVVERV9IT1NUPWhvc3QuZG9ja2VyLmludGVybmFsIHNvXG4vLyBmZXRjaCgpIHJlc29sdmVzIHRvIHRoZSBob3N0IHNlcnZlci4gT3V0c2lkZSBEb2NrZXIgKG9yIHdoZW5cbi8vIHRoZSB2YXIgaXMgdW5zZXQpIHdlIGZhbGwgYmFjayB0byB0aGUgbG9vcGJhY2sgYWRkcmVzcy5cbmNvbnN0IFNFUlZFUl9IT1NUID0gcHJvY2Vzcy5lbnYuTVVMTU9DTEFVREVfSE9TVCA/PyBcIjEyNy4wLjAuMVwiO1xuXG5pbnRlcmZhY2UgSG9va1BheWxvYWQge1xuICB0b29sX2lucHV0PzogeyBmaWxlX3BhdGg/OiB1bmtub3duIH07XG4gIHRvb2xfcmVzcG9uc2U/OiB7IGZpbGVQYXRoPzogdW5rbm93biB9O1xuICBzZXNzaW9uX2lkPzogdW5rbm93bjtcbn1cblxuYXN5bmMgZnVuY3Rpb24gcmVhZFN0ZGluKCk6IFByb21pc2U8SG9va1BheWxvYWQgfCBudWxsPiB7XG4gIGNvbnN0IGNodW5rczogQnVmZmVyW10gPSBbXTtcbiAgZm9yIGF3YWl0IChjb25zdCBjaHVuayBvZiBwcm9jZXNzLnN0ZGluKSB7XG4gICAgY2h1bmtzLnB1c2godHlwZW9mIGNodW5rID09PSBcInN0cmluZ1wiID8gQnVmZmVyLmZyb20oY2h1bmspIDogY2h1bmspO1xuICB9XG4gIGNvbnN0IHJhdyA9IEJ1ZmZlci5jb25jYXQoY2h1bmtzKS50b1N0cmluZyhcInV0Zi04XCIpO1xuICBpZiAoIXJhdy50cmltKCkpIHJldHVybiBudWxsO1xuICB0cnkge1xuICAgIHJldHVybiBKU09OLnBhcnNlKHJhdykgYXMgSG9va1BheWxvYWQ7XG4gIH0gY2F0Y2gge1xuICAgIHJldHVybiBudWxsO1xuICB9XG59XG5cbmZ1bmN0aW9uIGV4dHJhY3RGaWxlUGF0aChwYXlsb2FkOiBIb29rUGF5bG9hZCk6IHN0cmluZyB8IG51bGwge1xuICAvLyBDbGF1ZGUgQ0xJIGhvb2sgcGF5bG9hZDogeyB0b29sX25hbWUsIHRvb2xfaW5wdXQsIHRvb2xfcmVzcG9uc2UsIC4uLiB9XG4gIC8vIERpZmZlcmVudCB0b29scyBzdXJmYWNlIHRoZSBwYXRoIHVuZGVyIGRpZmZlcmVudCBrZXlzIFx1MjAxNFxuICAvLyBjaGVjayBib3RoIHNoYXBlcyBkZWZlbnNpdmVseS5cbiAgY29uc3QgZnJvbUlucHV0ID0gcGF5bG9hZC50b29sX2lucHV0Py5maWxlX3BhdGg7XG4gIGlmICh0eXBlb2YgZnJvbUlucHV0ID09PSBcInN0cmluZ1wiKSByZXR1cm4gZnJvbUlucHV0O1xuICBjb25zdCBmcm9tUmVzcG9uc2UgPSBwYXlsb2FkLnRvb2xfcmVzcG9uc2U/LmZpbGVQYXRoO1xuICBpZiAodHlwZW9mIGZyb21SZXNwb25zZSA9PT0gXCJzdHJpbmdcIikgcmV0dXJuIGZyb21SZXNwb25zZTtcbiAgcmV0dXJuIG51bGw7XG59XG5cbmZ1bmN0aW9uIHJlYWRUb2tlblNhZmUoKTogc3RyaW5nIHtcbiAgdHJ5IHtcbiAgICByZXR1cm4gcmVhZEZpbGVTeW5jKFRPS0VOX1BBVEgsIFwidXRmLThcIikudHJpbSgpO1xuICB9IGNhdGNoIHtcbiAgICByZXR1cm4gXCJcIjtcbiAgfVxufVxuXG5mdW5jdGlvbiByZWFkUG9ydFNhZmUoKTogbnVtYmVyIHwgbnVsbCB7XG4gIHRyeSB7XG4gICAgY29uc3QgcmF3ID0gcmVhZEZpbGVTeW5jKFBPUlRfUEFUSCwgXCJ1dGYtOFwiKS50cmltKCk7XG4gICAgY29uc3QgcG9ydCA9IE51bWJlci5wYXJzZUludChyYXcsIDEwKTtcbiAgICByZXR1cm4gTnVtYmVyLmlzSW50ZWdlcihwb3J0KSAmJiBwb3J0ID4gMCAmJiBwb3J0IDwgNjU1MzYgPyBwb3J0IDogbnVsbDtcbiAgfSBjYXRjaCB7XG4gICAgcmV0dXJuIG51bGw7XG4gIH1cbn1cblxuYXN5bmMgZnVuY3Rpb24gbWFpbigpOiBQcm9taXNlPHZvaWQ+IHtcbiAgY29uc3QgcGF5bG9hZCA9IGF3YWl0IHJlYWRTdGRpbigpO1xuICBpZiAoIXBheWxvYWQpIHJldHVybjtcblxuICBjb25zdCBmaWxlUGF0aCA9IGV4dHJhY3RGaWxlUGF0aChwYXlsb2FkKTtcbiAgaWYgKCFmaWxlUGF0aCkgcmV0dXJuO1xuICBjb25zdCBzbHVnID0gd2lraVNsdWdGcm9tQWJzUGF0aChmaWxlUGF0aCwgV0lLSV9QQUdFU19ESVIpO1xuICBpZiAoc2x1ZyA9PT0gbnVsbCkgcmV0dXJuO1xuXG4gIGNvbnN0IHRva2VuID0gcmVhZFRva2VuU2FmZSgpO1xuICBjb25zdCBwb3J0ID0gcmVhZFBvcnRTYWZlKCk7XG4gIGlmICghdG9rZW4gfHwgcG9ydCA9PT0gbnVsbCkgcmV0dXJuOyAvLyBzZXJ2ZXIgaXNuJ3QgcmVhY2hhYmxlOyBzaWxlbnQgbm8tb3BcblxuICAvLyBQcmVmZXIgb3VyIGNoYXQtc2Vzc2lvbiBpZCBmcm9tIHRoZSBzcGF3biBlbnYgKCM5NjMpIFx1MjAxNCB0aGVcbiAgLy8gc2VydmVyJ3Mgc2Vzc2lvbiBzdG9yZSBrZXlzIGJ5IGNoYXRTZXNzaW9uSWQsIG5vdCBieSBDbGF1ZGVcbiAgLy8gQ0xJJ3MgaW50ZXJuYWwgc2Vzc2lvbl9pZCwgc28gdGhlIHRvb2xSZXN1bHQgcHVibGlzaCBvbiB0aGVcbiAgLy8gc2VydmVyIHNpZGUgb25seSBtYXRjaGVzIHdoZW4gd2UgZm9yd2FyZCBvdXIgb3duIGlkLiBGYWxsIGJhY2tcbiAgLy8gdG8gQ2xhdWRlIENMSSdzIGBzZXNzaW9uX2lkYCAoc3RpbGwgdXNlZnVsIGFzIGEgdHJhY2luZyB0b2tlblxuICAvLyBpbiB0aGUgc25hcHNob3QgZnJvbnRtYXR0ZXIpIHdoZW4gdGhlIGVudiB2YXIgaXMgYWJzZW50IFx1MjAxNCBlLmcuXG4gIC8vIGFuIG9sZGVyIG11bG1vY2xhdWRlIHNlcnZlciBzcGF3bmluZyBhIG5ld2VyIGhvb2sgYnVuZGxlLlxuICBjb25zdCBlbnZDaGF0U2Vzc2lvbklkID0gcHJvY2Vzcy5lbnYuTVVMTU9DTEFVREVfQ0hBVF9TRVNTSU9OX0lEO1xuICBjb25zdCBwYXlsb2FkU2Vzc2lvbklkID0gdHlwZW9mIHBheWxvYWQuc2Vzc2lvbl9pZCA9PT0gXCJzdHJpbmdcIiAmJiBwYXlsb2FkLnNlc3Npb25faWQubGVuZ3RoID4gMCA/IHBheWxvYWQuc2Vzc2lvbl9pZCA6IHVuZGVmaW5lZDtcbiAgY29uc3Qgc2Vzc2lvbklkID0gZW52Q2hhdFNlc3Npb25JZCAmJiBlbnZDaGF0U2Vzc2lvbklkLmxlbmd0aCA+IDAgPyBlbnZDaGF0U2Vzc2lvbklkIDogcGF5bG9hZFNlc3Npb25JZDtcbiAgY29uc3QgYm9keSA9IHNlc3Npb25JZCA9PT0gdW5kZWZpbmVkID8geyBzbHVnIH0gOiB7IHNsdWcsIHNlc3Npb25JZCB9O1xuXG4gIHRyeSB7XG4gICAgYXdhaXQgZmV0Y2goYGh0dHA6Ly8ke1NFUlZFUl9IT1NUfToke3BvcnR9L2FwaS93aWtpL2ludGVybmFsL3NuYXBzaG90YCwge1xuICAgICAgbWV0aG9kOiBcIlBPU1RcIixcbiAgICAgIGhlYWRlcnM6IHtcbiAgICAgICAgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uXCIsXG4gICAgICAgIEF1dGhvcml6YXRpb246IGBCZWFyZXIgJHt0b2tlbn1gLFxuICAgICAgfSxcbiAgICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KGJvZHkpLFxuICAgIH0pO1xuICB9IGNhdGNoIHtcbiAgICAvLyBOZXR3b3JrIC8gc2VydmVyIGRvd24gXHUyMDE0IGRyb3Agc2lsZW50bHkuIFRoZSB3aWtpIHBhZ2Ugd3JpdGVcbiAgICAvLyBpdHNlbGYgYWxyZWFkeSBzdWNjZWVkZWQ7IG1pc3Npbmcgb25lIHNuYXBzaG90IGlzIHJlY292ZXJhYmxlXG4gICAgLy8gKHRoZSBuZXh0IHNhdmUgd2lsbCByZWNvcmQgdGhlIG5leHQgc3RhdGUpLlxuICB9XG59XG5cbm1haW4oKS5jYXRjaCgoKSA9PiB7XG4gIC8vIEhvb2tzIHJ1biBzeW5jaHJvbm91c2x5IHRvIHRoZSBMTE0gdG9vbCBmbG93IFx1MjAxNCBuZXZlciB0aHJvdy5cbiAgLy8gQSBidXN0ZWQgaG9vayB3b3VsZCBtYWtlIGBXcml0ZWAgaXRzZWxmIGxvb2sgbGlrZSBpdCBmYWlsZWRcbiAgLy8gdG8gdGhlIExMTSwgd2hpY2ggaXMgbXVjaCB3b3JzZSB0aGFuIGEgbWlzc2luZyBzbmFwc2hvdC5cbn0pO1xuIiwgIi8vIFNoYXJlZCB3aWtpLXBhZ2Ugc2x1ZyBsb2dpYyBcdTIwMTQgcHVyZSBmdW5jdGlvbnMgcmV1c2VkIGJ5OlxuLy9cbi8vICAgLSBzZXJ2ZXIvd29ya3NwYWNlL3dpa2ktcGFnZXMvaW8udHMgKHRoZSB3cml0ZSBjaG9rZXBvaW50KVxuLy8gICAtIHNlcnZlci93b3Jrc3BhY2Uvd2lraS1oaXN0b3J5L2hvb2svc25hcHNob3QudHMgKHRoZVxuLy8gICAgIFBvc3RUb29sVXNlIGhvb2sgc2NyaXB0IHRoYXQgZGV0ZWN0cyBMTE0gV3JpdGUvRWRpdCBvbiBhXG4vLyAgICAgd2lraSBwYWdlIGFuZCB0cmlnZ2VycyBhIHNuYXBzaG90KVxuLy9cbi8vIEJvdGggY2FsbCBzaXRlcyBuZWVkIHRoZSBzYW1lIGFuc3dlcjogXCJnaXZlbiB0aGlzIGFic29sdXRlXG4vLyBwYXRoIGFuZCB0aGUgcGFnZXMgZGlyZWN0b3J5LCBpcyBpdCBhIHdpa2kgcGFnZSwgYW5kIGlmIHNvXG4vLyB3aGF0IHNsdWc/XCIuIEJlZm9yZSB0aGUgZXh0cmFjdGlvbiB0aGUgaG9vayByZS1pbXBsZW1lbnRlZFxuLy8gdGhlIHJ1bGVzIGFzIGEgSlMtYXMtc3RyaW5nIHRlbXBsYXRlIGxpdGVyYWwgdGhhdCBkcmlmdGVkXG4vLyBmcm9tIHRoZSBzZXJ2ZXItc2lkZSBsb2dpYyBcdTIwMTQgc2VlICM5NTEncyBkaXNjdXNzaW9uIG9mIGhvdyB0b1xuLy8gc3RvcCBjb3B5aW5nICNsaW50IGxvZ2ljIGJldHdlZW4gaG9vayBhbmQgc2VydmVyLlxuLy9cbi8vIE9ubHkgYG5vZGU6cGF0aGAgaXMgYWxsb3dlZCBoZXJlIHNvIGVzYnVpbGQgY2FuIGJ1bmRsZSB0aGlzXG4vLyBmaWxlIGludG8gdGhlIGhvb2sgc2NyaXB0IChubyBOb2RlLXNwZWNpZmljIHNlcnZlci1zaWRlXG4vLyBpbXBvcnRzIGxlYWsgaW50byB0aGUgYnVuZGxlKS5cblxuaW1wb3J0IHBhdGggZnJvbSBcIm5vZGU6cGF0aFwiO1xuXG4vKiogUmVqZWN0IHNsdWdzIHRoYXQgd291bGQgZXNjYXBlIGBkYXRhL3dpa2kvcGFnZXMvYCBvbmNlXG4gKiAgam9pbmVkIGJhY2sgaW50byBhIHBhdGgsIG9yIHRoYXQgYXJlIG90aGVyd2lzZSBpbnZhbGlkIGFzXG4gKiAgcGFnZSBmaWxlbmFtZXMuIFRoZSBjaG9rZXBvaW50IG11c3QgZGVmZW5kIGl0c2VsZiBldmVuIHdoZW5cbiAqICBjYWxsZXJzIGRlcml2ZSB0aGUgc2x1ZyBmcm9tIGEgdHJ1c3RlZCBzb3VyY2UgXHUyMDE0IGEgdHlwbyBvclxuICogIGZ1dHVyZSBjYWxsZXIgbWlzdGFrZSBzaG91bGQgZmFpbCBsb3VkLCBub3Qgc2lsZW50bHkgd3JpdGVcbiAqICBvdXRzaWRlIHRoZSB3aWtpIHRyZWUuXG4gKlxuICogIFRoZSBydWxlIGlzIGludGVudGlvbmFsbHkgbmFycm93IFx1MjAxNCBzZXBhcmF0b3JzIC8gYC4uYCAvIE5VTCAvXG4gKiAgZW1wdHkgXHUyMDE0IHNvIGl0IG9ubHkgcmVqZWN0cyB1bmFtYmlndW91cyB2aW9sYXRpb25zLiBBZXN0aGV0aWNcbiAqICBjb25jZXJucyAoZS5nLiBkb3QtcHJlZml4ZWQgZmlsZW5hbWVzKSBhcmUgb3V0IG9mIHNjb3BlOiBhXG4gKiAgcHJlLWV4aXN0aW5nIGBkYXRhL3dpa2kvcGFnZXMvLmZvby5tZGAgc2hvdWxkIHJlbWFpbiB3cml0YWJsZVxuICogIHRocm91Z2ggdGhlIGNob2tlcG9pbnQgKGNvZGV4IHJldmlldyBpdGVyLTIgIzg4MykuICovXG5leHBvcnQgZnVuY3Rpb24gaXNTYWZlU2x1ZyhzbHVnOiBzdHJpbmcpOiBib29sZWFuIHtcbiAgaWYgKHNsdWcubGVuZ3RoID09PSAwKSByZXR1cm4gZmFsc2U7XG4gIGlmIChzbHVnID09PSBcIi5cIiB8fCBzbHVnID09PSBcIi4uXCIpIHJldHVybiBmYWxzZTtcbiAgaWYgKHNsdWcuaW5jbHVkZXMoXCIvXCIpIHx8IHNsdWcuaW5jbHVkZXMoXCJcXFxcXCIpKSByZXR1cm4gZmFsc2U7XG4gIGlmIChzbHVnLmluY2x1ZGVzKFwiXFwwXCIpKSByZXR1cm4gZmFsc2U7XG4gIHJldHVybiB0cnVlO1xufVxuXG4vKiogR2l2ZW4gYW4gYWJzb2x1dGUgcGF0aCBhbmQgdGhlIGFic29sdXRlIGBwYWdlc0RpcmAsIHJldHVybiB0aGVcbiAqICBzbHVnIGlmIGBhYnNQYXRoYCBpcyBhIGRpcmVjdCBgLm1kYCBjaGlsZCBvZiBgcGFnZXNEaXJgLCBlbHNlXG4gKiAgbnVsbC4gUHVyZSBwYXRoLXN0cmluZyBtYXRoIFx1MjAxNCBubyBmcyBJTywgbm8gc3ltbGluayByZXNvbHV0aW9uLlxuICpcbiAqICBDYWxsZXIgcmVzcG9uc2liaWxpdHk6IHBhc3MgYWxyZWFkeS1yZWFscGF0aCdkIHZhbHVlcyBmb3IgYm90aFxuICogIGFyZ3VtZW50cy4gTWl4aW5nIGEgcmVhbHBhdGgnZCBgYWJzUGF0aGAgd2l0aCBhIHN5bWxpbmtlZFxuICogIGBwYWdlc0RpcmAgKG9yIHZpY2UgdmVyc2EpIHNpbGVudGx5IG1pc21hdGNoZXMgYmVjYXVzZVxuICogIGBwYXRoLnJlbGF0aXZlYCBpcyBwbGFpbiBzdHJpbmcgYXJpdGhtZXRpYy4gVGhlIHRyYXAgY2F1c2VkXG4gKiAgIzg4MyByZXZpZXctaXRlci0xIFx1MjAxNCBhIHN5bWxpbmtlZCB3b3Jrc3BhY2Ugc2lsZW50bHkgcm91dGVkXG4gKiAgd2lraSB3cml0ZXMgdGhyb3VnaCB0aGUgZ2VuZXJpYyB3cml0ZXIuICovXG5leHBvcnQgZnVuY3Rpb24gd2lraVNsdWdGcm9tQWJzUGF0aChhYnNQYXRoOiBzdHJpbmcsIHBhZ2VzRGlyOiBzdHJpbmcpOiBzdHJpbmcgfCBudWxsIHtcbiAgY29uc3QgcmVsID0gcGF0aC5yZWxhdGl2ZShwYWdlc0RpciwgYWJzUGF0aCk7XG4gIGlmIChyZWwubGVuZ3RoID09PSAwKSByZXR1cm4gbnVsbDtcbiAgaWYgKHBhdGguaXNBYnNvbHV0ZShyZWwpKSByZXR1cm4gbnVsbDtcbiAgLy8gRGlyZWN0IGNoaWxkIG9ubHkgXHUyMDE0IG5vIG5lc3RlZCBsYXlvdXQgdG9kYXkuIEFueSBzZXBhcmF0b3JcbiAgLy8gbWVhbnMgdGhlIHBhdGggZWl0aGVyIGVzY2FwZXMgKGAuLi9zZWNyZXQubWRgKSBvciBkZXNjZW5kc1xuICAvLyAoYHN1YmRpci9mb28ubWRgKS4gQSBsaXRlcmFsIHBhZ2UgbmFtZSBsaWtlIGAuLmZvby5tZGAgaXMgYVxuICAvLyBzaW5nbGUgc2VnbWVudCB3aXRob3V0IGEgc2VwYXJhdG9yIGFuZCBpcyBhbGxvd2VkIChjb2RleFxuICAvLyBpdGVyLTMgIzg4MyBcdTIwMTQgdGhlIHByaW9yIGBzdGFydHNXaXRoKFwiLi5cIilgIHJ1bGUgd3JvbmdseVxuICAvLyByZWplY3RlZCBpdCkuXG4gIGlmIChyZWwuaW5jbHVkZXMocGF0aC5zZXApKSByZXR1cm4gbnVsbDtcbiAgaWYgKCFyZWwuZW5kc1dpdGgoXCIubWRcIikpIHJldHVybiBudWxsO1xuICBjb25zdCBzbHVnID0gcmVsLnNsaWNlKDAsIC1cIi5tZFwiLmxlbmd0aCk7XG4gIGlmICghaXNTYWZlU2x1ZyhzbHVnKSkgcmV0dXJuIG51bGw7XG4gIHJldHVybiBzbHVnO1xufVxuIl0sCiAgIm1hcHBpbmdzIjogIjs7O0FBbUJBLFNBQVMsb0JBQW9CO0FBQzdCLFNBQVMsZUFBZTtBQUN4QixPQUFPQSxXQUFV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
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// PostToolUse hook source — runs after every Claude CLI Write /
|
|
2
|
+
// Edit tool call. Detects writes that target a wiki page file
|
|
3
|
+
// and POSTs `{ slug, sessionId? }` to the parent server's
|
|
4
|
+
// `/api/wiki/internal/snapshot` endpoint so the snapshot pipeline
|
|
5
|
+
// (#763 PR 2) records the new state.
|
|
6
|
+
//
|
|
7
|
+
// THIS FILE IS THE SOURCE OF TRUTH. Edits here, then run
|
|
8
|
+
// `yarn build:hooks` (or `yarn build`) to regenerate
|
|
9
|
+
// `./snapshot.mjs`. The bundled `.mjs` is committed to git so
|
|
10
|
+
// `provision.ts` can read it at server startup without invoking
|
|
11
|
+
// esbuild on the runtime path. CI runs `yarn build:hooks &&
|
|
12
|
+
// git diff --exit-code` to catch a stale bundle.
|
|
13
|
+
//
|
|
14
|
+
// The hook executes inside Claude CLI's process space — must be
|
|
15
|
+
// self-contained ESM. esbuild bundles imports from the shared
|
|
16
|
+
// `src/lib/wiki-page/slug.ts` module so the hook and the
|
|
17
|
+
// server-side `classifyAsWikiPage` agree on what counts as a
|
|
18
|
+
// wiki page (no copy-paste drift).
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { wikiSlugFromAbsPath } from "../../../../src/lib/wiki-page/slug.js";
|
|
24
|
+
|
|
25
|
+
// Workspace layout matches `WORKSPACE_DIRS.wikiPages`. We hard-
|
|
26
|
+
// code "data/wiki/pages" rather than importing the constants
|
|
27
|
+
// because the hook needs to stay independent of the server's
|
|
28
|
+
// runtime config — the bundle must work even when the server
|
|
29
|
+
// process isn't running yet (e.g. during a startup race).
|
|
30
|
+
const WORKSPACE_ROOT = path.join(homedir(), "mulmoclaude");
|
|
31
|
+
const WIKI_PAGES_DIR = path.join(WORKSPACE_ROOT, "data", "wiki", "pages");
|
|
32
|
+
const TOKEN_PATH = path.join(WORKSPACE_ROOT, ".session-token");
|
|
33
|
+
const PORT_PATH = path.join(WORKSPACE_ROOT, ".server-port");
|
|
34
|
+
// In Docker mode the parent server runs on the host's 127.0.0.1
|
|
35
|
+
// which the container can't reach via plain loopback. The Docker
|
|
36
|
+
// spawn plumbing sets MULMOCLAUDE_HOST=host.docker.internal so
|
|
37
|
+
// fetch() resolves to the host server. Outside Docker (or when
|
|
38
|
+
// the var is unset) we fall back to the loopback address.
|
|
39
|
+
const SERVER_HOST = process.env.MULMOCLAUDE_HOST ?? "127.0.0.1";
|
|
40
|
+
|
|
41
|
+
interface HookPayload {
|
|
42
|
+
tool_input?: { file_path?: unknown };
|
|
43
|
+
tool_response?: { filePath?: unknown };
|
|
44
|
+
session_id?: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readStdin(): Promise<HookPayload | null> {
|
|
48
|
+
const chunks: Buffer[] = [];
|
|
49
|
+
for await (const chunk of process.stdin) {
|
|
50
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
51
|
+
}
|
|
52
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
53
|
+
if (!raw.trim()) return null;
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(raw) as HookPayload;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractFilePath(payload: HookPayload): string | null {
|
|
62
|
+
// Claude CLI hook payload: { tool_name, tool_input, tool_response, ... }
|
|
63
|
+
// Different tools surface the path under different keys —
|
|
64
|
+
// check both shapes defensively.
|
|
65
|
+
const fromInput = payload.tool_input?.file_path;
|
|
66
|
+
if (typeof fromInput === "string") return fromInput;
|
|
67
|
+
const fromResponse = payload.tool_response?.filePath;
|
|
68
|
+
if (typeof fromResponse === "string") return fromResponse;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readTokenSafe(): string {
|
|
73
|
+
try {
|
|
74
|
+
return readFileSync(TOKEN_PATH, "utf-8").trim();
|
|
75
|
+
} catch {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readPortSafe(): number | null {
|
|
81
|
+
try {
|
|
82
|
+
const raw = readFileSync(PORT_PATH, "utf-8").trim();
|
|
83
|
+
const port = Number.parseInt(raw, 10);
|
|
84
|
+
return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function main(): Promise<void> {
|
|
91
|
+
const payload = await readStdin();
|
|
92
|
+
if (!payload) return;
|
|
93
|
+
|
|
94
|
+
const filePath = extractFilePath(payload);
|
|
95
|
+
if (!filePath) return;
|
|
96
|
+
const slug = wikiSlugFromAbsPath(filePath, WIKI_PAGES_DIR);
|
|
97
|
+
if (slug === null) return;
|
|
98
|
+
|
|
99
|
+
const token = readTokenSafe();
|
|
100
|
+
const port = readPortSafe();
|
|
101
|
+
if (!token || port === null) return; // server isn't reachable; silent no-op
|
|
102
|
+
|
|
103
|
+
// Prefer our chat-session id from the spawn env (#963) — the
|
|
104
|
+
// server's session store keys by chatSessionId, not by Claude
|
|
105
|
+
// CLI's internal session_id, so the toolResult publish on the
|
|
106
|
+
// server side only matches when we forward our own id. Fall back
|
|
107
|
+
// to Claude CLI's `session_id` (still useful as a tracing token
|
|
108
|
+
// in the snapshot frontmatter) when the env var is absent — e.g.
|
|
109
|
+
// an older mulmoclaude server spawning a newer hook bundle.
|
|
110
|
+
const envChatSessionId = process.env.MULMOCLAUDE_CHAT_SESSION_ID;
|
|
111
|
+
const payloadSessionId = typeof payload.session_id === "string" && payload.session_id.length > 0 ? payload.session_id : undefined;
|
|
112
|
+
const sessionId = envChatSessionId && envChatSessionId.length > 0 ? envChatSessionId : payloadSessionId;
|
|
113
|
+
const body = sessionId === undefined ? { slug } : { slug, sessionId };
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await fetch(`http://${SERVER_HOST}:${port}/api/wiki/internal/snapshot`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
Authorization: `Bearer ${token}`,
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify(body),
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
// Network / server down — drop silently. The wiki page write
|
|
126
|
+
// itself already succeeded; missing one snapshot is recoverable
|
|
127
|
+
// (the next save will record the next state).
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main().catch(() => {
|
|
132
|
+
// Hooks run synchronously to the LLM tool flow — never throw.
|
|
133
|
+
// A busted hook would make `Write` itself look like it failed
|
|
134
|
+
// to the LLM, which is much worse than a missing snapshot.
|
|
135
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Workspace provisioning for the LLM wiki-write hook (#763 PR 2 prereq).
|
|
2
|
+
//
|
|
3
|
+
// At server startup, ensure the workspace's `.claude/settings.json`
|
|
4
|
+
// has a `PostToolUse` hook on Write|Edit that runs our snapshot
|
|
5
|
+
// script. The script itself is rewritten from the source-of-truth
|
|
6
|
+
// constant in `hookScript.ts` on every startup so updates ship via
|
|
7
|
+
// the normal mulmoclaude code path — no per-workspace migration.
|
|
8
|
+
//
|
|
9
|
+
// Idempotent: running provisioning twice produces the same on-disk
|
|
10
|
+
// state. Existing user-supplied keys in `settings.json` are
|
|
11
|
+
// preserved; we only touch the one PostToolUse entry that we own
|
|
12
|
+
// (identified by a `mulmoclaudeWikiHistory: true` marker on the
|
|
13
|
+
// hook descriptor).
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { readTextOrNull } from "../../utils/files/safe.js";
|
|
19
|
+
import { writeFileAtomic } from "../../utils/files/atomic.js";
|
|
20
|
+
import { workspacePath as defaultWorkspacePath } from "../workspace.js";
|
|
21
|
+
import { log } from "../../system/logger/index.js";
|
|
22
|
+
|
|
23
|
+
// The esbuild-built bundle is the source of truth for the hook
|
|
24
|
+
// script written into `<workspace>/.claude/hooks/`. Source TS is
|
|
25
|
+
// `hook/snapshot.ts`; `yarn build:hooks` (chained from
|
|
26
|
+
// `yarn build`) regenerates `hook/snapshot.mjs`, and CI fails
|
|
27
|
+
// when the committed bundle drifts from the source. The read
|
|
28
|
+
// happens inside `provisionWikiHistoryHook` (not at module load)
|
|
29
|
+
// so a missing / unreadable bundle degrades to a logged warning
|
|
30
|
+
// without breaking server startup — provisioning is best-effort
|
|
31
|
+
// and the rest of the server still functions (codex review
|
|
32
|
+
// iter-1 #955).
|
|
33
|
+
const HOOK_BUNDLE_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "hook", "snapshot.mjs");
|
|
34
|
+
|
|
35
|
+
function readHookBundle(): string | null {
|
|
36
|
+
try {
|
|
37
|
+
return readFileSync(HOOK_BUNDLE_PATH, "utf-8");
|
|
38
|
+
} catch (err) {
|
|
39
|
+
log.warn("wiki-history", "hook bundle unreadable, skipping provisioning", {
|
|
40
|
+
bundlePath: HOOK_BUNDLE_PATH,
|
|
41
|
+
error: err instanceof Error ? err.message : String(err),
|
|
42
|
+
});
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const SETTINGS_REL = path.join(".claude", "settings.json");
|
|
48
|
+
const HOOK_SCRIPT_REL = path.join(".claude", "hooks", "wiki-snapshot.mjs");
|
|
49
|
+
// Forward-slash form for the shell command field — settings.json
|
|
50
|
+
// is read by Claude CLI, and a hook command is interpreted by a
|
|
51
|
+
// POSIX shell (Mac / Linux / inside the Docker container). We never
|
|
52
|
+
// embed the workspace root directly: the same settings.json is
|
|
53
|
+
// bind-mounted into the container, so a host-absolute path
|
|
54
|
+
// (`/Users/.../mulmoclaude/...`) silently fails inside the container
|
|
55
|
+
// where the workspace lives at `/home/node/mulmoclaude/...`. Claude
|
|
56
|
+
// CLI exports `CLAUDE_PROJECT_DIR` to hooks, which resolves
|
|
57
|
+
// correctly in both contexts.
|
|
58
|
+
const HOOK_SCRIPT_REL_POSIX = ".claude/hooks/wiki-snapshot.mjs";
|
|
59
|
+
const HOOK_COMMAND = `node "$CLAUDE_PROJECT_DIR/${HOOK_SCRIPT_REL_POSIX}"`;
|
|
60
|
+
const OWNER_MARKER = "mulmoclaudeWikiHistory";
|
|
61
|
+
|
|
62
|
+
interface HookCommandEntry {
|
|
63
|
+
type: "command";
|
|
64
|
+
command: string;
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface HookMatcher {
|
|
69
|
+
matcher?: string;
|
|
70
|
+
hooks?: HookCommandEntry[];
|
|
71
|
+
[key: string]: unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface SettingsShape {
|
|
75
|
+
hooks?: {
|
|
76
|
+
PostToolUse?: HookMatcher[];
|
|
77
|
+
[key: string]: HookMatcher[] | undefined;
|
|
78
|
+
};
|
|
79
|
+
[key: string]: unknown;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ProvisionOptions {
|
|
83
|
+
workspaceRoot?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Ensure the hook script + `.claude/settings.json` are up to date.
|
|
87
|
+
* Safe to call on every startup. Logs a one-line info on first
|
|
88
|
+
* install, debug-only on subsequent no-op runs. Skips quietly
|
|
89
|
+
* (with a warning) when the bundled hook script can't be read
|
|
90
|
+
* — provisioning is best-effort and the rest of the server is
|
|
91
|
+
* still functional. */
|
|
92
|
+
export async function provisionWikiHistoryHook(opts: ProvisionOptions = {}): Promise<void> {
|
|
93
|
+
const bundle = readHookBundle();
|
|
94
|
+
if (bundle === null) return;
|
|
95
|
+
|
|
96
|
+
const root = opts.workspaceRoot ?? defaultWorkspacePath;
|
|
97
|
+
const scriptPath = path.join(root, HOOK_SCRIPT_REL);
|
|
98
|
+
const settingsPath = path.join(root, SETTINGS_REL);
|
|
99
|
+
|
|
100
|
+
await writeHookScript(scriptPath, bundle);
|
|
101
|
+
const changed = await mergeHookIntoSettings(settingsPath);
|
|
102
|
+
if (changed) {
|
|
103
|
+
log.info("wiki-history", "provisioned wiki-snapshot hook", { settingsPath, scriptPath });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function writeHookScript(absPath: string, bundle: string): Promise<void> {
|
|
108
|
+
// Always overwrite — the esbuild bundle on disk is the source of
|
|
109
|
+
// truth, and rewriting on every startup means a mulmoclaude
|
|
110
|
+
// update propagates without per-workspace migration.
|
|
111
|
+
await writeFileAtomic(absPath, bundle, { mode: 0o700 });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function mergeHookIntoSettings(settingsPath: string): Promise<boolean> {
|
|
115
|
+
const existingRaw = await readTextOrNull(settingsPath);
|
|
116
|
+
const existing: SettingsShape = existingRaw ? safeParse(existingRaw) : {};
|
|
117
|
+
|
|
118
|
+
const desiredHook: HookCommandEntry = {
|
|
119
|
+
type: "command",
|
|
120
|
+
command: HOOK_COMMAND,
|
|
121
|
+
[OWNER_MARKER]: true,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const next = upsertOurHook(existing, desiredHook);
|
|
125
|
+
const nextRaw = `${JSON.stringify(next, null, 2)}\n`;
|
|
126
|
+
if (existingRaw === nextRaw) return false;
|
|
127
|
+
|
|
128
|
+
// mode 0644 is fine — settings.json doesn't carry secrets, and
|
|
129
|
+
// matching the user's own conventions for `~/.claude/settings.json`
|
|
130
|
+
// (which is typically world-readable) avoids permission surprises.
|
|
131
|
+
await writeFileAtomic(settingsPath, nextRaw);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function safeParse(raw: string): SettingsShape {
|
|
136
|
+
try {
|
|
137
|
+
const parsed: unknown = JSON.parse(raw);
|
|
138
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
139
|
+
return parsed as SettingsShape;
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Fall through — corrupted settings get rebuilt with our entry only.
|
|
143
|
+
}
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function upsertOurHook(settings: SettingsShape, desiredHook: HookCommandEntry): SettingsShape {
|
|
148
|
+
const hooks = settings.hooks ?? {};
|
|
149
|
+
// Normalise to [] when an unexpected shape sits under `PostToolUse`
|
|
150
|
+
// (object, string, etc.). The `?? []` form only handles
|
|
151
|
+
// null/undefined; without this guard, `findIndex` throws and the
|
|
152
|
+
// hook silently fails to install for the session (codex iter-2 #917).
|
|
153
|
+
const rawPostToolUse = hooks.PostToolUse;
|
|
154
|
+
const postToolUse = Array.isArray(rawPostToolUse) ? rawPostToolUse : [];
|
|
155
|
+
|
|
156
|
+
const ownedIndex = postToolUse.findIndex((entry) => entryHasOwnedHook(entry));
|
|
157
|
+
const desiredEntry: HookMatcher = {
|
|
158
|
+
matcher: "Write|Edit",
|
|
159
|
+
hooks: [desiredHook],
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const nextPostToolUse = [...postToolUse];
|
|
163
|
+
if (ownedIndex === -1) {
|
|
164
|
+
nextPostToolUse.push(desiredEntry);
|
|
165
|
+
} else {
|
|
166
|
+
nextPostToolUse[ownedIndex] = desiredEntry;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
...settings,
|
|
171
|
+
hooks: {
|
|
172
|
+
...hooks,
|
|
173
|
+
PostToolUse: nextPostToolUse,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function entryHasOwnedHook(entry: HookMatcher): boolean {
|
|
179
|
+
if (!Array.isArray(entry.hooks)) return false;
|
|
180
|
+
return entry.hooks.some((hook) => hook[OWNER_MARKER] === true);
|
|
181
|
+
}
|