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,243 @@
|
|
|
1
|
+
// Workspace-scoped user settings, loaded fresh on every agent
|
|
2
|
+
// invocation so the UI can change things without a server restart.
|
|
3
|
+
//
|
|
4
|
+
// Layout under <workspace>/config/ (post-#284):
|
|
5
|
+
// settings.json ← AppSettings (this file)
|
|
6
|
+
// mcp.json ← user-defined MCP servers
|
|
7
|
+
//
|
|
8
|
+
// All helpers tolerate missing / malformed files by falling back to
|
|
9
|
+
// defaults. Writers perform an atomic replace (tmp + rename) so a
|
|
10
|
+
// reader never observes a half-written file.
|
|
11
|
+
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { log } from "./logger/index.js";
|
|
15
|
+
import { WORKSPACE_PATHS } from "../workspace/paths.js";
|
|
16
|
+
import { writeFileAtomicSync } from "../utils/files/atomic.js";
|
|
17
|
+
import { readTextSafeSync } from "../utils/files/safe.js";
|
|
18
|
+
import { isRecord, isStringArray, isStringRecord } from "../utils/types.js";
|
|
19
|
+
|
|
20
|
+
export interface AppSettings {
|
|
21
|
+
// Extra tool names appended to BASE_ALLOWED_TOOLS in
|
|
22
|
+
// server/agent/config.ts#buildCliArgs. Typical entries are
|
|
23
|
+
// Claude Code built-in MCP prefixes like
|
|
24
|
+
// "mcp__claude_ai_Gmail"
|
|
25
|
+
// "mcp__claude_ai_Google_Calendar"
|
|
26
|
+
extraAllowedTools: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_SETTINGS: AppSettings = { extraAllowedTools: [] };
|
|
30
|
+
|
|
31
|
+
export const SETTINGS_FILE_NAME = "settings.json";
|
|
32
|
+
export const MCP_FILE_NAME = "mcp.json";
|
|
33
|
+
|
|
34
|
+
export function configsDir(): string {
|
|
35
|
+
return WORKSPACE_PATHS.configs;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function settingsPath(): string {
|
|
39
|
+
return path.join(configsDir(), SETTINGS_FILE_NAME);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function mcpConfigPath(): string {
|
|
43
|
+
return path.join(configsDir(), MCP_FILE_NAME);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function ensureConfigsDir(): void {
|
|
47
|
+
fs.mkdirSync(configsDir(), { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isAppSettings(value: unknown): value is AppSettings {
|
|
51
|
+
if (!isRecord(value)) return false;
|
|
52
|
+
return isStringArray(value.extraAllowedTools);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function loadSettings(): AppSettings {
|
|
56
|
+
const file = settingsPath();
|
|
57
|
+
const raw = readTextSafeSync(file);
|
|
58
|
+
if (raw === null) return { ...DEFAULT_SETTINGS };
|
|
59
|
+
let parsed: unknown;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(raw);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
log.warn("config", "settings.json is not valid JSON — using defaults", {
|
|
64
|
+
file,
|
|
65
|
+
error: String(err),
|
|
66
|
+
});
|
|
67
|
+
return { ...DEFAULT_SETTINGS };
|
|
68
|
+
}
|
|
69
|
+
if (!isAppSettings(parsed)) {
|
|
70
|
+
log.warn("config", "settings.json does not match AppSettings schema — using defaults", { file });
|
|
71
|
+
return { ...DEFAULT_SETTINGS };
|
|
72
|
+
}
|
|
73
|
+
// Defensive copy — callers shouldn't be able to mutate the file on
|
|
74
|
+
// disk via the returned object.
|
|
75
|
+
return { extraAllowedTools: [...parsed.extraAllowedTools] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function saveSettings(settings: AppSettings): void {
|
|
79
|
+
if (!isAppSettings(settings)) {
|
|
80
|
+
throw new Error("saveSettings: invalid AppSettings shape");
|
|
81
|
+
}
|
|
82
|
+
ensureConfigsDir();
|
|
83
|
+
const serialised = JSON.stringify({ extraAllowedTools: [...settings.extraAllowedTools] }, null, 2);
|
|
84
|
+
writeFileAtomicSync(settingsPath(), `${serialised}\n`, { mode: 0o600 });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── MCP user-defined servers ────────────────────────────────────
|
|
88
|
+
//
|
|
89
|
+
// Stored under <workspace>/config/mcp.json in the Claude CLI's
|
|
90
|
+
// standard `--mcp-config` shape so the file is portable:
|
|
91
|
+
// { "mcpServers": { "<id>": <McpServerSpec> } }
|
|
92
|
+
//
|
|
93
|
+
// A server is either HTTP (remote, always Docker-safe) or stdio
|
|
94
|
+
// (local command). See plans/done/feat-web-settings-ui.md for Phase 2a /
|
|
95
|
+
// 2b scope notes.
|
|
96
|
+
|
|
97
|
+
export interface McpHttpSpec {
|
|
98
|
+
type: "http";
|
|
99
|
+
url: string;
|
|
100
|
+
headers?: Record<string, string>;
|
|
101
|
+
enabled?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface McpStdioSpec {
|
|
105
|
+
type: "stdio";
|
|
106
|
+
command: string;
|
|
107
|
+
args?: string[];
|
|
108
|
+
env?: Record<string, string>;
|
|
109
|
+
enabled?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type McpServerSpec = McpHttpSpec | McpStdioSpec;
|
|
113
|
+
|
|
114
|
+
// UI-friendly flat array form. Storage uses the record form; conversion
|
|
115
|
+
// helpers below keep the two in sync.
|
|
116
|
+
export interface McpServerEntry {
|
|
117
|
+
id: string;
|
|
118
|
+
spec: McpServerSpec;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface McpConfigFile {
|
|
122
|
+
mcpServers: Record<string, McpServerSpec>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const DEFAULT_MCP: McpConfigFile = { mcpServers: {} };
|
|
126
|
+
|
|
127
|
+
// Accepts only allow-listed commands for stdio servers — user input
|
|
128
|
+
// that asks Claude to spawn arbitrary binaries (eg. a shell one-liner)
|
|
129
|
+
// is rejected upstream. Anything that needs more tools should go in
|
|
130
|
+
// the sandbox image (see #162), not here.
|
|
131
|
+
const STDIO_COMMAND_ALLOWLIST = new Set(["npx", "node", "tsx"]);
|
|
132
|
+
|
|
133
|
+
// Accept only http: / https: URLs. Rejects malformed strings, other
|
|
134
|
+
// protocols (ftp:, file:, javascript:, ...), and empty values so bad
|
|
135
|
+
// endpoints can't be persisted even if a client bypasses the UI.
|
|
136
|
+
function isHttpUrl(value: string): boolean {
|
|
137
|
+
try {
|
|
138
|
+
const parsed = new URL(value);
|
|
139
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function isMcpHttpSpec(value: unknown): value is McpHttpSpec {
|
|
146
|
+
if (!isRecord(value)) return false;
|
|
147
|
+
|
|
148
|
+
if (value.type !== "http") return false;
|
|
149
|
+
if (typeof value.url !== "string" || !isHttpUrl(value.url)) return false;
|
|
150
|
+
if (value.headers !== undefined && !isStringRecord(value.headers)) return false;
|
|
151
|
+
if (value.enabled !== undefined && typeof value.enabled !== "boolean") return false;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function isMcpStdioSpec(value: unknown): value is McpStdioSpec {
|
|
156
|
+
if (!isRecord(value)) return false;
|
|
157
|
+
|
|
158
|
+
if (value.type !== "stdio") return false;
|
|
159
|
+
if (typeof value.command !== "string" || value.command.length === 0) return false;
|
|
160
|
+
if (!STDIO_COMMAND_ALLOWLIST.has(value.command)) return false;
|
|
161
|
+
if (value.args !== undefined && !isStringArray(value.args)) return false;
|
|
162
|
+
if (value.env !== undefined && !isStringRecord(value.env)) return false;
|
|
163
|
+
if (value.enabled !== undefined && typeof value.enabled !== "boolean") return false;
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function isMcpServerSpec(value: unknown): value is McpServerSpec {
|
|
168
|
+
return isMcpHttpSpec(value) || isMcpStdioSpec(value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Workspace id must be slug-shaped so it survives being used as the
|
|
172
|
+
// mcpServers map key and in the `mcp__<id>__<tool>` tool naming.
|
|
173
|
+
const MCP_ID_RE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
174
|
+
|
|
175
|
+
export function isMcpServerId(value: unknown): value is string {
|
|
176
|
+
return typeof value === "string" && MCP_ID_RE.test(value);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function isMcpConfigFile(value: unknown): value is McpConfigFile {
|
|
180
|
+
if (!isRecord(value)) return false;
|
|
181
|
+
|
|
182
|
+
const servers = value.mcpServers;
|
|
183
|
+
if (!isRecord(servers)) return false;
|
|
184
|
+
for (const [id, spec] of Object.entries(servers)) {
|
|
185
|
+
if (!isMcpServerId(id)) return false;
|
|
186
|
+
if (!isMcpServerSpec(spec)) return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function loadMcpConfig(): McpConfigFile {
|
|
192
|
+
const file = mcpConfigPath();
|
|
193
|
+
const raw = readTextSafeSync(file);
|
|
194
|
+
if (raw === null) return { mcpServers: { ...DEFAULT_MCP.mcpServers } };
|
|
195
|
+
let parsed: unknown;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(raw);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
log.warn("config", "mcp.json is not valid JSON — using defaults", {
|
|
200
|
+
file,
|
|
201
|
+
error: String(err),
|
|
202
|
+
});
|
|
203
|
+
return { mcpServers: {} };
|
|
204
|
+
}
|
|
205
|
+
if (!isMcpConfigFile(parsed)) {
|
|
206
|
+
log.warn("config", "mcp.json does not match McpConfigFile schema — using defaults", { file });
|
|
207
|
+
return { mcpServers: {} };
|
|
208
|
+
}
|
|
209
|
+
return parsed;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function saveMcpConfig(cfg: McpConfigFile): void {
|
|
213
|
+
if (!isMcpConfigFile(cfg)) {
|
|
214
|
+
throw new Error("saveMcpConfig: invalid McpConfigFile shape");
|
|
215
|
+
}
|
|
216
|
+
ensureConfigsDir();
|
|
217
|
+
const serialised = JSON.stringify(cfg, null, 2);
|
|
218
|
+
writeFileAtomicSync(mcpConfigPath(), `${serialised}\n`, { mode: 0o600 });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Flatten storage form to UI-friendly array.
|
|
222
|
+
export function toMcpEntries(cfg: McpConfigFile): McpServerEntry[] {
|
|
223
|
+
return Object.entries(cfg.mcpServers).map(([id, spec]) => ({ id, spec }));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Re-inflate UI-friendly array to storage form. Duplicate ids are
|
|
227
|
+
// rejected so the record shape stays lossless.
|
|
228
|
+
export function fromMcpEntries(entries: McpServerEntry[]): McpConfigFile {
|
|
229
|
+
const out: Record<string, McpServerSpec> = {};
|
|
230
|
+
for (const { id, spec } of entries) {
|
|
231
|
+
if (!isMcpServerId(id)) {
|
|
232
|
+
throw new Error(`invalid MCP server id: ${JSON.stringify(id)}`);
|
|
233
|
+
}
|
|
234
|
+
if (id in out) {
|
|
235
|
+
throw new Error(`duplicate MCP server id: ${id}`);
|
|
236
|
+
}
|
|
237
|
+
if (!isMcpServerSpec(spec)) {
|
|
238
|
+
throw new Error(`invalid MCP server spec for id ${id}`);
|
|
239
|
+
}
|
|
240
|
+
out[id] = spec;
|
|
241
|
+
}
|
|
242
|
+
return { mcpServers: out };
|
|
243
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { writeFile } from "fs/promises";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
import { log } from "./logger/index.js";
|
|
7
|
+
import { ONE_SECOND_MS, ONE_MINUTE_MS } from "../utils/time.js";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
const CREDENTIALS_PATH = join(homedir(), ".claude", ".credentials.json");
|
|
12
|
+
const KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
13
|
+
|
|
14
|
+
/** Safety margin — treat tokens as expired 60s before actual expiry. */
|
|
15
|
+
const EXPIRY_MARGIN_MS = ONE_MINUTE_MS;
|
|
16
|
+
/** Maximum time to wait for the claude CLI to respond. */
|
|
17
|
+
const PTY_TIMEOUT_MS = 30 * ONE_SECOND_MS;
|
|
18
|
+
/** Delay before sending input to the claude CLI. */
|
|
19
|
+
const PTY_INPUT_DELAY_MS = 3 * ONE_SECOND_MS;
|
|
20
|
+
|
|
21
|
+
interface CredentialsJson {
|
|
22
|
+
claudeAiOauth?: {
|
|
23
|
+
accessToken?: string;
|
|
24
|
+
refreshToken?: string;
|
|
25
|
+
expiresAt?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read the raw credentials string from macOS Keychain.
|
|
31
|
+
*/
|
|
32
|
+
async function readFromKeychain(): Promise<string | null> {
|
|
33
|
+
try {
|
|
34
|
+
const { stdout } = await execFileAsync("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"]);
|
|
35
|
+
const credentials = stdout.trim();
|
|
36
|
+
return credentials || null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check whether the access token in the credentials JSON is expired.
|
|
44
|
+
*/
|
|
45
|
+
function isTokenExpired(raw: string): boolean {
|
|
46
|
+
try {
|
|
47
|
+
const creds: CredentialsJson = JSON.parse(raw);
|
|
48
|
+
const expiresAt = creds.claudeAiOauth?.expiresAt;
|
|
49
|
+
if (!expiresAt) return true; // no expiry info — treat as expired
|
|
50
|
+
|
|
51
|
+
const expiresMs = new Date(expiresAt).getTime();
|
|
52
|
+
if (isNaN(expiresMs)) return true;
|
|
53
|
+
|
|
54
|
+
return Date.now() >= expiresMs - EXPIRY_MARGIN_MS;
|
|
55
|
+
} catch {
|
|
56
|
+
log.error("credentials", "Failed to parse credentials JSON from Keychain");
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Spawn `claude` interactively via a PTY to force the CLI to refresh its
|
|
63
|
+
* OAuth token. The CLI handles the refresh internally and writes the new
|
|
64
|
+
* token back to the macOS Keychain.
|
|
65
|
+
*/
|
|
66
|
+
async function renewTokenViaPty(): Promise<boolean> {
|
|
67
|
+
// Dynamic import — node-pty is a native module that may not be present
|
|
68
|
+
// on all platforms. Guard with try/catch.
|
|
69
|
+
let pty: typeof import("node-pty");
|
|
70
|
+
try {
|
|
71
|
+
pty = await import("node-pty");
|
|
72
|
+
} catch {
|
|
73
|
+
log.error("credentials", "node-pty not available, cannot renew token");
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const proc = pty.spawn("claude", [], {
|
|
79
|
+
name: "xterm-color",
|
|
80
|
+
cols: 80,
|
|
81
|
+
rows: 30,
|
|
82
|
+
cwd: process.cwd(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
let responded = false;
|
|
86
|
+
let buffer = "";
|
|
87
|
+
let settled = false;
|
|
88
|
+
|
|
89
|
+
const finish = (success: boolean) => {
|
|
90
|
+
if (settled) return;
|
|
91
|
+
settled = true;
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
proc.kill();
|
|
94
|
+
resolve(success);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const timeout = setTimeout(() => {
|
|
98
|
+
log.error("credentials", `Token renewal timed out after ${PTY_TIMEOUT_MS / ONE_SECOND_MS}s`);
|
|
99
|
+
finish(false);
|
|
100
|
+
}, PTY_TIMEOUT_MS);
|
|
101
|
+
|
|
102
|
+
// Match "hi" as a whole token so unrelated output containing those
|
|
103
|
+
// bytes (e.g. ANSI sequences, words like "This" or "high") can't
|
|
104
|
+
// false-positive the echo detection.
|
|
105
|
+
const ECHO_RE = /\bhi\b/;
|
|
106
|
+
|
|
107
|
+
// After the echo, only treat output as a successful renewal when
|
|
108
|
+
// it looks like a real Claude response — a conversational opener
|
|
109
|
+
// (Hello / Hi / I'm / …) AND a non-trivial amount of text. Error
|
|
110
|
+
// chunks ("Please log in", "Invalid credentials", network blips)
|
|
111
|
+
// don't match both conditions, so they fall through to the
|
|
112
|
+
// 30-second timeout and we treat the renewal as failed. A final
|
|
113
|
+
// safety net: `refreshCredentials()` re-reads the Keychain and
|
|
114
|
+
// calls `isTokenExpired()` before writing, so even a false
|
|
115
|
+
// positive here can't persist a stale token.
|
|
116
|
+
const RESPONSE_PATTERN_RE = /\b(Hello|Hi|I['’]m|I can|How can)\b/i;
|
|
117
|
+
const MIN_RESPONSE_CHARS = 20;
|
|
118
|
+
let echoEndIdx = -1;
|
|
119
|
+
|
|
120
|
+
proc.onData((data: string) => {
|
|
121
|
+
buffer += data;
|
|
122
|
+
|
|
123
|
+
if (!responded) {
|
|
124
|
+
const m = ECHO_RE.exec(buffer);
|
|
125
|
+
if (m) {
|
|
126
|
+
// Claude echoed our "hi" — remember where the response
|
|
127
|
+
// window starts so the success check looks only at bytes
|
|
128
|
+
// that arrived AFTER the echo.
|
|
129
|
+
responded = true;
|
|
130
|
+
echoEndIdx = m.index + m[0].length;
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const response = buffer.slice(echoEndIdx);
|
|
136
|
+
if (response.length >= MIN_RESPONSE_CHARS && RESPONSE_PATTERN_RE.test(response)) {
|
|
137
|
+
finish(true);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Wait for initial prompt before sending input
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
if (!settled) {
|
|
144
|
+
proc.write("hi\r");
|
|
145
|
+
}
|
|
146
|
+
}, PTY_INPUT_DELAY_MS);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Extract the current OAuth credentials from the macOS Keychain and write them
|
|
152
|
+
* to ~/.claude/.credentials.json so that the Docker-based sandbox can read them.
|
|
153
|
+
*
|
|
154
|
+
* If the access token is expired, spawns `claude` interactively via a PTY to
|
|
155
|
+
* force the CLI to refresh its token, then re-reads the fresh credentials.
|
|
156
|
+
*
|
|
157
|
+
* Returns true if credentials were successfully refreshed, false otherwise.
|
|
158
|
+
* Only works on macOS (darwin).
|
|
159
|
+
*/
|
|
160
|
+
export async function refreshCredentials(): Promise<boolean> {
|
|
161
|
+
if (process.platform !== "darwin") return false;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
let credentials = await readFromKeychain();
|
|
165
|
+
if (!credentials) {
|
|
166
|
+
log.error("credentials", "No credentials found in macOS Keychain");
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (isTokenExpired(credentials)) {
|
|
171
|
+
// Extract expiry for logging
|
|
172
|
+
try {
|
|
173
|
+
const creds: CredentialsJson = JSON.parse(credentials);
|
|
174
|
+
const expiresAt = creds.claudeAiOauth?.expiresAt ?? "unknown";
|
|
175
|
+
log.warn("credentials", `Access token expired at ${expiresAt}, launching claude CLI to renew...`);
|
|
176
|
+
} catch {
|
|
177
|
+
log.warn("credentials", "Access token expired (could not parse expiry), launching claude CLI to renew...");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const renewed = await renewTokenViaPty();
|
|
181
|
+
if (!renewed) {
|
|
182
|
+
log.error("credentials", "Token renewal via claude CLI failed");
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
log.info("credentials", "Token renewed successfully via claude CLI");
|
|
187
|
+
|
|
188
|
+
// Re-read the now-fresh credentials from Keychain
|
|
189
|
+
credentials = await readFromKeychain();
|
|
190
|
+
if (!credentials) {
|
|
191
|
+
log.error("credentials", "No credentials in Keychain after renewal — unexpected");
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
// Guard against writing a still-expired token as "fresh": the PTY
|
|
195
|
+
// echo check is a proxy for "Claude responded", not proof that the
|
|
196
|
+
// Keychain entry was actually refreshed.
|
|
197
|
+
if (isTokenExpired(credentials)) {
|
|
198
|
+
log.error("credentials", "Token still expired after renewal — Keychain was not refreshed");
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
try {
|
|
203
|
+
const creds: CredentialsJson = JSON.parse(credentials);
|
|
204
|
+
const expiresAt = creds.claudeAiOauth?.expiresAt ?? "unknown";
|
|
205
|
+
log.info("credentials", `Access token is valid, expires at ${expiresAt}`);
|
|
206
|
+
} catch {
|
|
207
|
+
log.info("credentials", "Access token appears valid");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await writeFile(CREDENTIALS_PATH, credentials + "\n", { mode: 0o600 });
|
|
212
|
+
log.info("credentials", "Fresh credentials written to ~/.claude/.credentials.json");
|
|
213
|
+
return true;
|
|
214
|
+
} catch (err) {
|
|
215
|
+
log.error("credentials", "Failed to refresh credentials from Keychain", {
|
|
216
|
+
error: String(err),
|
|
217
|
+
});
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { execFile, spawn } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
import { readFileSync, statSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join, resolve as resolvePath } from "path";
|
|
7
|
+
import { log } from "./logger/index.js";
|
|
8
|
+
import { env } from "./env.js";
|
|
9
|
+
import { SUBPROCESS_PROBE_TIMEOUT_MS } from "../utils/time.js";
|
|
10
|
+
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
|
|
13
|
+
const IMAGE_NAME = "mulmoclaude-sandbox";
|
|
14
|
+
const DOCKERFILE = "Dockerfile.sandbox";
|
|
15
|
+
const LABEL_KEY = "mulmoclaude.dockerfile.sha256";
|
|
16
|
+
|
|
17
|
+
let _dockerEnabled: boolean | null = null;
|
|
18
|
+
|
|
19
|
+
function assertClaudeFiles(): void {
|
|
20
|
+
const claudeDir = join(homedir(), ".claude");
|
|
21
|
+
const claudeJson = join(homedir(), ".claude.json");
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
if (!statSync(claudeDir).isDirectory()) {
|
|
25
|
+
log.error("sandbox", `${claudeDir} exists but is not a directory.`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
log.error("sandbox", `${claudeDir} not found. Run 'claude' once to initialize.`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (!statSync(claudeJson).isFile()) {
|
|
35
|
+
log.error("sandbox", `${claudeJson} exists but is not a file.`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
log.error("sandbox", `${claudeJson} not found. Run 'claude' once to initialize.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function isDockerAvailable(): Promise<boolean> {
|
|
45
|
+
if (env.disableSandbox) return false;
|
|
46
|
+
if (_dockerEnabled !== null) return _dockerEnabled;
|
|
47
|
+
assertClaudeFiles();
|
|
48
|
+
try {
|
|
49
|
+
await execFileAsync("docker", ["ps", "-q"], {
|
|
50
|
+
timeout: SUBPROCESS_PROBE_TIMEOUT_MS,
|
|
51
|
+
});
|
|
52
|
+
_dockerEnabled = true;
|
|
53
|
+
} catch {
|
|
54
|
+
_dockerEnabled = false;
|
|
55
|
+
}
|
|
56
|
+
return _dockerEnabled;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getDockerfileSha256(): string {
|
|
60
|
+
const content = readFileSync(resolvePath(process.cwd(), DOCKERFILE));
|
|
61
|
+
return createHash("sha256").update(content).digest("hex");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function buildImage(sha: string): Promise<void> {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const proc = spawn("docker", ["build", "-t", IMAGE_NAME, "--label", `${LABEL_KEY}=${sha}`, "-f", DOCKERFILE, "--load", "."], {
|
|
67
|
+
cwd: process.cwd(),
|
|
68
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
69
|
+
});
|
|
70
|
+
proc.on("error", reject);
|
|
71
|
+
proc.on("close", (code) => {
|
|
72
|
+
if (code === 0) resolve();
|
|
73
|
+
else reject(new Error(`docker build exited with code ${code}`));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function ensureSandboxImage(): Promise<void> {
|
|
79
|
+
const expectedSha = getDockerfileSha256();
|
|
80
|
+
|
|
81
|
+
let needsBuild = false;
|
|
82
|
+
try {
|
|
83
|
+
const { stdout } = await execFileAsync("docker", ["image", "inspect", IMAGE_NAME, "--format", `{{index .Config.Labels "${LABEL_KEY}"}}`]);
|
|
84
|
+
if (stdout.trim() !== expectedSha) {
|
|
85
|
+
log.info("sandbox", "Dockerfile.sandbox changed, rebuilding sandbox image...");
|
|
86
|
+
needsBuild = true;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
log.info("sandbox", "Building sandbox image (first time only, may take a minute)...");
|
|
90
|
+
needsBuild = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (needsBuild) {
|
|
94
|
+
await buildImage(expectedSha);
|
|
95
|
+
log.info("sandbox", "Sandbox image built.");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Single source of truth for environment-variable reads.
|
|
2
|
+
//
|
|
3
|
+
// Before this module existed, `process.env.X` calls were sprinkled
|
|
4
|
+
// across 8 files with each call site doing its own type coercion
|
|
5
|
+
// (`Number(process.env.PORT) || 3001`, `process.env.X === "1"`, …).
|
|
6
|
+
// Renaming an env var, changing a default, or auditing what we read
|
|
7
|
+
// from the environment all required grepping the codebase.
|
|
8
|
+
//
|
|
9
|
+
// All env-var reads should now go through `env.*`. The exception is
|
|
10
|
+
// `server/logger/config.ts` which has its own self-contained env
|
|
11
|
+
// reader (`resolveConfig(env)`) — that subsystem stays independent
|
|
12
|
+
// because it's loaded at extremely early bootstrap and accepts an
|
|
13
|
+
// arbitrary `env`-shaped object for testability.
|
|
14
|
+
//
|
|
15
|
+
// `docs/developer.md` lists every env var and what it does; this
|
|
16
|
+
// module is the runtime side of that table.
|
|
17
|
+
|
|
18
|
+
// ── Type coercion helpers ───────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function asInt(value: string | undefined, fallback: number, opts: { min?: number; max?: number } = {}): number {
|
|
21
|
+
if (value === undefined || value === "") return fallback;
|
|
22
|
+
const n = Number(value);
|
|
23
|
+
if (!Number.isInteger(n)) return fallback;
|
|
24
|
+
if (opts.min !== undefined && n < opts.min) return fallback;
|
|
25
|
+
if (opts.max !== undefined && n > opts.max) return fallback;
|
|
26
|
+
return n;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function asFlag(value: string | undefined): boolean {
|
|
30
|
+
// Established convention in this project: env flags are "1"
|
|
31
|
+
// (truthy) vs anything else (falsy). Avoids the trap of
|
|
32
|
+
// `process.env.FOO === "false"` evaluating truthy as a string.
|
|
33
|
+
return value === "1";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function asCsv(value: string | undefined): readonly string[] {
|
|
37
|
+
return Object.freeze(
|
|
38
|
+
(value ?? "")
|
|
39
|
+
.split(",")
|
|
40
|
+
.map((part) => part.trim())
|
|
41
|
+
.filter(Boolean),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Snapshot ────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Frozen snapshot of every env var the app reads, with type coercion
|
|
49
|
+
* and defaults baked in. Read at module load time so tests can
|
|
50
|
+
* import a stable view without re-reading process.env on every
|
|
51
|
+
* access.
|
|
52
|
+
*/
|
|
53
|
+
export const env = Object.freeze({
|
|
54
|
+
// HTTP server
|
|
55
|
+
port: asInt(process.env.PORT, 3001, { min: 0, max: 65_535 }),
|
|
56
|
+
nodeEnv: process.env.NODE_ENV ?? "development",
|
|
57
|
+
isProduction: process.env.NODE_ENV === "production",
|
|
58
|
+
|
|
59
|
+
// Sandbox / Docker
|
|
60
|
+
disableSandbox: asFlag(process.env.DISABLE_SANDBOX),
|
|
61
|
+
// Host-credential opt-ins for the Docker sandbox (#259). Both off
|
|
62
|
+
// by default. See docs/sandbox-credentials.md for the contract.
|
|
63
|
+
sandboxSshAgentForward: asFlag(process.env.SANDBOX_SSH_AGENT_FORWARD),
|
|
64
|
+
sandboxSshAllowedHosts: process.env.SANDBOX_SSH_ALLOWED_HOSTS || "github.com",
|
|
65
|
+
sandboxMountConfigs: asCsv(process.env.SANDBOX_MOUNT_CONFIGS),
|
|
66
|
+
|
|
67
|
+
// API credentials (undefined when not configured)
|
|
68
|
+
geminiApiKey: process.env.GEMINI_API_KEY,
|
|
69
|
+
xBearerToken: process.env.X_BEARER_TOKEN,
|
|
70
|
+
|
|
71
|
+
// Bearer auth token (#272, #316): if set, the server uses this
|
|
72
|
+
// verbatim instead of generating a fresh random token at startup.
|
|
73
|
+
// Matches the env var already honoured by `bridges/_lib/token.ts`
|
|
74
|
+
// and the Vite dev plugin, so pinning on both sides survives a
|
|
75
|
+
// server restart. Undefined / empty → random-per-startup path.
|
|
76
|
+
authTokenOverride: process.env.MULMOCLAUDE_AUTH_TOKEN,
|
|
77
|
+
|
|
78
|
+
// Sessions index API
|
|
79
|
+
sessionsListWindowDays: asInt(process.env.SESSIONS_LIST_WINDOW_DAYS, 90, {
|
|
80
|
+
min: 0,
|
|
81
|
+
}),
|
|
82
|
+
|
|
83
|
+
// Debug-only force-run flags. Off by default; `=1` triggers an
|
|
84
|
+
// immediate run on startup instead of waiting for the scheduled
|
|
85
|
+
// interval.
|
|
86
|
+
journalForceRunOnStartup: asFlag(process.env.JOURNAL_FORCE_RUN_ON_STARTUP),
|
|
87
|
+
chatIndexForceRunOnStartup: asFlag(process.env.CHAT_INDEX_FORCE_RUN_ON_STARTUP),
|
|
88
|
+
|
|
89
|
+
// MulmoBridge Relay (#520). Optional — when both are set the server
|
|
90
|
+
// connects to the Relay via WebSocket and forwards bridge messages.
|
|
91
|
+
relayUrl: process.env.RELAY_URL,
|
|
92
|
+
relayToken: process.env.RELAY_TOKEN,
|
|
93
|
+
|
|
94
|
+
// MCP subprocess: set by the parent server when spawning
|
|
95
|
+
// mcp-server.ts. The MCP process reads them via this same module —
|
|
96
|
+
// OS-level env vars are shared across both processes.
|
|
97
|
+
mcpSessionId: process.env.SESSION_ID ?? "",
|
|
98
|
+
mcpHost: process.env.MCP_HOST ?? "localhost",
|
|
99
|
+
mcpPluginNames: asCsv(process.env.PLUGIN_NAMES),
|
|
100
|
+
mcpRoleIds: asCsv(process.env.ROLE_IDS),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── Derived helpers ─────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/** True iff a Gemini API key is configured. Drives the "image
|
|
106
|
+
* generation available" hint in the UI. */
|
|
107
|
+
export function isGeminiAvailable(): boolean {
|
|
108
|
+
return env.geminiApiKey !== undefined && env.geminiApiKey !== "";
|
|
109
|
+
}
|