mulmoclaude 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/bin/mulmoclaude.js +202 -0
- package/bin/prepare-dist.js +93 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
- package/client/assets/index-D8rhwXLq.js +4906 -0
- package/client/assets/index-KNLBjwuh.css +1 -0
- package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
- package/client/assets/material-icons-Dr0goTwe.woff +0 -0
- package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
- package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
- package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
- package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
- package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
- package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
- package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
- package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
- package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
- package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
- package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
- package/client/index.html +28 -0
- package/package.json +66 -0
- package/server/agent/attachmentConverter.ts +270 -0
- package/server/agent/config.ts +414 -0
- package/server/agent/index.ts +260 -0
- package/server/agent/mcp-server.ts +412 -0
- package/server/agent/mcp-tools/index.ts +63 -0
- package/server/agent/mcp-tools/x.ts +188 -0
- package/server/agent/plugin-names.ts +75 -0
- package/server/agent/prompt.ts +349 -0
- package/server/agent/resumeFailover.ts +129 -0
- package/server/agent/sandboxMounts.ts +329 -0
- package/server/agent/stream.ts +194 -0
- package/server/api/auth/bearerAuth.ts +61 -0
- package/server/api/auth/token.ts +98 -0
- package/server/api/csrfGuard.ts +85 -0
- package/server/api/routes/agent.ts +478 -0
- package/server/api/routes/chart.ts +98 -0
- package/server/api/routes/chat-index.ts +46 -0
- package/server/api/routes/config.ts +258 -0
- package/server/api/routes/dispatchResponse.ts +79 -0
- package/server/api/routes/files.ts +812 -0
- package/server/api/routes/html.ts +101 -0
- package/server/api/routes/image.ts +169 -0
- package/server/api/routes/mulmo-script.ts +712 -0
- package/server/api/routes/mulmoScriptValidate.ts +101 -0
- package/server/api/routes/notifications.ts +69 -0
- package/server/api/routes/pdf.ts +163 -0
- package/server/api/routes/plugins.ts +276 -0
- package/server/api/routes/presentHtml.ts +48 -0
- package/server/api/routes/roles.ts +125 -0
- package/server/api/routes/scheduler.ts +153 -0
- package/server/api/routes/schedulerHandlers.ts +151 -0
- package/server/api/routes/schedulerTasks.ts +163 -0
- package/server/api/routes/sessions.ts +294 -0
- package/server/api/routes/sessionsCursor.ts +59 -0
- package/server/api/routes/skills.ts +195 -0
- package/server/api/routes/sources.ts +540 -0
- package/server/api/routes/todos.ts +263 -0
- package/server/api/routes/todosColumnsHandlers.ts +347 -0
- package/server/api/routes/todosHandlers.ts +274 -0
- package/server/api/routes/todosItemsHandlers.ts +386 -0
- package/server/api/routes/wiki/pageIndex.ts +53 -0
- package/server/api/routes/wiki.ts +363 -0
- package/server/api/sandboxStatus.ts +64 -0
- package/server/events/notifications.ts +160 -0
- package/server/events/pub-sub/index.ts +45 -0
- package/server/events/relay-client.ts +288 -0
- package/server/events/scheduler-adapter.ts +302 -0
- package/server/events/session-store/index.ts +492 -0
- package/server/events/task-manager/index.ts +181 -0
- package/server/index.ts +572 -0
- package/server/system/config.ts +243 -0
- package/server/system/credentials.ts +220 -0
- package/server/system/docker.ts +97 -0
- package/server/system/env.ts +109 -0
- package/server/system/logger/config.ts +112 -0
- package/server/system/logger/formatters.ts +40 -0
- package/server/system/logger/index.ts +53 -0
- package/server/system/logger/rotation.ts +37 -0
- package/server/system/logger/sinks.ts +101 -0
- package/server/system/logger/types.ts +29 -0
- package/server/utils/date.ts +57 -0
- package/server/utils/errors.ts +7 -0
- package/server/utils/fetch.ts +27 -0
- package/server/utils/files/atomic.ts +125 -0
- package/server/utils/files/html-io.ts +20 -0
- package/server/utils/files/image-store.ts +66 -0
- package/server/utils/files/index.ts +45 -0
- package/server/utils/files/journal-io.ts +213 -0
- package/server/utils/files/json.ts +69 -0
- package/server/utils/files/markdown-store.ts +33 -0
- package/server/utils/files/naming.ts +50 -0
- package/server/utils/files/reference-dirs-io.ts +45 -0
- package/server/utils/files/roles-io.ts +45 -0
- package/server/utils/files/safe.ts +106 -0
- package/server/utils/files/scheduler-io.ts +20 -0
- package/server/utils/files/scheduler-overrides-io.ts +64 -0
- package/server/utils/files/session-io.ts +136 -0
- package/server/utils/files/spreadsheet-store.ts +63 -0
- package/server/utils/files/todos-io.ts +29 -0
- package/server/utils/files/user-tasks-io.ts +25 -0
- package/server/utils/files/workspace-io.ts +221 -0
- package/server/utils/gemini.ts +59 -0
- package/server/utils/gitignore.ts +69 -0
- package/server/utils/http.ts +15 -0
- package/server/utils/httpError.ts +61 -0
- package/server/utils/id.ts +16 -0
- package/server/utils/json.ts +83 -0
- package/server/utils/logBackgroundError.ts +22 -0
- package/server/utils/markdown.ts +82 -0
- package/server/utils/request.ts +29 -0
- package/server/utils/slug.ts +50 -0
- package/server/utils/spawn.ts +62 -0
- package/server/utils/time.ts +34 -0
- package/server/utils/types.ts +47 -0
- package/server/workspace/chat-index/index.ts +153 -0
- package/server/workspace/chat-index/indexer.ts +209 -0
- package/server/workspace/chat-index/paths.ts +34 -0
- package/server/workspace/chat-index/summarizer.ts +247 -0
- package/server/workspace/chat-index/types.ts +38 -0
- package/server/workspace/custom-dirs.ts +220 -0
- package/server/workspace/helps/business.md +104 -0
- package/server/workspace/helps/github.md +23 -0
- package/server/workspace/helps/index.md +60 -0
- package/server/workspace/helps/mulmoscript.md +249 -0
- package/server/workspace/helps/sandbox.md +90 -0
- package/server/workspace/helps/spreadsheet.md +43 -0
- package/server/workspace/helps/telegram.md +135 -0
- package/server/workspace/helps/wiki.md +131 -0
- package/server/workspace/journal/archivist.ts +386 -0
- package/server/workspace/journal/dailyPass.ts +743 -0
- package/server/workspace/journal/diff.ts +71 -0
- package/server/workspace/journal/index.ts +185 -0
- package/server/workspace/journal/indexFile.ts +136 -0
- package/server/workspace/journal/linkRewrite.ts +4 -0
- package/server/workspace/journal/memoryExtractor.ts +130 -0
- package/server/workspace/journal/optimizationPass.ts +160 -0
- package/server/workspace/journal/paths.ts +76 -0
- package/server/workspace/journal/state.ts +125 -0
- package/server/workspace/paths.ts +158 -0
- package/server/workspace/reference-dirs.ts +252 -0
- package/server/workspace/roles.ts +37 -0
- package/server/workspace/skills/discovery.ts +125 -0
- package/server/workspace/skills/index.ts +10 -0
- package/server/workspace/skills/parser.ts +144 -0
- package/server/workspace/skills/paths.ts +41 -0
- package/server/workspace/skills/scheduler.ts +149 -0
- package/server/workspace/skills/types.ts +30 -0
- package/server/workspace/skills/user-tasks.ts +257 -0
- package/server/workspace/skills/writer.ts +189 -0
- package/server/workspace/sources/arxivDiscovery.ts +182 -0
- package/server/workspace/sources/classifier.ts +268 -0
- package/server/workspace/sources/fetchers/arxiv.ts +170 -0
- package/server/workspace/sources/fetchers/github.ts +106 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
- package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
- package/server/workspace/sources/fetchers/index.ts +71 -0
- package/server/workspace/sources/fetchers/registerAll.ts +15 -0
- package/server/workspace/sources/fetchers/rss.ts +141 -0
- package/server/workspace/sources/fetchers/rssParser.ts +295 -0
- package/server/workspace/sources/httpFetcher.ts +230 -0
- package/server/workspace/sources/interests.ts +120 -0
- package/server/workspace/sources/paths.ts +110 -0
- package/server/workspace/sources/pipeline/dedup.ts +60 -0
- package/server/workspace/sources/pipeline/fetch.ts +136 -0
- package/server/workspace/sources/pipeline/index.ts +249 -0
- package/server/workspace/sources/pipeline/notify.ts +72 -0
- package/server/workspace/sources/pipeline/plan.ts +66 -0
- package/server/workspace/sources/pipeline/summarize.ts +189 -0
- package/server/workspace/sources/pipeline/write.ts +185 -0
- package/server/workspace/sources/rateLimiter.ts +148 -0
- package/server/workspace/sources/registry.ts +326 -0
- package/server/workspace/sources/robots.ts +271 -0
- package/server/workspace/sources/sourceState.ts +135 -0
- package/server/workspace/sources/taxonomy.ts +74 -0
- package/server/workspace/sources/types.ts +144 -0
- package/server/workspace/sources/urls.ts +112 -0
- package/server/workspace/tool-trace/classify.ts +114 -0
- package/server/workspace/tool-trace/index.ts +250 -0
- package/server/workspace/tool-trace/writeSearch.ts +98 -0
- package/server/workspace/wiki-backlinks/index.ts +107 -0
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
- package/server/workspace/workspace.ts +66 -0
- package/src/App.vue +720 -0
- package/src/assets/mulmo_bw.png +0 -0
- package/src/components/CanvasViewToggle.vue +27 -0
- package/src/components/ChatAttachmentPreview.vue +45 -0
- package/src/components/ChatImagePreview.vue +17 -0
- package/src/components/ChatInput.vue +208 -0
- package/src/components/FileContentHeader.vue +49 -0
- package/src/components/FileContentRenderer.vue +162 -0
- package/src/components/FileTree.vue +115 -0
- package/src/components/FileTreePane.vue +85 -0
- package/src/components/FilesView.vue +206 -0
- package/src/components/LockStatusPopup.vue +111 -0
- package/src/components/NotificationBell.vue +131 -0
- package/src/components/NotificationToast.vue +72 -0
- package/src/components/PluginLauncher.vue +138 -0
- package/src/components/RightSidebar.vue +113 -0
- package/src/components/RoleSelector.vue +64 -0
- package/src/components/SessionHistoryPanel.vue +176 -0
- package/src/components/SessionTabBar.vue +81 -0
- package/src/components/SettingsMcpTab.vue +350 -0
- package/src/components/SettingsModal.vue +275 -0
- package/src/components/SettingsReferenceDirsTab.vue +173 -0
- package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
- package/src/components/SidebarHeader.vue +69 -0
- package/src/components/StackView.vue +360 -0
- package/src/components/SuggestionsPanel.vue +65 -0
- package/src/components/TodoExplorer.vue +358 -0
- package/src/components/ToolResultsPanel.vue +77 -0
- package/src/components/todo/TodoAddDialog.vue +131 -0
- package/src/components/todo/TodoEditDialog.vue +47 -0
- package/src/components/todo/TodoEditPanel.vue +113 -0
- package/src/components/todo/TodoKanbanView.vue +249 -0
- package/src/components/todo/TodoListView.vue +79 -0
- package/src/components/todo/TodoTableView.vue +177 -0
- package/src/composables/useActiveSession.ts +40 -0
- package/src/composables/useAppApi.ts +45 -0
- package/src/composables/useCanvasViewMode.ts +121 -0
- package/src/composables/useChatScroll.ts +47 -0
- package/src/composables/useClickOutside.ts +26 -0
- package/src/composables/useClipboardCopy.ts +44 -0
- package/src/composables/useContentDisplay.ts +52 -0
- package/src/composables/useDebugBeat.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +115 -0
- package/src/composables/useEventListeners.ts +42 -0
- package/src/composables/useExpandedDirs.ts +64 -0
- package/src/composables/useFaviconState.ts +30 -0
- package/src/composables/useFileSelection.ts +115 -0
- package/src/composables/useFileSortMode.ts +24 -0
- package/src/composables/useFileTree.ts +85 -0
- package/src/composables/useFreshPluginData.ts +89 -0
- package/src/composables/useHealth.ts +38 -0
- package/src/composables/useImeAwareEnter.ts +57 -0
- package/src/composables/useKeyNavigation.ts +60 -0
- package/src/composables/useMarkdownLinkHandler.ts +46 -0
- package/src/composables/useMarkdownMode.ts +17 -0
- package/src/composables/useMcpTools.ts +71 -0
- package/src/composables/useMergedSessions.ts +27 -0
- package/src/composables/useNotifications.ts +90 -0
- package/src/composables/usePdfDownload.ts +60 -0
- package/src/composables/usePendingCalls.ts +77 -0
- package/src/composables/usePubSub.ts +85 -0
- package/src/composables/useRightSidebar.ts +23 -0
- package/src/composables/useRoles.ts +34 -0
- package/src/composables/useSandboxStatus.ts +67 -0
- package/src/composables/useSelectedResult.ts +49 -0
- package/src/composables/useSessionDerived.ts +51 -0
- package/src/composables/useSessionHistory.ts +81 -0
- package/src/composables/useSessionSync.ts +57 -0
- package/src/composables/useViewLayout.ts +55 -0
- package/src/config/apiRoutes.ts +173 -0
- package/src/config/pubsubChannels.ts +45 -0
- package/src/config/roles.ts +335 -0
- package/src/config/schedulerActions.ts +25 -0
- package/src/config/toolNames.ts +71 -0
- package/src/config/workspacePaths.ts +24 -0
- package/src/index.css +107 -0
- package/src/main.ts +25 -0
- package/src/plugins/canvas/Preview.vue +13 -0
- package/src/plugins/canvas/View.vue +333 -0
- package/src/plugins/canvas/definition.ts +38 -0
- package/src/plugins/canvas/index.ts +36 -0
- package/src/plugins/chart/Preview.vue +49 -0
- package/src/plugins/chart/View.vue +143 -0
- package/src/plugins/chart/definition.ts +58 -0
- package/src/plugins/chart/index.ts +52 -0
- package/src/plugins/editImage/Preview.vue +13 -0
- package/src/plugins/editImage/View.vue +13 -0
- package/src/plugins/editImage/definition.ts +27 -0
- package/src/plugins/editImage/index.ts +36 -0
- package/src/plugins/generateImage/Preview.vue +13 -0
- package/src/plugins/generateImage/View.vue +33 -0
- package/src/plugins/generateImage/definition.ts +32 -0
- package/src/plugins/generateImage/index.ts +56 -0
- package/src/plugins/manageRoles/Preview.vue +49 -0
- package/src/plugins/manageRoles/View.vue +525 -0
- package/src/plugins/manageRoles/definition.ts +43 -0
- package/src/plugins/manageRoles/index.ts +47 -0
- package/src/plugins/manageSkills/Preview.vue +21 -0
- package/src/plugins/manageSkills/View.vue +321 -0
- package/src/plugins/manageSkills/definition.ts +49 -0
- package/src/plugins/manageSkills/index.ts +49 -0
- package/src/plugins/manageSource/Preview.vue +33 -0
- package/src/plugins/manageSource/View.vue +697 -0
- package/src/plugins/manageSource/definition.ts +63 -0
- package/src/plugins/manageSource/index.ts +66 -0
- package/src/plugins/markdown/Preview.vue +77 -0
- package/src/plugins/markdown/View.vue +476 -0
- package/src/plugins/markdown/definition.ts +50 -0
- package/src/plugins/markdown/index.ts +36 -0
- package/src/plugins/presentHtml/Preview.vue +25 -0
- package/src/plugins/presentHtml/View.vue +52 -0
- package/src/plugins/presentHtml/definition.ts +27 -0
- package/src/plugins/presentHtml/helpers.ts +72 -0
- package/src/plugins/presentHtml/index.ts +41 -0
- package/src/plugins/presentMulmoScript/Preview.vue +23 -0
- package/src/plugins/presentMulmoScript/View.vue +1166 -0
- package/src/plugins/presentMulmoScript/definition.ts +95 -0
- package/src/plugins/presentMulmoScript/helpers.ts +162 -0
- package/src/plugins/presentMulmoScript/index.ts +40 -0
- package/src/plugins/scheduler/Preview.vue +67 -0
- package/src/plugins/scheduler/TasksTab.vue +205 -0
- package/src/plugins/scheduler/View.vue +565 -0
- package/src/plugins/scheduler/definition.ts +57 -0
- package/src/plugins/scheduler/index.ts +45 -0
- package/src/plugins/scheduler/viewModes.ts +26 -0
- package/src/plugins/spreadsheet/Preview.vue +29 -0
- package/src/plugins/spreadsheet/View.vue +997 -0
- package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
- package/src/plugins/spreadsheet/definition.ts +121 -0
- package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
- package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
- package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
- package/src/plugins/spreadsheet/engine/engine.ts +176 -0
- package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
- package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
- package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
- package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
- package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
- package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
- package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
- package/src/plugins/spreadsheet/engine/index.ts +27 -0
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
- package/src/plugins/spreadsheet/engine/parser.ts +143 -0
- package/src/plugins/spreadsheet/engine/registry.ts +150 -0
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
- package/src/plugins/spreadsheet/engine/types.ts +64 -0
- package/src/plugins/spreadsheet/index.ts +36 -0
- package/src/plugins/textResponse/Preview.vue +94 -0
- package/src/plugins/textResponse/View.vue +503 -0
- package/src/plugins/textResponse/definition.ts +34 -0
- package/src/plugins/textResponse/index.ts +27 -0
- package/src/plugins/textResponse/plugin.ts +29 -0
- package/src/plugins/textResponse/samples.ts +97 -0
- package/src/plugins/textResponse/types.ts +11 -0
- package/src/plugins/todo/Preview.vue +63 -0
- package/src/plugins/todo/View.vue +364 -0
- package/src/plugins/todo/composables/useTodos.ts +177 -0
- package/src/plugins/todo/definition.ts +45 -0
- package/src/plugins/todo/index.ts +61 -0
- package/src/plugins/todo/labels.ts +163 -0
- package/src/plugins/todo/priority.ts +98 -0
- package/src/plugins/todo/viewModes.ts +19 -0
- package/src/plugins/ui-image/ImagePreview.vue +23 -0
- package/src/plugins/ui-image/ImageView.vue +34 -0
- package/src/plugins/ui-image/index.ts +3 -0
- package/src/plugins/ui-image/types.ts +4 -0
- package/src/plugins/wiki/Preview.vue +65 -0
- package/src/plugins/wiki/View.vue +342 -0
- package/src/plugins/wiki/definition.ts +25 -0
- package/src/plugins/wiki/helpers.ts +59 -0
- package/src/plugins/wiki/index.ts +52 -0
- package/src/router/guards.ts +61 -0
- package/src/router/index.ts +50 -0
- package/src/tools/index.ts +52 -0
- package/src/tools/types.ts +27 -0
- package/src/types/events.ts +16 -0
- package/src/types/fileTree.ts +13 -0
- package/src/types/notification.ts +67 -0
- package/src/types/session.ts +116 -0
- package/src/types/sse.ts +90 -0
- package/src/types/toolCallHistory.ts +13 -0
- package/src/utils/agent/eventDispatch.ts +74 -0
- package/src/utils/agent/request.ts +55 -0
- package/src/utils/agent/toolCalls.ts +62 -0
- package/src/utils/api.ts +218 -0
- package/src/utils/canvas/viewMode.ts +46 -0
- package/src/utils/dom/authTokenMeta.ts +20 -0
- package/src/utils/dom/clickOutside.ts +11 -0
- package/src/utils/dom/externalLink.ts +57 -0
- package/src/utils/dom/scrollable.ts +24 -0
- package/src/utils/errors.ts +11 -0
- package/src/utils/files/expandedDirs.ts +25 -0
- package/src/utils/files/filename.ts +12 -0
- package/src/utils/files/sortChildren.ts +20 -0
- package/src/utils/filesPreview/schedulerPreview.ts +38 -0
- package/src/utils/filesPreview/todoPreview.ts +40 -0
- package/src/utils/format/date.ts +85 -0
- package/src/utils/format/frontmatter.ts +80 -0
- package/src/utils/format/jsonSyntax.ts +109 -0
- package/src/utils/html/previewCsp.ts +65 -0
- package/src/utils/image/resolve.ts +8 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
- package/src/utils/markdown/extractFirstH1.ts +39 -0
- package/src/utils/notification/dispatch.ts +22 -0
- package/src/utils/path/relativeLink.ts +130 -0
- package/src/utils/role/icon.ts +20 -0
- package/src/utils/role/merge.ts +10 -0
- package/src/utils/role/plugins.ts +12 -0
- package/src/utils/session/mergeSessions.ts +103 -0
- package/src/utils/session/seedRoleDefault.ts +35 -0
- package/src/utils/session/sessionEntries.ts +121 -0
- package/src/utils/session/sessionFactory.ts +22 -0
- package/src/utils/session/sessionHelpers.ts +99 -0
- package/src/utils/tools/dedup.ts +17 -0
- package/src/utils/tools/mcp.ts +33 -0
- package/src/utils/tools/pendingCalls.ts +16 -0
- package/src/utils/tools/result.ts +40 -0
- package/src/utils/types.ts +44 -0
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
import path from "node:path";
|
|
6
|
+
import { WORKSPACE_DIRS } from "../paths.js";
|
|
7
|
+
import { isValidIsoDate } from "../../utils/date.js";
|
|
8
|
+
|
|
9
|
+
// Directory layout under workspace/conversations/summaries/ is an
|
|
10
|
+
// implementation detail of the journal module; keep it centralised
|
|
11
|
+
// here so tests and callers all agree on the structure.
|
|
12
|
+
export const SUMMARIES_DIR = WORKSPACE_DIRS.summaries;
|
|
13
|
+
export const STATE_FILE = "_state.json";
|
|
14
|
+
export const INDEX_FILE = "_index.md";
|
|
15
|
+
export const DAILY_DIR = "daily";
|
|
16
|
+
export const TOPICS_DIR = "topics";
|
|
17
|
+
export const ARCHIVE_DIR = "archive";
|
|
18
|
+
|
|
19
|
+
// Absolute path to the summaries root inside a workspace.
|
|
20
|
+
export function summariesRoot(workspaceRoot: string): string {
|
|
21
|
+
return path.join(workspaceRoot, SUMMARIES_DIR);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// summaries/daily/YYYY/MM/DD.md for a given ISO-ish date ("YYYY-MM-DD").
|
|
25
|
+
// Throws if `isoDate` is not exactly YYYY-MM-DD — catches typos at
|
|
26
|
+
// the boundary instead of producing "undefined/undefined.md" paths
|
|
27
|
+
// downstream.
|
|
28
|
+
export function dailyPathFor(workspaceRoot: string, isoDate: string): string {
|
|
29
|
+
if (!isValidIsoDate(isoDate)) {
|
|
30
|
+
throw new Error(`[journal] dailyPathFor: expected YYYY-MM-DD, got "${isoDate}"`);
|
|
31
|
+
}
|
|
32
|
+
const [year, month, day] = isoDate.split("-");
|
|
33
|
+
return path.join(summariesRoot(workspaceRoot), DAILY_DIR, year, month, `${day}.md`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// summaries/topics/<slug>.md
|
|
37
|
+
export function topicPathFor(workspaceRoot: string, slug: string): string {
|
|
38
|
+
return path.join(summariesRoot(workspaceRoot), TOPICS_DIR, `${slug}.md`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// summaries/archive/topics/<slug>.md — where the optimizer moves
|
|
42
|
+
// merged or stale topic files.
|
|
43
|
+
export function archivedTopicPathFor(workspaceRoot: string, slug: string): string {
|
|
44
|
+
return path.join(summariesRoot(workspaceRoot), ARCHIVE_DIR, TOPICS_DIR, `${slug}.md`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Re-export for backwards compatibility — callers that import
|
|
48
|
+
// toIsoDate from journal/paths keep working.
|
|
49
|
+
export { toLocalIsoDate as toIsoDate } from "../../utils/date.js";
|
|
50
|
+
|
|
51
|
+
// Convert a free-form topic name into a filesystem-safe slug.
|
|
52
|
+
// Rules:
|
|
53
|
+
// - Lowercase ASCII letters, digits, and hyphens only
|
|
54
|
+
// - Whitespace and punctuation collapse to a single hyphen
|
|
55
|
+
// - Non-ASCII characters (Japanese, emoji) are dropped; if the
|
|
56
|
+
// result is empty we fall back to "topic" so we always yield a
|
|
57
|
+
// valid filename (LLMs occasionally emit pure-Japanese topic
|
|
58
|
+
// names; the markdown body still holds the original title for
|
|
59
|
+
// display, this slug is only the filesystem key)
|
|
60
|
+
// - Leading/trailing hyphens stripped
|
|
61
|
+
// - Empty-string input yields "topic"
|
|
62
|
+
export function slugify(raw: string): string {
|
|
63
|
+
const lowered = raw.toLowerCase();
|
|
64
|
+
// Replace runs of non-ASCII-alnum with a single hyphen. Because
|
|
65
|
+
// we use `+` on a character class, this single pass already
|
|
66
|
+
// collapses runs — no second dedupe pass needed.
|
|
67
|
+
const hyphenated = lowered.replace(/[^a-z0-9]+/g, "-");
|
|
68
|
+
// Trim leading/trailing hyphens without a regex — sonarjs/slow-regex
|
|
69
|
+
// flags `^-+` / `-+$` patterns even though these inputs are tiny.
|
|
70
|
+
let start = 0;
|
|
71
|
+
let end = hyphenated.length;
|
|
72
|
+
while (start < end && hyphenated[start] === "-") start++;
|
|
73
|
+
while (end > start && hyphenated[end - 1] === "-") end--;
|
|
74
|
+
const trimmed = hyphenated.slice(start, end);
|
|
75
|
+
return trimmed.length > 0 ? trimmed : "topic";
|
|
76
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
import {
|
|
11
|
+
readJournalState as readJournalStateRaw,
|
|
12
|
+
writeJournalState as writeJournalStateRaw,
|
|
13
|
+
journalStateExists as journalStateExistsRaw,
|
|
14
|
+
} from "../../utils/files/journal-io.js";
|
|
15
|
+
import { ONE_HOUR_MS, ONE_DAY_MS } from "../../utils/time.js";
|
|
16
|
+
import { isRecord } from "../../utils/types.js";
|
|
17
|
+
|
|
18
|
+
// Bump this when the schema changes in a backwards-incompatible way.
|
|
19
|
+
// Older state files are treated as corrupted and replaced with a
|
|
20
|
+
// fresh default (ingest everything from scratch) — cheap because it
|
|
21
|
+
// only costs one extra archivist pass.
|
|
22
|
+
export const JOURNAL_STATE_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
export interface ProcessedSessionRecord {
|
|
25
|
+
// mtime (ms since epoch) of the session's .jsonl file when we
|
|
26
|
+
// last ingested it. If mtime advances on the next run, the session
|
|
27
|
+
// has appended events and needs re-ingest.
|
|
28
|
+
lastMtimeMs: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface JournalState {
|
|
32
|
+
version: number;
|
|
33
|
+
lastDailyRunAt: string | null;
|
|
34
|
+
lastOptimizationRunAt: string | null;
|
|
35
|
+
dailyIntervalHours: number;
|
|
36
|
+
optimizationIntervalDays: number;
|
|
37
|
+
processedSessions: Record<string, ProcessedSessionRecord>;
|
|
38
|
+
knownTopics: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const DEFAULT_DAILY_INTERVAL_HOURS = 1;
|
|
42
|
+
export const DEFAULT_OPTIMIZATION_INTERVAL_DAYS = 7;
|
|
43
|
+
|
|
44
|
+
// --- Pure helpers (unit-testable without disk) ---------------------
|
|
45
|
+
|
|
46
|
+
export function defaultState(): JournalState {
|
|
47
|
+
return {
|
|
48
|
+
version: JOURNAL_STATE_VERSION,
|
|
49
|
+
lastDailyRunAt: null,
|
|
50
|
+
lastOptimizationRunAt: null,
|
|
51
|
+
dailyIntervalHours: DEFAULT_DAILY_INTERVAL_HOURS,
|
|
52
|
+
optimizationIntervalDays: DEFAULT_OPTIMIZATION_INTERVAL_DAYS,
|
|
53
|
+
processedSessions: {},
|
|
54
|
+
knownTopics: [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Narrow an `unknown` into a JournalState. Accepts partial / missing
|
|
59
|
+
// fields and fills defaults — users can hand-edit the file to change
|
|
60
|
+
// intervals and we want to be forgiving.
|
|
61
|
+
export function parseState(raw: unknown): JournalState {
|
|
62
|
+
if (!isRecord(raw)) return defaultState();
|
|
63
|
+
const obj = raw as Record<string, unknown>;
|
|
64
|
+
|
|
65
|
+
// Version mismatch → throw it all out. Cheap to rebuild.
|
|
66
|
+
if (obj.version !== JOURNAL_STATE_VERSION) return defaultState();
|
|
67
|
+
|
|
68
|
+
const d = defaultState();
|
|
69
|
+
return {
|
|
70
|
+
version: JOURNAL_STATE_VERSION,
|
|
71
|
+
lastDailyRunAt: typeof obj.lastDailyRunAt === "string" ? obj.lastDailyRunAt : null,
|
|
72
|
+
lastOptimizationRunAt: typeof obj.lastOptimizationRunAt === "string" ? obj.lastOptimizationRunAt : null,
|
|
73
|
+
dailyIntervalHours: typeof obj.dailyIntervalHours === "number" && obj.dailyIntervalHours > 0 ? obj.dailyIntervalHours : d.dailyIntervalHours,
|
|
74
|
+
optimizationIntervalDays:
|
|
75
|
+
typeof obj.optimizationIntervalDays === "number" && obj.optimizationIntervalDays > 0 ? obj.optimizationIntervalDays : d.optimizationIntervalDays,
|
|
76
|
+
processedSessions: parseProcessedSessions(obj.processedSessions),
|
|
77
|
+
knownTopics: Array.isArray(obj.knownTopics) ? obj.knownTopics.filter((t): t is string => typeof t === "string") : [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseProcessedSessions(raw: unknown): Record<string, ProcessedSessionRecord> {
|
|
82
|
+
if (!isRecord(raw)) return {};
|
|
83
|
+
const out: Record<string, ProcessedSessionRecord> = {};
|
|
84
|
+
for (const [id, rec] of Object.entries(raw as Record<string, unknown>)) {
|
|
85
|
+
if (!isRecord(rec)) continue;
|
|
86
|
+
const mtime = (rec as Record<string, unknown>).lastMtimeMs;
|
|
87
|
+
if (typeof mtime === "number" && mtime >= 0) {
|
|
88
|
+
out[id] = { lastMtimeMs: mtime };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Has the configured daily interval elapsed since the last run? A
|
|
95
|
+
// null lastDailyRunAt means "never run" → always due.
|
|
96
|
+
export function isDailyDue(state: JournalState, nowMs: number): boolean {
|
|
97
|
+
if (state.lastDailyRunAt === null) return true;
|
|
98
|
+
const last = Date.parse(state.lastDailyRunAt);
|
|
99
|
+
if (Number.isNaN(last)) return true;
|
|
100
|
+
const intervalMs = state.dailyIntervalHours * ONE_HOUR_MS;
|
|
101
|
+
return nowMs - last >= intervalMs;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function isOptimizationDue(state: JournalState, nowMs: number): boolean {
|
|
105
|
+
if (state.lastOptimizationRunAt === null) return true;
|
|
106
|
+
const last = Date.parse(state.lastOptimizationRunAt);
|
|
107
|
+
if (Number.isNaN(last)) return true;
|
|
108
|
+
const intervalMs = state.optimizationIntervalDays * ONE_DAY_MS;
|
|
109
|
+
return nowMs - last >= intervalMs;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- Filesystem helpers (delegated to journal-io) --------------------
|
|
113
|
+
|
|
114
|
+
export async function readState(workspaceRoot: string): Promise<JournalState> {
|
|
115
|
+
const raw = await readJournalStateRaw<unknown>(null, workspaceRoot);
|
|
116
|
+
return parseState(raw);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function writeState(workspaceRoot: string, state: JournalState): Promise<void> {
|
|
120
|
+
await writeJournalStateRaw(state, workspaceRoot);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function stateFileExists(workspaceRoot: string): boolean {
|
|
124
|
+
return journalStateExistsRaw(workspaceRoot);
|
|
125
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Single source of truth for workspace directory / file names and
|
|
2
|
+
// their absolute paths. The record below uses workspace-relative
|
|
3
|
+
// paths (possibly multi-segment, e.g. `config/roles`) as values; code
|
|
4
|
+
// looks up via `WORKSPACE_PATHS.<key>` to get the absolute form.
|
|
5
|
+
//
|
|
6
|
+
// Layout grouping (issue #284):
|
|
7
|
+
//
|
|
8
|
+
// config/ settings + roles + helps
|
|
9
|
+
// conversations/ chat + memory.md + summaries
|
|
10
|
+
// data/ user-managed (wiki, todos, calendar, contacts,
|
|
11
|
+
// scheduler, sources, transports)
|
|
12
|
+
// artifacts/ LLM-generated (charts, html, images, documents,
|
|
13
|
+
// spreadsheets, stories, news)
|
|
14
|
+
//
|
|
15
|
+
// Existing workspaces need the one-shot `scripts/migrate-workspace-284.ts`
|
|
16
|
+
// script run before first startup with this code. `server/workspace.ts`
|
|
17
|
+
// detects the pre-migration layout at boot and aborts with a pointer
|
|
18
|
+
// to the script.
|
|
19
|
+
//
|
|
20
|
+
// When adding a new top-level directory: add the name to the
|
|
21
|
+
// `WORKSPACE_DIRS` record below. The absolute path is derived
|
|
22
|
+
// automatically via `WORKSPACE_PATHS`.
|
|
23
|
+
|
|
24
|
+
import os from "os";
|
|
25
|
+
import path from "path";
|
|
26
|
+
|
|
27
|
+
// Workspace root. Hard-coded to `~/mulmoclaude` — there is no
|
|
28
|
+
// WORKSPACE_PATH env override today; changing the location
|
|
29
|
+
// requires a code edit or a symlink. Re-exported by
|
|
30
|
+
// `server/workspace.ts` for backwards compatibility of existing
|
|
31
|
+
// callers that `import { workspacePath } from "./workspace.js"`.
|
|
32
|
+
export const workspacePath = path.join(os.homedir(), "mulmoclaude");
|
|
33
|
+
|
|
34
|
+
// Workspace-relative paths. Keys are the stable code-side identifiers
|
|
35
|
+
// (e.g. `markdowns` — unchanged for call-site compatibility); values
|
|
36
|
+
// are the on-disk paths, grouped per issue #284.
|
|
37
|
+
export const WORKSPACE_DIRS = {
|
|
38
|
+
// conversations/
|
|
39
|
+
chat: "conversations/chat",
|
|
40
|
+
summaries: "conversations/summaries",
|
|
41
|
+
// Tool-trace output for WebSearch (one .md per search, referenced
|
|
42
|
+
// from chat JSONL `contentRef`). Lives alongside chat/ so search
|
|
43
|
+
// trace and chat session share the same grouping.
|
|
44
|
+
searches: "conversations/searches",
|
|
45
|
+
// data/
|
|
46
|
+
wiki: "data/wiki",
|
|
47
|
+
todos: "data/todos",
|
|
48
|
+
calendar: "data/calendar",
|
|
49
|
+
contacts: "data/contacts",
|
|
50
|
+
scheduler: "data/scheduler",
|
|
51
|
+
sources: "data/sources",
|
|
52
|
+
transports: "data/transports",
|
|
53
|
+
// artifacts/
|
|
54
|
+
charts: "artifacts/charts",
|
|
55
|
+
// `markdowns` key preserved for call-site compatibility; on-disk
|
|
56
|
+
// name is `documents` for clarity.
|
|
57
|
+
markdowns: "artifacts/documents",
|
|
58
|
+
// `htmls` = `presentHtml` plugin output (many files, persistent).
|
|
59
|
+
// On-disk normalized to lowercase `html`.
|
|
60
|
+
htmls: "artifacts/html",
|
|
61
|
+
// Distinct from `htmls`: scratch buffer for the `/api/html`
|
|
62
|
+
// generate-and-preview route. One file (`current.html`), always
|
|
63
|
+
// overwritten. Kept separate so reloading a saved HTML artifact
|
|
64
|
+
// doesn't clobber the current preview.
|
|
65
|
+
html: "artifacts/html-scratch",
|
|
66
|
+
images: "artifacts/images",
|
|
67
|
+
spreadsheets: "artifacts/spreadsheets",
|
|
68
|
+
stories: "artifacts/stories",
|
|
69
|
+
news: "artifacts/news",
|
|
70
|
+
// config/
|
|
71
|
+
configs: "config",
|
|
72
|
+
roles: "config/roles",
|
|
73
|
+
helps: "config/helps",
|
|
74
|
+
// Nested subdirs inside a top-level grouping. Kept here (rather
|
|
75
|
+
// than module-local constants) when multiple modules need to
|
|
76
|
+
// reference the same nested path — e.g. wiki/pages/ is used by
|
|
77
|
+
// the wiki route, the wiki-backlinks driver, and the system
|
|
78
|
+
// prompt hint.
|
|
79
|
+
wikiPages: "data/wiki/pages",
|
|
80
|
+
wikiSources: "data/wiki/sources",
|
|
81
|
+
// Development — git-cloned repositories (#256).
|
|
82
|
+
github: "github",
|
|
83
|
+
} as const;
|
|
84
|
+
|
|
85
|
+
// Well-known individual files — imported from the shared
|
|
86
|
+
// src/config/workspacePaths.ts (single source of truth for both
|
|
87
|
+
// server and frontend). Re-exported so server callers keep the
|
|
88
|
+
// same `import { WORKSPACE_FILES } from "./paths.js"` they use.
|
|
89
|
+
import { WORKSPACE_FILES } from "../../src/config/workspacePaths.js";
|
|
90
|
+
export { WORKSPACE_FILES };
|
|
91
|
+
|
|
92
|
+
// Absolute paths, built once at module load from `workspacePath`.
|
|
93
|
+
// The `workspacePath` const is itself fixed (reads `os.homedir()`
|
|
94
|
+
// at process start — no env override, see `server/workspace.ts`),
|
|
95
|
+
// so freezing these paths is safe.
|
|
96
|
+
export const WORKSPACE_PATHS = {
|
|
97
|
+
chat: path.join(workspacePath, WORKSPACE_DIRS.chat),
|
|
98
|
+
todos: path.join(workspacePath, WORKSPACE_DIRS.todos),
|
|
99
|
+
calendar: path.join(workspacePath, WORKSPACE_DIRS.calendar),
|
|
100
|
+
contacts: path.join(workspacePath, WORKSPACE_DIRS.contacts),
|
|
101
|
+
scheduler: path.join(workspacePath, WORKSPACE_DIRS.scheduler),
|
|
102
|
+
roles: path.join(workspacePath, WORKSPACE_DIRS.roles),
|
|
103
|
+
stories: path.join(workspacePath, WORKSPACE_DIRS.stories),
|
|
104
|
+
images: path.join(workspacePath, WORKSPACE_DIRS.images),
|
|
105
|
+
markdowns: path.join(workspacePath, WORKSPACE_DIRS.markdowns),
|
|
106
|
+
spreadsheets: path.join(workspacePath, WORKSPACE_DIRS.spreadsheets),
|
|
107
|
+
charts: path.join(workspacePath, WORKSPACE_DIRS.charts),
|
|
108
|
+
configs: path.join(workspacePath, WORKSPACE_DIRS.configs),
|
|
109
|
+
helps: path.join(workspacePath, WORKSPACE_DIRS.helps),
|
|
110
|
+
wiki: path.join(workspacePath, WORKSPACE_DIRS.wiki),
|
|
111
|
+
news: path.join(workspacePath, WORKSPACE_DIRS.news),
|
|
112
|
+
sources: path.join(workspacePath, WORKSPACE_DIRS.sources),
|
|
113
|
+
summaries: path.join(workspacePath, WORKSPACE_DIRS.summaries),
|
|
114
|
+
searches: path.join(workspacePath, WORKSPACE_DIRS.searches),
|
|
115
|
+
htmls: path.join(workspacePath, WORKSPACE_DIRS.htmls),
|
|
116
|
+
html: path.join(workspacePath, WORKSPACE_DIRS.html),
|
|
117
|
+
transports: path.join(workspacePath, WORKSPACE_DIRS.transports),
|
|
118
|
+
github: path.join(workspacePath, WORKSPACE_DIRS.github),
|
|
119
|
+
// nested subdirs
|
|
120
|
+
wikiPages: path.join(workspacePath, WORKSPACE_DIRS.wikiPages),
|
|
121
|
+
wikiSources: path.join(workspacePath, WORKSPACE_DIRS.wikiSources),
|
|
122
|
+
// files
|
|
123
|
+
memory: path.join(workspacePath, WORKSPACE_FILES.memory),
|
|
124
|
+
sessionToken: path.join(workspacePath, WORKSPACE_FILES.sessionToken),
|
|
125
|
+
wikiIndex: path.join(workspacePath, WORKSPACE_FILES.wikiIndex),
|
|
126
|
+
wikiLog: path.join(workspacePath, WORKSPACE_FILES.wikiLog),
|
|
127
|
+
wikiSchema: path.join(workspacePath, WORKSPACE_FILES.wikiSchema),
|
|
128
|
+
wikiSummary: path.join(workspacePath, WORKSPACE_FILES.wikiSummary),
|
|
129
|
+
summariesIndex: path.join(workspacePath, WORKSPACE_FILES.summariesIndex),
|
|
130
|
+
todosItems: path.join(workspacePath, WORKSPACE_FILES.todosItems),
|
|
131
|
+
todosColumns: path.join(workspacePath, WORKSPACE_FILES.todosColumns),
|
|
132
|
+
schedulerItems: path.join(workspacePath, WORKSPACE_FILES.schedulerItems),
|
|
133
|
+
schedulerUserTasks: path.join(workspacePath, WORKSPACE_FILES.schedulerUserTasks),
|
|
134
|
+
schedulerOverrides: path.join(workspacePath, WORKSPACE_FILES.schedulerOverrides),
|
|
135
|
+
} as const;
|
|
136
|
+
|
|
137
|
+
export type WorkspaceDirKey = keyof typeof WORKSPACE_DIRS;
|
|
138
|
+
export type WorkspacePathKey = keyof typeof WORKSPACE_PATHS;
|
|
139
|
+
|
|
140
|
+
// Directories `initWorkspace()` creates eagerly on server start.
|
|
141
|
+
// Kept as a subset of `WORKSPACE_DIRS` so new entries are additive
|
|
142
|
+
// without touching `server/workspace.ts`. Everything *not* on this
|
|
143
|
+
// list is created lazily (first write) by its owning module.
|
|
144
|
+
export const EAGER_WORKSPACE_DIRS: readonly WorkspaceDirKey[] = [
|
|
145
|
+
"chat",
|
|
146
|
+
"todos",
|
|
147
|
+
"calendar",
|
|
148
|
+
"contacts",
|
|
149
|
+
"scheduler",
|
|
150
|
+
"roles",
|
|
151
|
+
"stories",
|
|
152
|
+
"images",
|
|
153
|
+
"markdowns",
|
|
154
|
+
"spreadsheets",
|
|
155
|
+
"charts",
|
|
156
|
+
"configs",
|
|
157
|
+
"github",
|
|
158
|
+
];
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// User-defined reference directories (#455).
|
|
2
|
+
//
|
|
3
|
+
// Loaded from `config/reference-dirs.json`. Users can specify external
|
|
4
|
+
// directories that the agent can read (but not write to).
|
|
5
|
+
//
|
|
6
|
+
// Docker mode: mounted as `:ro` — filesystem-enforced read-only.
|
|
7
|
+
// Non-Docker mode: prompt-based restriction only.
|
|
8
|
+
|
|
9
|
+
import { createHash } from "crypto";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import os from "os";
|
|
12
|
+
import { log } from "../system/logger/index.js";
|
|
13
|
+
import { readReferenceDirsJson, writeReferenceDirsJson, isExistingDirectory } from "../utils/files/reference-dirs-io.js";
|
|
14
|
+
import { isRecord } from "../utils/types.js";
|
|
15
|
+
|
|
16
|
+
// ── Types ───────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface ReferenceDirEntry {
|
|
19
|
+
/** Absolute host path to the directory. */
|
|
20
|
+
hostPath: string;
|
|
21
|
+
/** Short label shown in prompt and UI. */
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Constants ───────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const MAX_ENTRIES = 20;
|
|
28
|
+
const MAX_LABEL_LENGTH = 100;
|
|
29
|
+
const CONTAINER_MOUNT_ROOT = "/mnt/readonly";
|
|
30
|
+
|
|
31
|
+
/** Home-relative directories that must never be mounted. */
|
|
32
|
+
const HOME_RELATIVE_BLOCKED = [".ssh", ".aws", ".gnupg", ".config/gh", ".kube", ".docker"];
|
|
33
|
+
|
|
34
|
+
/** Absolute system paths that must never be mounted. */
|
|
35
|
+
const SYSTEM_BLOCKED_PREFIXES = ["/etc", "/root", "/var", "/proc", "/sys", "/boot", "/private/etc", "/private/var", "/System", "/Library"];
|
|
36
|
+
|
|
37
|
+
// eslint-disable-next-line no-control-regex
|
|
38
|
+
const CONTROL_CHAR_RE_G = /[\x00-\x1f]/g;
|
|
39
|
+
|
|
40
|
+
// ── Validation ──────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function expandHome(p: string): string {
|
|
43
|
+
if (p.startsWith("~/")) {
|
|
44
|
+
return path.join(os.homedir(), p.slice(2));
|
|
45
|
+
}
|
|
46
|
+
return p;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isSensitivePath(absPath: string): boolean {
|
|
50
|
+
const normalized = path.resolve(absPath);
|
|
51
|
+
|
|
52
|
+
// Reject filesystem root
|
|
53
|
+
if (normalized === path.parse(normalized).root) return true;
|
|
54
|
+
|
|
55
|
+
const home = os.homedir();
|
|
56
|
+
|
|
57
|
+
// Block $HOME itself (transitively exposes .ssh etc.)
|
|
58
|
+
if (normalized === home) return true;
|
|
59
|
+
|
|
60
|
+
// Block home-relative sensitive dirs
|
|
61
|
+
if (
|
|
62
|
+
HOME_RELATIVE_BLOCKED.some((bp) => {
|
|
63
|
+
const full = path.join(home, bp);
|
|
64
|
+
return normalized === full || normalized.startsWith(full + path.sep);
|
|
65
|
+
})
|
|
66
|
+
) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Block system directories
|
|
71
|
+
return SYSTEM_BLOCKED_PREFIXES.some((p) => normalized === p || normalized.startsWith(p + path.sep));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sanitizeLabel(raw: string): string {
|
|
75
|
+
if (typeof raw !== "string") return "";
|
|
76
|
+
return raw.replace(CONTROL_CHAR_RE_G, " ").trim().slice(0, MAX_LABEL_LENGTH);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasTraversalSegment(p: string): boolean {
|
|
80
|
+
return p.split(path.sep).some((seg) => seg === "..");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateEntry(raw: unknown): ReferenceDirEntry | null {
|
|
84
|
+
if (!isRecord(raw)) return null;
|
|
85
|
+
const obj = raw as Record<string, unknown>;
|
|
86
|
+
|
|
87
|
+
const rawPath = typeof obj.hostPath === "string" ? obj.hostPath : "";
|
|
88
|
+
if (!rawPath) return null;
|
|
89
|
+
|
|
90
|
+
const expanded = expandHome(rawPath);
|
|
91
|
+
|
|
92
|
+
// Must be absolute
|
|
93
|
+
if (!path.isAbsolute(expanded)) return null;
|
|
94
|
+
|
|
95
|
+
// Normalize to collapse . and // segments
|
|
96
|
+
const absPath = path.resolve(expanded);
|
|
97
|
+
|
|
98
|
+
// Reject actual ".." traversal segments (not substrings in filenames)
|
|
99
|
+
if (hasTraversalSegment(expanded)) return null;
|
|
100
|
+
|
|
101
|
+
// Block sensitive directories
|
|
102
|
+
if (isSensitivePath(absPath)) {
|
|
103
|
+
log.warn("reference-dirs", "blocked sensitive path", { path: absPath });
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const label = sanitizeLabel(String(obj.label ?? path.basename(absPath)));
|
|
108
|
+
|
|
109
|
+
return { hostPath: absPath, label };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Load ────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
export function loadReferenceDirs(root?: string): ReferenceDirEntry[] {
|
|
115
|
+
const parsed = readReferenceDirsJson(root);
|
|
116
|
+
const seenLabels = new Set<string>();
|
|
117
|
+
const entries = parsed
|
|
118
|
+
.slice(0, MAX_ENTRIES)
|
|
119
|
+
.map(validateEntry)
|
|
120
|
+
.filter((e): e is ReferenceDirEntry => {
|
|
121
|
+
if (!e) return false;
|
|
122
|
+
// Deduplicate labels — first entry wins
|
|
123
|
+
if (seenLabels.has(e.label)) return false;
|
|
124
|
+
seenLabels.add(e.label);
|
|
125
|
+
return true;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const skipped = parsed.length - entries.length;
|
|
129
|
+
if (skipped > 0) {
|
|
130
|
+
log.warn("reference-dirs", "skipped invalid entries", { skipped });
|
|
131
|
+
}
|
|
132
|
+
return entries;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Save ────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export function saveReferenceDirs(entries: readonly ReferenceDirEntry[], root?: string): void {
|
|
138
|
+
writeReferenceDirsJson(entries, root);
|
|
139
|
+
invalidateCache();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Validate input array (for API) ─────────────────────────────
|
|
143
|
+
|
|
144
|
+
export function validateReferenceDirs(raw: unknown): { entries: ReferenceDirEntry[] } | { error: string } {
|
|
145
|
+
if (!Array.isArray(raw)) {
|
|
146
|
+
return { error: "expected an array" };
|
|
147
|
+
}
|
|
148
|
+
if (raw.length > MAX_ENTRIES) {
|
|
149
|
+
return { error: `too many entries (max ${MAX_ENTRIES})` };
|
|
150
|
+
}
|
|
151
|
+
const entries: ReferenceDirEntry[] = [];
|
|
152
|
+
const errors: string[] = [];
|
|
153
|
+
raw.forEach((item, i) => {
|
|
154
|
+
const entry = validateEntry(item);
|
|
155
|
+
if (entry) {
|
|
156
|
+
entries.push(entry);
|
|
157
|
+
} else {
|
|
158
|
+
const p = isRecord(item) ? String((item as Record<string, unknown>).hostPath ?? "") : "";
|
|
159
|
+
errors.push(`entry ${i}: invalid or blocked path "${p}"`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
if (errors.length > 0) {
|
|
163
|
+
return { error: errors.join("; ") };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Reject duplicate labels — @ref/<label> routing requires uniqueness
|
|
167
|
+
const seenLabels = new Set<string>();
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
if (seenLabels.has(entry.label)) {
|
|
170
|
+
return { error: `duplicate label "${entry.label}"` };
|
|
171
|
+
}
|
|
172
|
+
seenLabels.add(entry.label);
|
|
173
|
+
}
|
|
174
|
+
return { entries };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Cached loader (for system prompt + Docker mounts) ───────────
|
|
178
|
+
|
|
179
|
+
let cachedEntries: ReferenceDirEntry[] | null = null;
|
|
180
|
+
|
|
181
|
+
export function getCachedReferenceDirs(): readonly ReferenceDirEntry[] {
|
|
182
|
+
if (cachedEntries === null) {
|
|
183
|
+
cachedEntries = loadReferenceDirs();
|
|
184
|
+
}
|
|
185
|
+
return cachedEntries;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function invalidateCache(): void {
|
|
189
|
+
cachedEntries = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Docker mount args ───────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/** Container path for a reference directory.
|
|
195
|
+
* Disambiguates with a short hash suffix to prevent collisions
|
|
196
|
+
* when different host paths share the same basename. */
|
|
197
|
+
export function containerPath(entry: ReferenceDirEntry): string {
|
|
198
|
+
const basename = path.basename(entry.hostPath);
|
|
199
|
+
const hash = createHash("sha256").update(entry.hostPath).digest("hex").slice(0, 8);
|
|
200
|
+
return path.posix.join(CONTAINER_MOUNT_ROOT, `${basename}-${hash}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Return Docker `-v` args for read-only reference directory mounts.
|
|
205
|
+
* Skips entries whose host path doesn't exist.
|
|
206
|
+
*/
|
|
207
|
+
export function referenceDirMountArgs(entries: readonly ReferenceDirEntry[]): string[] {
|
|
208
|
+
const args: string[] = [];
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
if (!isExistingDirectory(entry.hostPath)) {
|
|
211
|
+
log.info("reference-dirs", "skipped (not found or not a directory)", {
|
|
212
|
+
path: entry.hostPath,
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const host = entry.hostPath.replace(/\\/g, "/");
|
|
217
|
+
args.push("-v", `${host}:${containerPath(entry)}:ro`);
|
|
218
|
+
}
|
|
219
|
+
return args;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── System prompt snippet ───────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
export function buildReferenceDirsPrompt(entries: readonly ReferenceDirEntry[], useDocker: boolean): string {
|
|
225
|
+
if (entries.length === 0) return "";
|
|
226
|
+
|
|
227
|
+
const lines = [
|
|
228
|
+
"",
|
|
229
|
+
"## Reference Directories (Read-Only)",
|
|
230
|
+
"",
|
|
231
|
+
"The user has configured external directories for reference.",
|
|
232
|
+
"You may READ files in these directories but MUST NOT write, modify, or delete anything in them.",
|
|
233
|
+
"",
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
for (const e of entries) {
|
|
237
|
+
const mountPath = useDocker ? containerPath(e) : e.hostPath;
|
|
238
|
+
lines.push(`- \`${mountPath}\` — ${e.label}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!useDocker) {
|
|
242
|
+
lines.push("");
|
|
243
|
+
lines.push(
|
|
244
|
+
"**Important**: These directories are outside the workspace. " +
|
|
245
|
+
"Do not create, edit, or delete files in them. " +
|
|
246
|
+
"Only use read operations (read, glob, grep).",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
lines.push("");
|
|
251
|
+
return lines.join("\n");
|
|
252
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { BUILTIN_ROLES, RoleSchema, type Role } from "../../src/config/roles.js";
|
|
3
|
+
import { WORKSPACE_DIRS } from "./paths.js";
|
|
4
|
+
import { readdirUnderSync, readTextUnderSync } from "../utils/files/workspace-io.js";
|
|
5
|
+
import { workspacePath } from "./paths.js";
|
|
6
|
+
|
|
7
|
+
function withSwitchRole(role: Role): Role {
|
|
8
|
+
if (role.availablePlugins.includes("switchRole")) return role;
|
|
9
|
+
return {
|
|
10
|
+
...role,
|
|
11
|
+
availablePlugins: [...role.availablePlugins, "switchRole"],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadCustomRoles(): Role[] {
|
|
16
|
+
return readdirUnderSync(workspacePath, WORKSPACE_DIRS.roles)
|
|
17
|
+
.filter((f) => f.endsWith(".json"))
|
|
18
|
+
.flatMap((f) => {
|
|
19
|
+
try {
|
|
20
|
+
const raw = readTextUnderSync(workspacePath, path.posix.join(WORKSPACE_DIRS.roles, f));
|
|
21
|
+
if (!raw) return [];
|
|
22
|
+
return [withSwitchRole(RoleSchema.parse(JSON.parse(raw)))];
|
|
23
|
+
} catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadAllRoles(): Role[] {
|
|
30
|
+
const custom = loadCustomRoles();
|
|
31
|
+
const builtIn = BUILTIN_ROLES.filter((r) => !custom.find((c) => c.id === r.id));
|
|
32
|
+
return [...builtIn, ...custom];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getRole(id: string): Role {
|
|
36
|
+
return loadAllRoles().find((r) => r.id === id) ?? BUILTIN_ROLES[0];
|
|
37
|
+
}
|