clay-server 5.2.0-beta.1 → 5.2.0-beta.10
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/lib/account-auth-socket.js +1 -0
- package/lib/builtin-mates.js +19 -16
- package/lib/bundled-skills/clay-agent-browser/SKILL.md +6 -2
- package/lib/canonical-clay-record-authority.js +15 -0
- package/lib/canonical-clay-scheduling-authority.js +19 -0
- package/lib/claude-hook-installer.js +1 -239
- package/lib/claude-jsonl-watcher.js +1 -204
- package/lib/clay-control-presentation-binding.js +96 -0
- package/lib/clay-control-presentation.js +128 -0
- package/lib/clay-control-target-projection.js +26 -0
- package/lib/clay-mate-chat-receipts.js +26 -0
- package/lib/clay-mate-chat.js +88 -0
- package/lib/clay-mate-knowledge-input.js +26 -0
- package/lib/clay-mate-knowledge.js +95 -0
- package/lib/clay-mate-lifecycle.js +95 -0
- package/lib/clay-product-control-input.js +33 -0
- package/lib/clay-product-control-tools.js +51 -0
- package/lib/clay-product-control.js +247 -0
- package/lib/clay-project-lifecycle.js +154 -0
- package/lib/clay-project-record-input.js +44 -0
- package/lib/clay-project-records.js +84 -0
- package/lib/clay-scheduling-input.js +21 -0
- package/lib/clay-scheduling-receipts.js +26 -0
- package/lib/clay-scheduling.js +119 -0
- package/lib/clay-screen-controls.js +104 -0
- package/lib/clay-skill-install.js +1 -33
- package/lib/clay-workspace-controls.js +84 -0
- package/lib/clay-workspace-input.js +17 -0
- package/lib/clay-workspace-receipts.js +26 -0
- package/lib/cli-sessions.js +1 -300
- package/lib/codex-defaults.js +1 -28
- package/lib/context-usage.js +3 -2
- package/lib/contextual-screen-snapshot.js +38 -0
- package/lib/daemon-project-access.js +11 -0
- package/lib/daemon-project-lifecycle-callbacks.js +15 -0
- package/lib/daemon-project-lifecycle.js +262 -0
- package/lib/daemon-projects.js +1 -1
- package/lib/daemon.js +78 -276
- package/lib/default-ai-runtime.js +4 -8
- package/lib/dm-digest-cursor.js +120 -0
- package/lib/email-mcp-server.js +1 -1
- package/lib/issues-service.js +29 -15
- package/lib/kiro-defaults.js +1 -22
- package/lib/knowledge-db-import-validation.js +46 -0
- package/lib/knowledge-db-import.js +99 -0
- package/lib/knowledge-db-service.js +24 -33
- package/lib/knowledge-db-storage-nedb-maintenance.js +99 -0
- package/lib/knowledge-db-storage-nedb.js +66 -0
- package/lib/knowledge-db-storage.js +72 -0
- package/lib/knowledge-db-validation.js +0 -0
- package/lib/knowledge-document-index.js +2 -1
- package/lib/knowledge-file-store.js +7 -1
- package/lib/knowledge-product-operations.js +46 -0
- package/lib/mate-avatar-catalog.js +12 -0
- package/lib/mate-avatar-migration.js +41 -0
- package/lib/mate-context-service.js +138 -0
- package/lib/mate-context-tools.js +21 -0
- package/lib/mate-creation-mcp-server.js +2 -0
- package/lib/mate-knowledge-canonical-access.js +10 -0
- package/lib/mate-knowledge-mate-bound.js +53 -0
- package/lib/mate-knowledge-mcp-server.js +55 -8
- package/lib/mate-knowledge-service.js +16 -34
- package/lib/mate-knowledge-text-tools.js +106 -0
- package/lib/mate-lifecycle-service.js +94 -0
- package/lib/mate-message-delivery.js +43 -0
- package/lib/mate-ready-creation.js +9 -1
- package/lib/mate-schedule-input.js +29 -0
- package/lib/mate-schedule-lifecycle.js +151 -0
- package/lib/mate-scheduling.js +103 -0
- package/lib/mates.js +18 -6
- package/lib/mcp-agent-tools.js +117 -0
- package/lib/mcp-connection-store.js +119 -15
- package/lib/mcp-import-credentials.js +31 -0
- package/lib/mcp-local-transport.js +13 -0
- package/lib/mcp-oauth.js +8 -6
- package/lib/mcp-proxy.js +2 -32
- package/lib/mcp-query-bridge.js +30 -0
- package/lib/mcp-remote-connections.js +74 -20
- package/lib/mcp-source-adapters.js +1 -0
- package/lib/mcp-source-context.js +1 -0
- package/lib/mcp-source-discovery.js +1 -0
- package/lib/mcp-source-plugins.js +1 -0
- package/lib/mcp-source-restrictions.js +1 -0
- package/lib/mcp-tool-policy.js +9 -0
- package/lib/notes-checklist.js +50 -0
- package/lib/notes.js +49 -3
- package/lib/os-user-diagnostics.js +2 -4
- package/lib/os-users.js +24 -70
- package/lib/pane-pending-interactions.js +20 -0
- package/lib/pane-session-activation.js +114 -0
- package/lib/pane-session-transport.js +26 -0
- package/lib/project-admin-overview.js +51 -0
- package/lib/project-ask-user.js +8 -4
- package/lib/project-connection.js +14 -10
- package/lib/project-control-git-view.js +12 -0
- package/lib/project-debate.js +1 -0
- package/lib/project-delegated-follow-up.js +2 -1
- package/lib/project-directory-claim.js +65 -0
- package/lib/project-file-operations.js +61 -0
- package/lib/project-filesystem.js +15 -85
- package/lib/project-full-access.js +23 -2
- package/lib/project-http.js +7 -4
- package/lib/project-knowledge.js +32 -18
- package/lib/project-lifecycle-operations.js +98 -0
- package/lib/project-lifecycle-process-witness.js +32 -0
- package/lib/project-lifecycle-runtime.js +50 -0
- package/lib/project-linux-access.js +22 -0
- package/lib/project-logs-canonical-clay.js +27 -0
- package/lib/project-logs-clay-query.js +16 -0
- package/lib/project-logs-schema.js +4 -2
- package/lib/project-logs-service.js +8 -7
- package/lib/project-logs.js +1 -0
- package/lib/project-loop.js +1 -0
- package/lib/project-mate-chat-control.js +55 -0
- package/lib/project-mate-creation-proposal.js +18 -3
- package/lib/project-mate-interaction.js +40 -62
- package/lib/project-mcp.js +54 -16
- package/lib/project-models.js +14 -31
- package/lib/project-notifications.js +76 -17
- package/lib/project-ownership.js +77 -0
- package/lib/project-pair-result-delivery.js +14 -3
- package/lib/project-pair-usage.js +12 -13
- package/lib/project-pending-message-queue.js +4 -1
- package/lib/project-product-control.js +89 -0
- package/lib/project-product-session-queue.js +46 -0
- package/lib/project-push-access.js +28 -0
- package/lib/project-scheduled-message-queue.js +30 -0
- package/lib/project-scheduled-messages.js +67 -68
- package/lib/project-scheduled-task-control.js +83 -0
- package/lib/project-scheduled-tasks.js +13 -2
- package/lib/project-scheduling-access.js +10 -0
- package/lib/project-session-actions.js +65 -0
- package/lib/project-session-delete.js +8 -1
- package/lib/project-session-document.js +27 -7
- package/lib/project-session-github.js +63 -19
- package/lib/project-session-history-actions.js +167 -0
- package/lib/project-session-linear.js +14 -6
- package/lib/project-session-notes.js +7 -3
- package/lib/project-session-pair.js +1 -1
- package/lib/project-session-runtime-actions.js +64 -0
- package/lib/project-session-spawn.js +2 -3
- package/lib/project-session-vendor.js +55 -0
- package/lib/project-sessions.js +103 -272
- package/lib/project-shared-browser.js +78 -17
- package/lib/project-studio-presentation.js +58 -0
- package/lib/project-user-message.js +67 -1
- package/lib/project-vendor-login.js +6 -36
- package/lib/project-workspace-operations.js +126 -0
- package/lib/project-workspace-query.js +19 -2
- package/lib/project.js +178 -45
- package/lib/public/app.js +46 -0
- package/lib/public/appearance-bootstrap.js +16 -0
- package/lib/public/css/clay-tool-activity.css +51 -0
- package/lib/public/css/clay-workspace.css +29 -0
- package/lib/public/css/command-palette.css +12 -33
- package/lib/public/css/contextual-clay.css +240 -0
- package/lib/public/css/home-debate-planning.css +2 -0
- package/lib/public/css/home-mate-creation.css +5 -0
- package/lib/public/css/input.css +6 -0
- package/lib/public/css/mate-avatar-gallery.css +12 -0
- package/lib/public/css/mate-conversation.css +10 -5
- package/lib/public/css/messages.css +6 -2
- package/lib/public/css/overlays.css +4 -2
- package/lib/public/css/project-admin-overview.css +13 -0
- package/lib/public/css/session-folders.css +11 -0
- package/lib/public/css/session-github.css +46 -1
- package/lib/public/css/shared-browser.css +35 -1
- package/lib/public/css/sticky-notes.css +87 -1
- package/lib/public/index.html +11 -5
- package/lib/public/mate-avatars/B01.png +0 -0
- package/lib/public/mate-avatars/B02.png +0 -0
- package/lib/public/mate-avatars/B03.png +0 -0
- package/lib/public/mate-avatars/B04.png +0 -0
- package/lib/public/mate-avatars/B05.png +0 -0
- package/lib/public/mate-avatars/B06.png +0 -0
- package/lib/public/mate-avatars/B07.png +0 -0
- package/lib/public/mate-avatars/B08.png +0 -0
- package/lib/public/mate-avatars/B09.png +0 -0
- package/lib/public/mate-avatars/B10.png +0 -0
- package/lib/public/mate-avatars/B11.png +0 -0
- package/lib/public/mate-avatars/B12.png +0 -0
- package/lib/public/mate-avatars/B13.png +0 -0
- package/lib/public/mate-avatars/B14.png +0 -0
- package/lib/public/mate-avatars/B15.png +0 -0
- package/lib/public/mate-avatars/B16.png +0 -0
- package/lib/public/mate-avatars/B17.png +0 -0
- package/lib/public/mate-avatars/B18.png +0 -0
- package/lib/public/mate-avatars/B19.png +0 -0
- package/lib/public/mate-avatars/B20.png +0 -0
- package/lib/public/mate-avatars/B21.png +0 -0
- package/lib/public/mate-avatars/B22.png +0 -0
- package/lib/public/mate-avatars/B23.png +0 -0
- package/lib/public/mate-avatars/B24.png +0 -0
- package/lib/public/mate-avatars/B25.png +0 -0
- package/lib/public/mate-avatars/B26.png +0 -0
- package/lib/public/mate-avatars/B27.png +0 -0
- package/lib/public/mate-avatars/B28.png +0 -0
- package/lib/public/mate-avatars/B29.png +0 -0
- package/lib/public/mate-avatars/B30.png +0 -0
- package/lib/public/mate-avatars/B31.png +0 -0
- package/lib/public/mate-avatars/B32.png +0 -0
- package/lib/public/mate-avatars/B33.png +0 -0
- package/lib/public/mate-avatars/B34.png +0 -0
- package/lib/public/mate-avatars/B35.png +0 -0
- package/lib/public/mate-avatars/B36.png +0 -0
- package/lib/public/mate-avatars/B37.png +0 -0
- package/lib/public/mate-avatars/B38.png +0 -0
- package/lib/public/mate-avatars/B39.png +0 -0
- package/lib/public/mate-avatars/B40.png +0 -0
- package/lib/public/mate-avatars/B41.png +0 -0
- package/lib/public/mate-avatars/B42.png +0 -0
- package/lib/public/mate-avatars/B43.png +0 -0
- package/lib/public/mate-avatars/B44.png +0 -0
- package/lib/public/mate-avatars/B45.png +0 -0
- package/lib/public/mate-avatars/B46.png +0 -0
- package/lib/public/mate-avatars/B47.png +0 -0
- package/lib/public/mate-avatars/B48.png +0 -0
- package/lib/public/mate-avatars/C01.png +0 -0
- package/lib/public/mate-avatars/C02.png +0 -0
- package/lib/public/mate-avatars/C03.png +0 -0
- package/lib/public/mate-avatars/C04.png +0 -0
- package/lib/public/mate-avatars/C05.png +0 -0
- package/lib/public/mate-avatars/C06.png +0 -0
- package/lib/public/mate-avatars/C07.png +0 -0
- package/lib/public/mate-avatars/C08.png +0 -0
- package/lib/public/mate-avatars/C09.png +0 -0
- package/lib/public/mate-avatars/C10.png +0 -0
- package/lib/public/mate-avatars/C11.png +0 -0
- package/lib/public/mate-avatars/C12.png +0 -0
- package/lib/public/mate-avatars/C13.png +0 -0
- package/lib/public/mate-avatars/C14.png +0 -0
- package/lib/public/mate-avatars/C15.png +0 -0
- package/lib/public/mate-avatars/C16.png +0 -0
- package/lib/public/mate-avatars/C17.png +0 -0
- package/lib/public/mate-avatars/C18.png +0 -0
- package/lib/public/mate-avatars/C19.png +0 -0
- package/lib/public/mate-avatars/C20.png +0 -0
- package/lib/public/mate-avatars/C21.png +0 -0
- package/lib/public/mate-avatars/C22.png +0 -0
- package/lib/public/mate-avatars/C23.png +0 -0
- package/lib/public/mate-avatars/C24.png +0 -0
- package/lib/public/mate-avatars/C25.png +0 -0
- package/lib/public/mate-avatars/C26.png +0 -0
- package/lib/public/mate-avatars/C27.png +0 -0
- package/lib/public/mate-avatars/C28.png +0 -0
- package/lib/public/mate-avatars/C29.png +0 -0
- package/lib/public/mate-avatars/C30.png +0 -0
- package/lib/public/mate-avatars/C31.png +0 -0
- package/lib/public/mate-avatars/C32.png +0 -0
- package/lib/public/mate-avatars/C33.png +0 -0
- package/lib/public/mate-avatars/C34.png +0 -0
- package/lib/public/mate-avatars/C35.png +0 -0
- package/lib/public/mate-avatars/C36.png +0 -0
- package/lib/public/mate-avatars/C37.png +0 -0
- package/lib/public/mate-avatars/C38.png +0 -0
- package/lib/public/mate-avatars/C39.png +0 -0
- package/lib/public/mate-avatars/C40.png +0 -0
- package/lib/public/mate-avatars/C41.png +0 -0
- package/lib/public/mate-avatars/C42.png +0 -0
- package/lib/public/mate-avatars/C43.png +0 -0
- package/lib/public/mate-avatars/C44.png +0 -0
- package/lib/public/mate-avatars/C45.png +0 -0
- package/lib/public/mate-avatars/C46.png +0 -0
- package/lib/public/mate-avatars/C47.png +0 -0
- package/lib/public/mate-avatars/C48.png +0 -0
- package/lib/public/mate-avatars/catalog.json +482 -0
- package/lib/public/mate-avatars/lead-clay.png +0 -0
- package/lib/public/modules/admin-settings-projects.js +6 -2
- package/lib/public/modules/app-connection.js +11 -1
- package/lib/public/modules/app-favicon.js +18 -9
- package/lib/public/modules/app-header.js +7 -4
- package/lib/public/modules/app-loop-ui.js +2 -0
- package/lib/public/modules/app-message-router.js +22 -1
- package/lib/public/modules/app-messages.js +115 -17
- package/lib/public/modules/app-misc.js +3 -1
- package/lib/public/modules/app-notifications.js +11 -5
- package/lib/public/modules/app-panels.js +61 -123
- package/lib/public/modules/app-projects.js +13 -0
- package/lib/public/modules/app-rate-limit.js +7 -27
- package/lib/public/modules/app-rendering.js +4 -3
- package/lib/public/modules/avatar.js +15 -41
- package/lib/public/modules/chat-render-runtime.js +20 -0
- package/lib/public/modules/clay-browser-links.js +47 -0
- package/lib/public/modules/clay-control-activity.js +214 -0
- package/lib/public/modules/clay-control-copy.js +36 -0
- package/lib/public/modules/clay-control-detail-targets.js +44 -0
- package/lib/public/modules/clay-control-object-targets.js +52 -0
- package/lib/public/modules/clay-control-target-visibility.js +12 -0
- package/lib/public/modules/clay-control-targets.js +44 -0
- package/lib/public/modules/clay-log-links.js +23 -0
- package/lib/public/modules/clay-navigation-render.js +52 -0
- package/lib/public/modules/clay-owner-navigation.js +33 -0
- package/lib/public/modules/clay-session-links.js +31 -0
- package/lib/public/modules/clay-studio-bridge.js +20 -0
- package/lib/public/modules/clay-studio-document.js +67 -0
- package/lib/public/modules/clay-studio-render.js +146 -0
- package/lib/public/modules/clay-tool-activity.js +140 -0
- package/lib/public/modules/clay-tool-presentation.js +342 -0
- package/lib/public/modules/command-palette.js +162 -40
- package/lib/public/modules/contextual-ask-session.js +26 -0
- package/lib/public/modules/contextual-clay-geometry.js +155 -0
- package/lib/public/modules/contextual-clay-session.js +130 -0
- package/lib/public/modules/contextual-clay-workspace.js +169 -0
- package/lib/public/modules/contextual-clay.js +250 -0
- package/lib/public/modules/contextual-composer.js +91 -0
- package/lib/public/modules/contextual-mate-context.js +117 -0
- package/lib/public/modules/contextual-mate-hint.js +40 -0
- package/lib/public/modules/contextual-mate-identity.js +23 -0
- package/lib/public/modules/contextual-mate-sessions.js +134 -0
- package/lib/public/modules/contextual-search.js +137 -0
- package/lib/public/modules/contextual-session-citations.js +58 -0
- package/lib/public/modules/debate.js +1 -0
- package/lib/public/modules/default-ai.js +11 -3
- package/lib/public/modules/elicitation-form.js +5 -0
- package/lib/public/modules/filebrowser.js +20 -1
- package/lib/public/modules/generation-stop.js +8 -5
- package/lib/public/modules/git-panel.js +3 -1
- package/lib/public/modules/history-replay-batch.js +13 -1
- package/lib/public/modules/home-debate-live.js +2 -0
- package/lib/public/modules/home-debate-planning.js +7 -7
- package/lib/public/modules/home-mate-avatar-editor.js +127 -208
- package/lib/public/modules/home-mate-chat.js +8 -6
- package/lib/public/modules/home-mate-creation.js +73 -1
- package/lib/public/modules/home-mate-settings.js +19 -12
- package/lib/public/modules/icon-strip-tooltip.js +2 -1
- package/lib/public/modules/input.js +88 -10
- package/lib/public/modules/issues-render.js +3 -2
- package/lib/public/modules/knowledge-db-workbench.js +4 -4
- package/lib/public/modules/markdown.js +6 -34
- package/lib/public/modules/mate-avatar-catalog.js +97 -0
- package/lib/public/modules/mate-knowledge-workbench.js +1 -1
- package/lib/public/modules/mate-management.js +40 -7
- package/lib/public/modules/mcp-connection-view.js +31 -13
- package/lib/public/modules/mcp-discovery-view.js +31 -0
- package/lib/public/modules/mcp-ui.js +23 -4
- package/lib/public/modules/mention.js +3 -0
- package/lib/public/modules/message-delivery.js +20 -2
- package/lib/public/modules/model-picker.js +2 -0
- package/lib/public/modules/pane-activation.js +214 -0
- package/lib/public/modules/pane-binding.js +82 -0
- package/lib/public/modules/pane-bridge.js +7 -4
- package/lib/public/modules/pane-browser-bridge.js +22 -0
- package/lib/public/modules/pane-file-bridge.js +4 -0
- package/lib/public/modules/pane-issue-bridge.js +4 -1
- package/lib/public/modules/pending-message-queue.js +1 -0
- package/lib/public/modules/permission-control.js +5 -4
- package/lib/public/modules/personal-settings-dialog.js +7 -1
- package/lib/public/modules/product-review.js +26 -0
- package/lib/public/modules/profile.js +1 -238
- package/lib/public/modules/project-access-recovery.js +17 -0
- package/lib/public/modules/project-admin-overview.js +21 -0
- package/lib/public/modules/project-chat-identity.js +13 -4
- package/lib/public/modules/project-logs-render.js +3 -0
- package/lib/public/modules/project-mate-navigation.js +144 -22
- package/lib/public/modules/rewind.js +5 -0
- package/lib/public/modules/scheduled-tasks.js +2 -0
- package/lib/public/modules/scheduler.js +2 -0
- package/lib/public/modules/search-clay-chat.js +5 -440
- package/lib/public/modules/session-actions.js +5 -2
- package/lib/public/modules/session-context-menu.js +5 -0
- package/lib/public/modules/session-create-form.js +10 -10
- package/lib/public/modules/session-folder-layout.js +1 -1
- package/lib/public/modules/session-folder-view-menu.js +9 -0
- package/lib/public/modules/session-github.js +85 -14
- package/lib/public/modules/session-rich-drafts.js +21 -0
- package/lib/public/modules/session-switch-defer.js +67 -0
- package/lib/public/modules/session-work-primary.js +51 -0
- package/lib/public/modules/settings-defaults.js +27 -28
- package/lib/public/modules/shared-browser-settings.js +112 -0
- package/lib/public/modules/shared-browser-tabs.js +13 -3
- package/lib/public/modules/shared-browser.js +29 -6
- package/lib/public/modules/sidebar-mobile.js +4 -3
- package/lib/public/modules/sidebar-projects.js +1 -0
- package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
- package/lib/public/modules/sidebar-sessions.js +1 -0
- package/lib/public/modules/split-group-restore.js +68 -0
- package/lib/public/modules/split-pair-ui.js +1 -0
- package/lib/public/modules/split-pane-reconciler.js +15 -0
- package/lib/public/modules/split-pane-renderer.js +87 -6
- package/lib/public/modules/split-session-boundary.js +1 -1
- package/lib/public/modules/split-view.js +60 -68
- package/lib/public/modules/sticky-note-markdown.js +72 -6
- package/lib/public/modules/sticky-notes-actions.js +30 -0
- package/lib/public/modules/sticky-notes-browser.js +9 -2
- package/lib/public/modules/sticky-notes-card.js +23 -7
- package/lib/public/modules/sticky-notes-checklist.js +116 -0
- package/lib/public/modules/sticky-notes-editor.js +108 -44
- package/lib/public/modules/sticky-notes-shared.js +50 -2
- package/lib/public/modules/sticky-notes-state.js +66 -0
- package/lib/public/modules/sticky-notes.js +34 -51
- package/lib/public/modules/stt-controller.js +4 -1
- package/lib/public/modules/stt-recording.js +1 -1
- package/lib/public/modules/stt.js +7 -2
- package/lib/public/modules/terminal.js +9 -0
- package/lib/public/modules/theme.js +8 -1
- package/lib/public/modules/thinking-lifecycle.js +33 -3
- package/lib/public/modules/thinking-view.js +23 -1
- package/lib/public/modules/tools.js +60 -11
- package/lib/public/modules/tui-grab.js +1 -2
- package/lib/public/modules/user-settings-appearance.js +10 -3
- package/lib/public/modules/user-settings-editors.js +1 -1
- package/lib/public/modules/user-settings-layout.js +11 -2
- package/lib/public/modules/user-settings-preferences.js +6 -2
- package/lib/public/modules/user-settings-profile.js +12 -2
- package/lib/public/modules/user-settings.js +3 -2
- package/lib/public/modules/vendor-login-browser.js +5 -3
- package/lib/public/modules/vendor-login.js +2 -2
- package/lib/public/modules/vendor-priority.js +15 -1
- package/lib/public/modules/wireframe-workbench.js +5 -2
- package/lib/public/muse-avatar.svg +4 -0
- package/lib/public/style.css +5 -0
- package/lib/public/sw.js +3 -1
- package/lib/safe-bash-commands.js +3 -45
- package/lib/scheduled-task-records.js +3 -1
- package/lib/scheduler.js +2 -0
- package/lib/sdk-bridge.js +342 -401
- package/lib/sdk-foreground-admission.js +16 -0
- package/lib/sdk-mcp-descriptors.js +2 -37
- package/lib/sdk-message-processor.js +52 -17
- package/lib/sdk-quota-snapshot.js +1 -1
- package/lib/sdk-response-recovery.js +1 -0
- package/lib/sdk-worker.js +2 -677
- package/lib/server-admin.js +63 -16
- package/lib/server-appearance-settings.js +13 -1
- package/lib/server-contextual-clay-settings.js +40 -0
- package/lib/server-contextual-mate-sessions.js +70 -0
- package/lib/server-dm.js +2 -1
- package/lib/server-global-ws.js +10 -20
- package/lib/server-home-chat-events.js +18 -4
- package/lib/server-home-chat.js +22 -20
- package/lib/server-home-clay-entry.js +23 -4
- package/lib/server-home-clay-session-links.js +29 -9
- package/lib/server-home-mate-creation.js +1 -0
- package/lib/server-home-mcp-consent.js +14 -0
- package/lib/server-mates.js +25 -62
- package/lib/server-owner-ui.js +118 -0
- package/lib/server-path-access.js +38 -0
- package/lib/server-project-removal.js +22 -0
- package/lib/server-settings.js +6 -1
- package/lib/server.js +163 -43
- package/lib/session-config-transition.js +45 -0
- package/lib/session-folders.js +7 -2
- package/lib/session-fork-history.js +21 -0
- package/lib/session-github.js +9 -2
- package/lib/session-hygiene.js +3 -35
- package/lib/session-permission-mode.js +4 -3
- package/lib/session-skill-scope.js +11 -0
- package/lib/session-visibility.js +5 -2
- package/lib/session-work-primary.js +50 -0
- package/lib/sessions.js +64 -99
- package/lib/shared-browser-cookie-sync.js +32 -0
- package/lib/shared-browser-cookies.js +79 -0
- package/lib/shared-browser-runtime.js +25 -9
- package/lib/shared-browser-storage-sync.js +43 -0
- package/lib/shared-browser-storage.js +101 -0
- package/lib/shared-browser-tools.js +2 -1
- package/lib/shared-browser-worker.js +46 -10
- package/lib/terminal-manager.js +65 -6
- package/lib/tool-llm.js +1 -1
- package/lib/tui-transcript-index.js +1 -132
- package/lib/users-contextual-clay-preferences.js +31 -0
- package/lib/users-permissions.js +2 -8
- package/lib/users.js +7 -0
- package/lib/vendor-login-browser.js +1 -11
- package/lib/workspace-assignment-service.js +25 -0
- package/lib/workspace-query-access.js +4 -1
- package/lib/workspace-query-mcp-server.js +1 -1
- package/lib/workspace-query-service.js +13 -2
- package/lib/ws-request.js +1 -0
- package/lib/ws-schema.js +18 -2
- package/lib/yoke/acp-agent-profiles.js +7 -5
- package/lib/yoke/adapters/antigravity.js +3 -14
- package/lib/yoke/adapters/claude-query-contract.js +9 -0
- package/lib/yoke/adapters/claude.js +54 -50
- package/lib/yoke/adapters/codex-query-contract.js +24 -0
- package/lib/yoke/adapters/codex.js +53 -111
- package/lib/yoke/adapters/kiro.js +3 -1
- package/lib/yoke/adapters/muse-query-contract.js +7 -0
- package/lib/yoke/adapters/muse.js +122 -0
- package/lib/yoke/auth-events.js +17 -0
- package/lib/yoke/claude-hook-installer.js +271 -0
- package/lib/yoke/claude-jsonl-watcher.js +204 -0
- package/lib/yoke/claude-maintenance.js +55 -0
- package/lib/yoke/claude-permission-patterns.js +53 -0
- package/lib/yoke/claude-response-recovery.js +155 -0
- package/lib/yoke/claude-transcript-hygiene.js +34 -0
- package/lib/yoke/claude-worker-startup.js +28 -0
- package/lib/yoke/clay-skill-install.js +33 -0
- package/lib/yoke/cli-sessions.js +300 -0
- package/lib/yoke/codex-app-server.js +7 -12
- package/lib/yoke/codex-idle-recovery.js +81 -1
- package/lib/yoke/codex-recovery-admission.js +164 -0
- package/lib/yoke/configuration.js +13 -0
- package/lib/yoke/discovery.js +70 -0
- package/lib/yoke/errors.js +8 -0
- package/lib/yoke/host-environment.js +7 -0
- package/lib/yoke/index.js +9 -51
- package/lib/yoke/interface.js +116 -0
- package/lib/yoke/kiro-acp-server.js +5 -5
- package/lib/yoke/legacy-sessions.js +89 -0
- package/lib/yoke/login-protocol.js +25 -0
- package/lib/yoke/maintenance.js +12 -0
- package/lib/yoke/mcp-proxy.js +32 -0
- package/lib/yoke/mcp-source-adapters.js +91 -0
- package/lib/yoke/mcp-source-context.js +42 -0
- package/lib/yoke/mcp-source-discovery.js +169 -0
- package/lib/yoke/mcp-source-plugins.js +68 -0
- package/lib/yoke/mcp-source-restrictions.js +47 -0
- package/lib/yoke/mcp-tool-policy-normalize.js +28 -0
- package/lib/yoke/model-preferences.js +6 -0
- package/lib/yoke/muse-artifacts.js +36 -0
- package/lib/yoke/muse-controls.js +50 -0
- package/lib/yoke/muse-input.js +64 -0
- package/lib/yoke/muse-items.js +157 -0
- package/lib/yoke/muse-policy.js +52 -0
- package/lib/yoke/muse-query.js +428 -0
- package/lib/yoke/muse-runtime.js +211 -0
- package/lib/yoke/muse-services.js +79 -0
- package/lib/yoke/muse-skills.js +79 -0
- package/lib/yoke/muse-turns.js +71 -0
- package/lib/yoke/options/acp.js +2 -0
- package/lib/yoke/options/antigravity.js +2 -0
- package/lib/yoke/options/claude.js +21 -0
- package/lib/yoke/options/codex-defaults.js +29 -0
- package/lib/yoke/options/codex-settings.js +21 -0
- package/lib/yoke/options/codex.js +6 -0
- package/lib/yoke/options/kiro-defaults.js +22 -0
- package/lib/yoke/options/kiro.js +5 -0
- package/lib/yoke/options/muse.js +5 -0
- package/lib/yoke/provider-contract.js +5 -0
- package/lib/yoke/providers/antigravity-executable.js +8 -0
- package/lib/yoke/providers/claude-executable.js +40 -0
- package/lib/yoke/providers/claude-model-descriptions.js +14 -0
- package/lib/yoke/providers/claude-process.js +97 -0
- package/lib/yoke/providers/claude-provision.js +75 -0
- package/lib/yoke/providers/codex-errors.js +8 -0
- package/lib/yoke/providers/muse-executable.js +17 -0
- package/lib/yoke/provisioning.js +6 -0
- package/lib/yoke/query-options.js +10 -0
- package/lib/yoke/response-recovery.js +41 -0
- package/lib/yoke/sdk-mcp-descriptors.js +37 -0
- package/lib/yoke/sdk-server-factory.js +7 -0
- package/lib/yoke/session-history.js +8 -0
- package/lib/yoke/session-tool-transport.js +34 -13
- package/lib/yoke/settings.js +23 -0
- package/lib/yoke/source-locations.js +4 -0
- package/lib/yoke/title-query.js +18 -7
- package/lib/yoke/tui-transcript-index.js +132 -0
- package/lib/yoke/usage.js +48 -0
- package/lib/yoke/user-input.js +13 -0
- package/lib/yoke/vendor-registry.js +81 -1
- package/package.json +10 -4
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
var crypto = require('crypto');
|
|
2
|
+
var rule = require('./schedule-rule');
|
|
3
|
+
|
|
4
|
+
function copy(value) { return JSON.parse(JSON.stringify(value)); }
|
|
5
|
+
function attachMateScheduleLifecycle(ctx) {
|
|
6
|
+
var now = ctx.now || Date.now;
|
|
7
|
+
function save(session, records) {
|
|
8
|
+
var previous = session.mateSchedules;
|
|
9
|
+
session.mateSchedules = records;
|
|
10
|
+
if (ctx.sm.saveSessionFile(session) !== true) {
|
|
11
|
+
session.mateSchedules = previous;
|
|
12
|
+
throw new Error('The Mate schedule could not be saved.');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function records(session) { return copy(session.mateSchedules || []); }
|
|
16
|
+
function find(session, id) {
|
|
17
|
+
return records(session).find(function (record) { return record.id === id; }) || null;
|
|
18
|
+
}
|
|
19
|
+
function replace(session, record) {
|
|
20
|
+
var all = records(session);
|
|
21
|
+
var index = all.findIndex(function (item) { return item.id === record.id; });
|
|
22
|
+
if (index < 0) all.push(record); else all[index] = record;
|
|
23
|
+
save(session, all);
|
|
24
|
+
}
|
|
25
|
+
function jobFor(record) {
|
|
26
|
+
return ctx.scheduler.listJobs().reverse().find(function (job) {
|
|
27
|
+
return job.projectId === ctx.slug && job.payload && job.payload.mateScheduleId === record.id &&
|
|
28
|
+
job.payload.logicalRevision === record.revision && job.payload.occurrence === record.occurrence;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function current(session, job) {
|
|
32
|
+
var record = job && job.payload && find(session, job.payload.mateScheduleId);
|
|
33
|
+
if (record && record.createdBy && record.createdBy.type === 'canonicalClay') { try { (ctx.authorizeRuntime || ctx.authorize)(session); } catch (error) { return false; } }
|
|
34
|
+
return !!(record && record.state === 'active' && record.revision === job.payload.logicalRevision &&
|
|
35
|
+
record.occurrence === job.payload.occurrence && record.ownerId === job.ownerId);
|
|
36
|
+
}
|
|
37
|
+
function admit(session, record) {
|
|
38
|
+
if (record.state !== 'active') return null;
|
|
39
|
+
var existing = jobFor(record);
|
|
40
|
+
if (existing && !(existing.state === 'cancelled' && existing.cancelledReason === 'queue_admission_failed')) {
|
|
41
|
+
if (['failed', 'cancelled', 'interrupted'].indexOf(existing.state) >= 0) throw new Error('The occurrence requires recovery; it was not scheduled.');
|
|
42
|
+
return existing;
|
|
43
|
+
}
|
|
44
|
+
var job = ctx.schedule(session, record.message, Math.max(record.runAt, now() + 1),
|
|
45
|
+
{ id: record.ownerId }, null, null, 'mate-' + record.id + '-' + record.revision + '-' + record.occurrence,
|
|
46
|
+
null, { exact: true, scheduleRule: record.rule, mateScheduleId: record.id,
|
|
47
|
+
logicalScheduleId: record.id, logicalRevision: record.revision, occurrence: record.occurrence });
|
|
48
|
+
if (!job) throw new Error('The occurrence could not be admitted. The saved schedule needs retry.');
|
|
49
|
+
return job;
|
|
50
|
+
}
|
|
51
|
+
function create(session, value, id, control) {
|
|
52
|
+
var actor = ctx.authorize(session, control);
|
|
53
|
+
var existing = find(session, id);
|
|
54
|
+
if (existing) { if (existing.message !== value.message || JSON.stringify(existing.rule) !== JSON.stringify(value.rule)) throw new Error('Schedule request already names another intent.'); admit(session, existing); return existing; }
|
|
55
|
+
var runAt = rule.nextScheduleRuleTime(value.rule, now(), [], {});
|
|
56
|
+
if (!Number.isFinite(runAt)) throw new Error('The schedule time has expired.');
|
|
57
|
+
var record = { id: id, revision: crypto.randomUUID(), ownerId: ctx.owner(session), message: value.message,
|
|
58
|
+
createdBy: actor || null, updatedBy: actor || null, rule: value.rule, state: 'active', occurrence: 0, count: 0, dateCounts: {}, runAt: runAt };
|
|
59
|
+
replace(session, record);
|
|
60
|
+
admit(session, record);
|
|
61
|
+
return record;
|
|
62
|
+
}
|
|
63
|
+
function checked(session, args, control) {
|
|
64
|
+
ctx.authorize(session, control);
|
|
65
|
+
var record = find(session, args.id);
|
|
66
|
+
if (!record || record.ownerId !== ctx.owner(session)) throw new Error('Schedule not found.');
|
|
67
|
+
if (args.revision && args.revision !== record.revision) throw new Error('The schedule revision is stale.');
|
|
68
|
+
return record;
|
|
69
|
+
}
|
|
70
|
+
function cancel(session, args, control) {
|
|
71
|
+
var record = checked(session, args, control);
|
|
72
|
+
var actor = ctx.authorize(session, control);
|
|
73
|
+
if (record.state === 'cancelled') return record;
|
|
74
|
+
// Persist the tombstone before touching occurrences. Dispatch rechecks it.
|
|
75
|
+
record.state = 'cancelled';
|
|
76
|
+
if (actor) record.updatedBy = actor;
|
|
77
|
+
replace(session, record);
|
|
78
|
+
var job = jobFor(record);
|
|
79
|
+
if (job && job.state === 'queued' && !ctx.cancelJob(session, job)) throw new Error('Cancellation was saved, but occurrence cleanup failed.');
|
|
80
|
+
return record;
|
|
81
|
+
}
|
|
82
|
+
function update(session, args, value, control) {
|
|
83
|
+
var previous = checked(session, args, control);
|
|
84
|
+
var actor = ctx.authorize(session, control);
|
|
85
|
+
if (previous.state !== 'active') throw new Error('Only active schedules can be updated.');
|
|
86
|
+
var runAt = rule.nextScheduleRuleTime(value.rule, now(), [], {});
|
|
87
|
+
if (!Number.isFinite(runAt)) throw new Error('The schedule time has expired.');
|
|
88
|
+
var record = Object.assign({}, previous, { revision: crypto.randomUUID(), updatedBy: actor || previous.updatedBy || null, message: value.message,
|
|
89
|
+
rule: value.rule, count: 0, dateCounts: {}, occurrence: 0, runAt: runAt });
|
|
90
|
+
replace(session, record);
|
|
91
|
+
var oldJob = jobFor(previous);
|
|
92
|
+
if (oldJob && oldJob.state === 'queued' && !ctx.cancelJob(session, oldJob)) throw new Error('Update saved, but previous occurrence cleanup failed.');
|
|
93
|
+
admit(session, record);
|
|
94
|
+
return record;
|
|
95
|
+
}
|
|
96
|
+
function consumed(session, job) {
|
|
97
|
+
if (!current(session, job)) return false;
|
|
98
|
+
var record = find(session, job.payload.mateScheduleId);
|
|
99
|
+
record.count++;
|
|
100
|
+
var date = new Date(record.runAt);
|
|
101
|
+
var key = date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0');
|
|
102
|
+
record.dateCounts[key] = Number(record.dateCounts[key] || 0) + 1;
|
|
103
|
+
// Coalesce missed times: deliver the admitted occurrence once, then choose
|
|
104
|
+
// the next future time. Counts represent deliveries, not missed slots.
|
|
105
|
+
record.runAt = rule.nextScheduleRuleTime(record.rule, Math.max(record.runAt, now()), [],
|
|
106
|
+
{ startCount: record.count, dateStartCounts: record.dateCounts });
|
|
107
|
+
record.occurrence++;
|
|
108
|
+
if (!Number.isFinite(record.runAt)) record.state = 'completed';
|
|
109
|
+
replace(session, record);
|
|
110
|
+
admit(session, record);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
function reconcile(session) {
|
|
114
|
+
records(session).forEach(function (record) {
|
|
115
|
+
if (record.state !== 'active') return;
|
|
116
|
+
(ctx.authorizeRuntime || ctx.authorize)(session);
|
|
117
|
+
var job = jobFor(record);
|
|
118
|
+
var item = job && ctx.queueItem(session, job);
|
|
119
|
+
if (item && item.state === 'consumed') consumed(session, job);
|
|
120
|
+
else if (item && (item.state === 'cancelled' || item.state === 'failed')) {
|
|
121
|
+
record.state = item.state;
|
|
122
|
+
replace(session, record);
|
|
123
|
+
} else if (job && ['failed', 'cancelled', 'interrupted'].indexOf(job.state) >= 0 && (!item || item.state !== 'pending')) {
|
|
124
|
+
record.state = 'interrupted';
|
|
125
|
+
replace(session, record);
|
|
126
|
+
} else admit(session, record);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function onConsumed(session, job) {
|
|
130
|
+
try { return consumed(session, job); }
|
|
131
|
+
catch (error) {
|
|
132
|
+
// Queue consumption is already durable. Keep the accepted provider turn
|
|
133
|
+
// running; reconciliation repairs the saved next occurrence after restart.
|
|
134
|
+
ctx.sm.sendAndRecord(session, { type: 'error', text: 'The scheduled message was delivered, but its next occurrence needs recovery: ' + error.message });
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { create: create, read: checked, list: function (session, control) { ctx.authorize(session, control); return records(session); },
|
|
139
|
+
update: update, cancel: cancel, failed: function (session, job, error) {
|
|
140
|
+
if (!current(session, job)) return;
|
|
141
|
+
var record = find(session, job.payload.mateScheduleId);
|
|
142
|
+
record.state = 'failed'; record.error = error.message;
|
|
143
|
+
replace(session, record);
|
|
144
|
+
}, cancelled: function (session, job) {
|
|
145
|
+
if (!current(session, job)) return;
|
|
146
|
+
var record = find(session, job.payload.mateScheduleId);
|
|
147
|
+
record.state = 'cancelled';
|
|
148
|
+
replace(session, record);
|
|
149
|
+
}, current: current, consumed: onConsumed, reconcile: reconcile };
|
|
150
|
+
}
|
|
151
|
+
module.exports = { attachMateScheduleLifecycle: attachMateScheduleLifecycle };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
var crypto = require('crypto');
|
|
2
|
+
var userInput = require('./yoke/user-input');
|
|
3
|
+
var input = require('./mate-schedule-input');
|
|
4
|
+
var personal = require('./mcp-session-access');
|
|
5
|
+
|
|
6
|
+
function attachMateScheduling(ctx) {
|
|
7
|
+
function owner(session) { return ctx.users.isMultiUser() ? session.ownerId : 'local-user'; }
|
|
8
|
+
function privateSession(session) {
|
|
9
|
+
if (!ctx.isMate || !session || ctx.sm.sessions.get(session.localId) !== session ||
|
|
10
|
+
session.destroying || session.mateCreationMode || session.taskStopRequested || session._mcpForeignInput ||
|
|
11
|
+
session.sessionVisibility === 'shared' || session.delegated || session._delegatedBy || session._pairDelegation) throw new Error('An active private Mate conversation is required.');
|
|
12
|
+
if (ctx.users.isMultiUser() && (session.ownerId !== ctx.projectOwnerId || !personal.canUsePersonalConnections(session, ctx.sm.sessions))) throw new Error('Only the Mate owner can schedule messages.');
|
|
13
|
+
if (!ctx.users.isMultiUser() && (session.ownerId || (session.history || []).some(function (event) { return event.type === 'user_message' && event.from && event.from !== 'default'; }))) throw new Error('Foreign input cannot authorize Mate schedules.');
|
|
14
|
+
ctx.authorizeAccess(session, owner(session));
|
|
15
|
+
}
|
|
16
|
+
function authorize(session) {
|
|
17
|
+
if (ctx.isHostAgent) throw new Error('Canonical Clay scheduling requires its separate owner-reviewed controls.');
|
|
18
|
+
privateSession(session);
|
|
19
|
+
}
|
|
20
|
+
function authorizeControl(session, control) {
|
|
21
|
+
privateSession(session);
|
|
22
|
+
return require('./canonical-clay-scheduling-authority').validate(control, ctx.slug, session);
|
|
23
|
+
}
|
|
24
|
+
function authorizeLifecycle(session) {
|
|
25
|
+
privateSession(session);
|
|
26
|
+
require('./canonical-clay-scheduling-authority').assertMate(ctx.projectOwnerId, ctx.mateId, ctx.users.isMultiUser());
|
|
27
|
+
if (ctx.isHostAgent) require('./canonical-clay-scheduling-authority').assertHost(ctx.projectOwnerId, ctx.mateId, ctx.users.isMultiUser());
|
|
28
|
+
}
|
|
29
|
+
function persistDraft(session, draft) {
|
|
30
|
+
var previous = session.mateScheduleDraft;
|
|
31
|
+
session.mateScheduleDraft = draft;
|
|
32
|
+
if (ctx.sm.saveSessionFile(session) !== true) {
|
|
33
|
+
session.mateScheduleDraft = previous;
|
|
34
|
+
throw new Error('The schedule draft could not be saved.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function result(value) { return { content: [{ type: 'text', text: JSON.stringify(value) }] }; }
|
|
38
|
+
function getToolDefs(session) {
|
|
39
|
+
try { authorize(session); } catch (error) { return []; }
|
|
40
|
+
var generation = Number(session._sdkQueryGeneration || 0);
|
|
41
|
+
var originalOwner = owner(session);
|
|
42
|
+
function current() {
|
|
43
|
+
authorize(session);
|
|
44
|
+
if (generation !== Number(session._sdkQueryGeneration || 0) || owner(session) !== originalOwner) throw new Error('The schedule query is stale.');
|
|
45
|
+
}
|
|
46
|
+
function tool(name, description, properties, required, handler) {
|
|
47
|
+
return { name: name, queryBound: true, inputSchema: { type: 'object', properties: properties, required: required, additionalProperties: false },
|
|
48
|
+
description: description, handler: async function (args) {
|
|
49
|
+
try { current(); return result(await handler(args || {})); }
|
|
50
|
+
catch (error) { return result({ status: 'rejected', reason: error.message }); }
|
|
51
|
+
} };
|
|
52
|
+
}
|
|
53
|
+
var str = { type: 'string' };
|
|
54
|
+
var fields = { message: str, date: str, time: str, scheduleRule: { type: ['object', 'null'] } };
|
|
55
|
+
async function approve(draft, question, label, other) {
|
|
56
|
+
persistDraft(session, draft);
|
|
57
|
+
var reply = await userInput.dispatchUserInput(ctx.userInput(session), { questions: [{
|
|
58
|
+
id: 'approve', header: 'Mate schedule', question: question,
|
|
59
|
+
options: [{ label: label }, { label: other || 'Cancel' }], allowOther: false,
|
|
60
|
+
}] }, { source: 'mate-schedule', signal: session.abortController && session.abortController.signal });
|
|
61
|
+
current();
|
|
62
|
+
if (!session.mateScheduleDraft || session.mateScheduleDraft.id !== draft.id || session.mateScheduleDraft.revision !== draft.revision) throw new Error('The schedule draft is stale.');
|
|
63
|
+
var accepted = reply.status === 'submitted' && reply.answers.approve && reply.answers.approve.length === 1 && reply.answers.approve[0] === label;
|
|
64
|
+
if (!accepted) persistDraft(session, null);
|
|
65
|
+
return accepted;
|
|
66
|
+
}
|
|
67
|
+
async function review(args, update) {
|
|
68
|
+
var previous = update ? ctx.lifecycle.read(session, args) : null;
|
|
69
|
+
var value = input.normalize(args, previous);
|
|
70
|
+
var draft = { id: crypto.randomUUID(), revision: crypto.randomUUID(), value: value,
|
|
71
|
+
targetId: previous && previous.id, targetRevision: previous && previous.revision };
|
|
72
|
+
if (!await approve(draft, (update ? 'Update this schedule?\n' : 'Create this schedule?\n') + input.review(value), update ? 'Update schedule' : 'Create schedule')) return { status: 'cancelled' };
|
|
73
|
+
var record = update ? ctx.lifecycle.update(session, { id: draft.targetId, revision: draft.targetRevision }, value) : ctx.lifecycle.create(session, value, draft.id);
|
|
74
|
+
persistDraft(session, null);
|
|
75
|
+
return { status: update ? 'updated' : 'created', schedule: record, timezone: input.timezone() };
|
|
76
|
+
}
|
|
77
|
+
async function cancel(args) {
|
|
78
|
+
var record = ctx.lifecycle.read(session, args);
|
|
79
|
+
if (record.state === 'cancelled') return { status: 'cancelled', schedule: record };
|
|
80
|
+
var draft = { id: crypto.randomUUID(), revision: crypto.randomUUID(), targetId: record.id, targetRevision: record.revision };
|
|
81
|
+
if (!await approve(draft, 'Cancel this schedule?\n' + input.review({ message: record.message, rule: record.rule }), 'Cancel schedule', 'Keep schedule')) return { status: 'unchanged' };
|
|
82
|
+
record = ctx.lifecycle.cancel(session, { id: draft.targetId, revision: draft.targetRevision });
|
|
83
|
+
persistDraft(session, null);
|
|
84
|
+
return { status: 'cancelled', schedule: record };
|
|
85
|
+
}
|
|
86
|
+
var begin = tool('begin_mate_schedule_interview', 'Begin scheduling for this Mate. Ask exactly one structured question at a time. Never assume repetition.', {}, [], function () {
|
|
87
|
+
return { status: 'interview_started', timezone: input.timezone(), guidance: 'Collect the message, date/time, and explicitly requested repetition one question at a time. Then propose the complete schedule for the human approval card.' };
|
|
88
|
+
});
|
|
89
|
+
begin.structuredQuestionLimit = function () { try { current(); return 1; } catch (error) { return null; } };
|
|
90
|
+
return [begin,
|
|
91
|
+
tool('propose_mate_schedule', 'Review a complete message schedule in a server-owned approval card. Only authenticated human approval persists a schedule.', fields, ['message'], function (args) { return review(args, false); }),
|
|
92
|
+
tool('list_mate_schedules', 'List logical schedules and their stable IDs, revisions and lifecycle states in this private conversation.', {}, [], function () { return { schedules: ctx.lifecycle.list(session) }; }),
|
|
93
|
+
tool('read_mate_schedule', 'Read one logical schedule using its listed ID.', { id: str }, ['id'], function (args) { return { schedule: ctx.lifecycle.read(session, args) }; }),
|
|
94
|
+
tool('update_mate_schedule', 'Propose changes to an active schedule by listed ID and revision, with a fresh human approval card.', Object.assign({ id: str, revision: str }, fields), ['id', 'revision'], function (args) { return review(args, true); }),
|
|
95
|
+
tool('cancel_mate_schedule', 'Review cancellation by listed ID and revision in an authenticated human approval card. Cancellation persists across restart.', { id: str, revision: str }, ['id', 'revision'], cancel),
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
return { authorize: authorize, authorizeControl: authorizeControl, authorizeLifecycle: authorizeLifecycle, owner: owner, getToolDefs: getToolDefs, getSystemPrompt: function (session) {
|
|
99
|
+
try { authorize(session); } catch (error) { return ''; }
|
|
100
|
+
return 'When scheduling for this Mate, call begin_mate_schedule_interview, ask exactly one structured question at a time, and clarify the message, local date/time in ' + input.timezone() + ', and explicitly requested repetition. Use scheduleRule for recurrence and count/until limits. propose_mate_schedule and update_mate_schedule present the exact authenticated human approval card. Chat text and model flags never approve schedules. Use listed logical IDs and revisions to read, update or cancel. Scheduled output uses this private Mate runtime and tools. Project Scheduled Tasks Driver/Worker tools remain unavailable.';
|
|
101
|
+
} };
|
|
102
|
+
}
|
|
103
|
+
module.exports = { attachMateScheduling: attachMateScheduling };
|
package/lib/mates.js
CHANGED
|
@@ -4,6 +4,8 @@ var path = require("path");
|
|
|
4
4
|
var crypto = require("crypto");
|
|
5
5
|
var config = require("./config");
|
|
6
6
|
var workspaceColor = require("./mate-workspace-color");
|
|
7
|
+
var avatarCatalog = require("./mate-avatar-catalog");
|
|
8
|
+
var avatarMigration = require("./mate-avatar-migration");
|
|
7
9
|
|
|
8
10
|
var crisisSafety = require("./crisis-safety");
|
|
9
11
|
var matesPrompts = require("./mates-prompts");
|
|
@@ -98,10 +100,12 @@ function loadMates(ctx) {
|
|
|
98
100
|
var raw = fs.readFileSync(matesFilePath(ctx), "utf8");
|
|
99
101
|
var data = JSON.parse(raw);
|
|
100
102
|
if (!data.mates) data.mates = [];
|
|
101
|
-
return data;
|
|
102
103
|
} catch (e) {
|
|
103
104
|
return defaultData();
|
|
104
105
|
}
|
|
106
|
+
// Migration write failures must propagate, never masquerade as an empty registry.
|
|
107
|
+
if (avatarMigration.migrate(data)) saveMates(ctx, data);
|
|
108
|
+
return data;
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
function saveMates(ctx, data) {
|
|
@@ -135,6 +139,7 @@ function createMate(ctx, seedData) {
|
|
|
135
139
|
displayName: null,
|
|
136
140
|
avatarColor: "#5857fc",
|
|
137
141
|
avatarStyle: "imprint",
|
|
142
|
+
avatarCatalogId: "B" + String(crypto.randomInt(1, 49)).padStart(2, "0"),
|
|
138
143
|
avatarSeed: crypto.randomBytes(4).toString("hex"),
|
|
139
144
|
},
|
|
140
145
|
bio: null,
|
|
@@ -142,6 +147,7 @@ function createMate(ctx, seedData) {
|
|
|
142
147
|
interviewProjectPath: null,
|
|
143
148
|
};
|
|
144
149
|
|
|
150
|
+
avatarMigration.normalizeMate(mate);
|
|
145
151
|
data.mates.push(mate);
|
|
146
152
|
saveMates(ctx, data);
|
|
147
153
|
|
|
@@ -205,6 +211,8 @@ function updateMate(ctx, id, updates) {
|
|
|
205
211
|
if (data.mates[i].primary || data.mates[i].builtinKey === "clay") delete updates.workspaceColor;
|
|
206
212
|
else if (!workspaceColor.valid(updates.workspaceColor)) return { error: "Choose a supported workspace color." };
|
|
207
213
|
}
|
|
214
|
+
var avatarError = avatarCatalog.validate(updates.profile, data.mates[i]);
|
|
215
|
+
if (avatarError) return { error: avatarError };
|
|
208
216
|
// Primary mates: protect name and core identity fields
|
|
209
217
|
if (data.mates[i].primary) {
|
|
210
218
|
delete updates.name;
|
|
@@ -219,6 +227,7 @@ function updateMate(ctx, id, updates) {
|
|
|
219
227
|
// Locked built-in artwork is server-managed. Preserve it even when a
|
|
220
228
|
// stale or crafted client submits a replacement profile object.
|
|
221
229
|
if (data.mates[i].profile && data.mates[i].profile.avatarLocked && updates.profile) {
|
|
230
|
+
updates.profile.avatarCatalogId = data.mates[i].profile.avatarCatalogId || "";
|
|
222
231
|
updates.profile.avatarStyle = data.mates[i].profile.avatarStyle;
|
|
223
232
|
updates.profile.avatarSeed = data.mates[i].profile.avatarSeed;
|
|
224
233
|
updates.profile.avatarColor = data.mates[i].profile.avatarColor;
|
|
@@ -229,6 +238,7 @@ function updateMate(ctx, id, updates) {
|
|
|
229
238
|
for (var j = 0; j < keys.length; j++) {
|
|
230
239
|
data.mates[i][keys[j]] = updates[keys[j]];
|
|
231
240
|
}
|
|
241
|
+
avatarMigration.normalizeMate(data.mates[i]);
|
|
232
242
|
saveMates(ctx, data);
|
|
233
243
|
return data.mates[i];
|
|
234
244
|
}
|
|
@@ -236,7 +246,7 @@ function updateMate(ctx, id, updates) {
|
|
|
236
246
|
return null;
|
|
237
247
|
}
|
|
238
248
|
|
|
239
|
-
function deleteMate(ctx, id) {
|
|
249
|
+
function deleteMate(ctx, id, options) {
|
|
240
250
|
var data = loadMates(ctx);
|
|
241
251
|
// Primary mates cannot be deleted (they are system infrastructure)
|
|
242
252
|
for (var di = 0; di < data.mates.length; di++) {
|
|
@@ -249,15 +259,16 @@ function deleteMate(ctx, id) {
|
|
|
249
259
|
return m.id !== id;
|
|
250
260
|
});
|
|
251
261
|
if (data.mates.length === before) return { error: "Mate not found" };
|
|
252
|
-
saveMates(ctx, data);
|
|
262
|
+
if (!options || !options.strictCleanup) saveMates(ctx, data);
|
|
253
263
|
|
|
254
264
|
// Remove mate directory
|
|
255
265
|
var mateDir = getMateDir(ctx, id);
|
|
256
266
|
try {
|
|
257
267
|
fs.rmSync(mateDir, { recursive: true, force: true });
|
|
258
268
|
} catch (e) {
|
|
259
|
-
|
|
269
|
+
if (options && options.strictCleanup) throw e;
|
|
260
270
|
}
|
|
271
|
+
if (options && options.strictCleanup) saveMates(ctx, data);
|
|
261
272
|
|
|
262
273
|
return { ok: true };
|
|
263
274
|
}
|
|
@@ -506,6 +517,7 @@ function createBuiltinMate(ctx, builtinKey) {
|
|
|
506
517
|
interviewProjectPath: null,
|
|
507
518
|
};
|
|
508
519
|
|
|
520
|
+
avatarMigration.normalizeMate(mate);
|
|
509
521
|
data.mates.push(mate);
|
|
510
522
|
saveMates(ctx, data);
|
|
511
523
|
|
|
@@ -661,8 +673,8 @@ function syncPrimaryMates(ctx) {
|
|
|
661
673
|
}
|
|
662
674
|
// Locked avatars are code-managed — refresh them from the def so a
|
|
663
675
|
// built-in's avatar can be updated by changing the source file
|
|
664
|
-
//
|
|
665
|
-
if (def.avatarLocked) {
|
|
676
|
+
// User uploads are preserved while canonical library artwork follows the definition.
|
|
677
|
+
if (def.avatarLocked && !avatarMigration.hasCustom(mate)) {
|
|
666
678
|
if (def.avatarCustom != null && mate.profile.avatarCustom !== def.avatarCustom) {
|
|
667
679
|
mate.profile.avatarCustom = def.avatarCustom; changed = true;
|
|
668
680
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Agent import consent is server-owned, owner-only, exact-query and single-use.
|
|
2
|
+
var crypto = require('crypto');
|
|
3
|
+
var userInput = require('./yoke/user-input');
|
|
4
|
+
var createProxy = require('./mcp-proxy').createProxy;
|
|
5
|
+
var SERVER = 'clay-mcp-connections';
|
|
6
|
+
function result(value, error) { return { content: [{ type: 'text', text: JSON.stringify(value) }], isError: !!error }; }
|
|
7
|
+
function attachMcpAgentTools(ctx) {
|
|
8
|
+
var viewers = new Map(), pending = new Map(), used = new WeakMap(), closed = false;
|
|
9
|
+
function ownerOf(session) { return ctx.isMultiUser() ? String(session.ownerId) : 'default'; }
|
|
10
|
+
function authorized(session, owner, generation) {
|
|
11
|
+
if (closed || !session || session.youCurator || session._mcpForeignInput || session.sessionVisibility === 'shared' || !ctx.canUseSession(session) || ownerOf(session) !== owner || Number(session._sdkQueryGeneration || 0) !== generation || session.taskStopRequested || session.abortController && session.abortController.signal.aborted) throw new Error('MCP management requires the current authorized private query.');
|
|
12
|
+
ctx.authorizeOwner(owner);
|
|
13
|
+
}
|
|
14
|
+
function canRespond(ws, session, owner) {
|
|
15
|
+
try {
|
|
16
|
+
if (!ws || ws.readyState !== 1 || ctx.ownerFor(ws) !== owner) return false;
|
|
17
|
+
return ctx.clients.has(ws) && ws._clayActiveSession === session.localId || viewers.has(ws) && viewers.get(ws).has(session);
|
|
18
|
+
} catch (error) { return false; }
|
|
19
|
+
}
|
|
20
|
+
function humans(session, owner) {
|
|
21
|
+
return Array.from(ctx.clients).concat(Array.from(viewers.keys())).some(function (ws) { return canRespond(ws, session, owner); });
|
|
22
|
+
}
|
|
23
|
+
function registerViewer(ws, session) {
|
|
24
|
+
var owner = ownerOf(session);
|
|
25
|
+
if (closed || !session || !ctx.canUseSession(session) || session._mcpForeignInput || session.sessionVisibility === 'shared') throw new Error('Consent viewer requires an authorized private session.');
|
|
26
|
+
ctx.authorizeOwner(owner);
|
|
27
|
+
if (ctx.ownerFor(ws) !== owner || ws.readyState !== 1) throw new Error('Consent viewer must be the authenticated owner.');
|
|
28
|
+
var sessions = viewers.get(ws) || new Map(), detached = false;
|
|
29
|
+
sessions.set(session, (sessions.get(session) || 0) + 1); viewers.set(ws, sessions);
|
|
30
|
+
return function () {
|
|
31
|
+
if (detached) return; detached = true;
|
|
32
|
+
var count = sessions.get(session) || 0;
|
|
33
|
+
if (count <= 1) sessions.delete(session); else sessions.set(session, count - 1);
|
|
34
|
+
if (!sessions.size && viewers.get(ws) === sessions) viewers.delete(ws);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function review(candidate, owner, config) {
|
|
38
|
+
var restrictions = config.enabledTools === null ? 'Tool allowlist: unrestricted' : 'Tool allowlist: ' + JSON.stringify(config.enabledTools || []);
|
|
39
|
+
restrictions += '\nExcluded tools: ' + JSON.stringify(config.disabledTools || []);
|
|
40
|
+
if (config.toolPatterns && config.toolPatterns.length) restrictions += '\nExcluded vendor patterns: ' + JSON.stringify(config.toolPatterns);
|
|
41
|
+
var question = 'Import "' + candidate.name + '" from ' + candidate.vendor + ' (' + candidate.scope + ') ?\nSource: ' + candidate.file + '\nTransport: ' + candidate.transport + '\nDependency: ' + candidate.dependency +
|
|
42
|
+
'\nDestination: owner ' + owner + ' — private MCP catalog shared across your Clay sessions, project sessions and Mates; other accounts cannot use it.\n' + restrictions + '\nRequired credential names: ' + (candidate.references.join(', ') || 'None') +
|
|
43
|
+
'\nThe imported copy will be DISABLED. This approval only saves that exact copy. Enable, Test and Sign in require separate actions. Original vendor files stay unchanged. Enter credentials only in MCP management, never in chat.';
|
|
44
|
+
if (question.length > 2000) throw new Error('This candidate needs a larger policy review than the consent card supports. Review it in MCP management; no import was made.');
|
|
45
|
+
return question;
|
|
46
|
+
}
|
|
47
|
+
function definitions(session) {
|
|
48
|
+
var owner = ownerOf(session), generation = Number(session._sdkQueryGeneration || 0);
|
|
49
|
+
function authorize() { authorized(session, owner, generation); }
|
|
50
|
+
async function discover(args) {
|
|
51
|
+
authorize();
|
|
52
|
+
if (Object.keys(args).some(function (key) { return key !== 'vendor' && key !== 'file'; })) throw new Error('Unsupported discovery input.');
|
|
53
|
+
if (!!args.file !== !!args.vendor) throw new Error('Configured discovery requires both vendor and absolute file path.');
|
|
54
|
+
var scan = ctx.discovery.scan(owner, session, args.file ? { vendor: args.vendor, file: args.file } : null);
|
|
55
|
+
authorize(); return result(Object.assign({ status: 'discovered', imported: false }, scan));
|
|
56
|
+
}
|
|
57
|
+
async function propose(args) {
|
|
58
|
+
authorize();
|
|
59
|
+
if (Object.keys(args).some(function (key) { return key !== 'scanId' && key !== 'candidateId'; })) throw new Error('Import approval cannot be supplied by an agent.');
|
|
60
|
+
var selected = ctx.discovery.selected(owner, session, args);
|
|
61
|
+
var key = JSON.stringify([owner, session.localId, generation, args.scanId, args.candidateId]);
|
|
62
|
+
var attempts = used.get(session);
|
|
63
|
+
if (!attempts || attempts.scanId !== args.scanId || attempts.generation !== generation) { attempts = { scanId: args.scanId, generation: generation, candidates: new Set() }; used.set(session, attempts); }
|
|
64
|
+
if (attempts.candidates.has(args.candidateId) || pending.has(key)) throw new Error('This import proposal has already been requested. Scan again for a new review.');
|
|
65
|
+
var candidate = ctx.discovery.candidate(owner, session, args);
|
|
66
|
+
if (!humans(session, owner) || session.loop && session.loop.active && session.loop.role !== 'crafting') return result({ status: 'consent_unavailable', imported: false, message: 'An authenticated owner must open this conversation to approve an import. Scheduled or unattended runs cannot approve for the user.' });
|
|
67
|
+
var question = review(candidate, owner, selected.config), requestId = 'mcp_import_' + crypto.randomUUID();
|
|
68
|
+
var controller = new AbortController(), timer, timedOut = false, sessionSignal = session.abortController && session.abortController.signal;
|
|
69
|
+
var entry = { controller: controller, requestId: requestId };
|
|
70
|
+
pending.set(key, entry); attempts.candidates.add(args.candidateId);
|
|
71
|
+
var parentAbort = function () { controller.abort(); };
|
|
72
|
+
if (sessionSignal) sessionSignal.addEventListener('abort', parentAbort, { once: true });
|
|
73
|
+
try {
|
|
74
|
+
timer = setTimeout(function () { timedOut = true; controller.abort(); }, Math.min(ctx.consentTimeout || 45000, 45000));
|
|
75
|
+
var handler = ctx.userInput && ctx.userInput(session);
|
|
76
|
+
var answer = await userInput.dispatchUserInput(function (request, respond) {
|
|
77
|
+
request.authorizeResponse = function (ws) {
|
|
78
|
+
try { authorize(); return pending.get(key) === entry && canRespond(ws, session, owner); } catch (error) { return false; }
|
|
79
|
+
};
|
|
80
|
+
if (typeof handler !== 'function') return respond.cancel('Structured owner consent is unavailable.');
|
|
81
|
+
return handler(request, respond);
|
|
82
|
+
}, { questions: [{ id: 'mcp_import', header: 'Import MCP', question: question, allowOther: false, options: [{ label: 'Keep unchanged' }, { label: 'Import disabled copy' }] }] }, { source: 'mcp-import', requestId: requestId, signal: controller.signal });
|
|
83
|
+
if (answer.status !== 'submitted' || !answer.answers.mcp_import || answer.answers.mcp_import.length !== 1 || answer.answers.mcp_import[0] !== 'Import disabled copy') return result({ status: timedOut ? 'expired' : answer.status === 'submitted' ? 'declined' : answer.status, imported: false, message: 'No connection was imported.' });
|
|
84
|
+
authorize();
|
|
85
|
+
if (pending.get(key) !== entry || !humans(session, owner)) throw new Error('Import consent is no longer available.');
|
|
86
|
+
// Consume before entering the queue. Queued mutation revalidates everything again.
|
|
87
|
+
pending.delete(key);
|
|
88
|
+
function authorizeImport() { authorize(); if (controller.signal.aborted) throw new Error('Import consent expired before the copy could be saved. Scan again.'); }
|
|
89
|
+
var id = await ctx.importConnection(owner, function () {
|
|
90
|
+
authorizeImport();
|
|
91
|
+
if (!humans(session, owner)) throw new Error('The owner disconnected before import.');
|
|
92
|
+
var live = ctx.discovery.selected(owner, session, args);
|
|
93
|
+
if (live.signature !== selected.signature || live.revision !== selected.revision) throw new Error('The reviewed candidate changed. Scan again.');
|
|
94
|
+
return live;
|
|
95
|
+
}, authorizeImport);
|
|
96
|
+
return result({ status: 'imported', imported: true, id: id, enabled: false, label: candidate.name, message: 'Saved a disabled copy in your private catalog. Configure credentials in MCP management; activation, testing and login are separate actions.' });
|
|
97
|
+
} finally {
|
|
98
|
+
clearTimeout(timer); pending.delete(key);
|
|
99
|
+
if (sessionSignal) sessionSignal.removeEventListener('abort', parentAbort);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function wrap(handler) { return function (args) { return Promise.resolve().then(function () { return handler(args || {}); }).catch(function (error) { return result({ status: 'refused', imported: false, message: error.message }, true); }); }; }
|
|
103
|
+
return [{ name: 'discover_mcp_connections', description: 'Read-only inspection of your vendor MCP sources. Returns redacted candidate IDs and provenance. Never executes commands, imports, activates, logs in or reads vendor OAuth stores. Use credential entry only in MCP management.', inputSchema: { type: 'object', properties: { vendor: { type: 'string' }, file: { type: 'string' } }, additionalProperties: false }, handler: wrap(discover) },
|
|
104
|
+
{ name: 'propose_mcp_import', description: 'Present one exact discovered candidate to the authenticated owner for structured approval, then save a disabled copy only after explicit approval. Denial, cancellation, timeout or absent owner causes no import. Pass only scanId and candidateId; agent-provided approval is never accepted.', inputSchema: { type: 'object', properties: { scanId: { type: 'string' }, candidateId: { type: 'string' } }, required: ['scanId', 'candidateId'], additionalProperties: false }, handler: wrap(propose) }];
|
|
105
|
+
}
|
|
106
|
+
function getServer(session) {
|
|
107
|
+
try {
|
|
108
|
+
authorized(session, ownerOf(session), Number(session._sdkQueryGeneration || 0));
|
|
109
|
+
if (ctx.available && !ctx.available(session)) return null;
|
|
110
|
+
var tools = definitions(session);
|
|
111
|
+
return createProxy(SERVER, tools, function (name, args) { var tool = tools.find(function (item) { return item.name === name; }); return tool.handler(args); });
|
|
112
|
+
} catch (error) { return null; }
|
|
113
|
+
}
|
|
114
|
+
function destroy() { closed = true; pending.forEach(function (entry) { entry.controller.abort(); }); pending.clear(); used = new WeakMap(); viewers.clear(); }
|
|
115
|
+
return { getServer: getServer, registerViewer: registerViewer, destroy: destroy };
|
|
116
|
+
}
|
|
117
|
+
module.exports = { attachMcpAgentTools: attachMcpAgentTools, SERVER: SERVER };
|
|
@@ -1,39 +1,143 @@
|
|
|
1
|
-
// Owner
|
|
1
|
+
// Owner-scoped remote records and project-scoped computer preferences with encrypted credentials; never sent to the UI.
|
|
2
2
|
var fs = require('fs');
|
|
3
3
|
var path = require('path');
|
|
4
4
|
var crypto = require('crypto');
|
|
5
|
-
|
|
5
|
+
var equal = require('util').isDeepStrictEqual;
|
|
6
|
+
|
|
7
|
+
function createConnectionStore(root, project, options) {
|
|
6
8
|
var directory = path.join(root, 'mcp-connections');
|
|
9
|
+
var shared = !!(options && options.shared);
|
|
7
10
|
function scope(owner) {
|
|
8
11
|
if (typeof owner !== 'string' || !owner || owner.length > 160) throw new Error('MCP account owner is required.');
|
|
9
|
-
return JSON.stringify([project, owner]);
|
|
12
|
+
return shared ? JSON.stringify([owner]) : JSON.stringify([project, owner]);
|
|
10
13
|
}
|
|
11
14
|
function filename(owner) { return path.join(directory, crypto.createHash('sha256').update(scope(owner)).digest('hex') + '.json'); }
|
|
15
|
+
function legacyFilename(owner) { return path.join(directory, crypto.createHash('sha256').update(JSON.stringify([project, owner])).digest('hex') + '.json'); }
|
|
12
16
|
function key() {
|
|
13
17
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
14
|
-
var file = path.join(directory, '.key');
|
|
15
|
-
try { fs.
|
|
16
|
-
catch (error) {
|
|
17
|
-
|
|
18
|
+
var file = path.join(directory, '.key'), bytes;
|
|
19
|
+
try { bytes = fs.readFileSync(file); }
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code !== 'ENOENT') throw error;
|
|
22
|
+
var temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
|
|
23
|
+
try {
|
|
24
|
+
fs.writeFileSync(temp, crypto.randomBytes(32), { flag: 'wx', mode: 0o600 });
|
|
25
|
+
try { fs.linkSync(temp, file); } catch (publishError) { if (publishError.code !== 'EEXIST') throw publishError; }
|
|
26
|
+
} finally { fs.unlinkSync(temp); }
|
|
27
|
+
bytes = fs.readFileSync(file);
|
|
28
|
+
}
|
|
29
|
+
if (bytes.length !== 32) throw new Error('MCP encryption key is invalid.');
|
|
30
|
+
return bytes;
|
|
18
31
|
}
|
|
19
|
-
function
|
|
32
|
+
function decode(file, aad) {
|
|
20
33
|
var data;
|
|
21
|
-
try { data = JSON.parse(fs.readFileSync(
|
|
22
|
-
catch (error) { if (error.code === 'ENOENT') return []; throw new Error('Could not read MCP connections.'); }
|
|
34
|
+
try { data = JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
35
|
+
catch (error) { if (error.code === 'ENOENT') return { records: [], revision: null, migratedProjects: [], retired: [] }; throw new Error('Could not read MCP connections.'); }
|
|
23
36
|
var parts = data.encrypted.split('.').map(function (part) { return Buffer.from(part, 'base64'); });
|
|
24
37
|
var cipher = crypto.createDecipheriv('aes-256-gcm', key(), parts[0]);
|
|
25
|
-
cipher.setAAD(Buffer.from(
|
|
26
|
-
|
|
38
|
+
cipher.setAAD(Buffer.from(aad)); cipher.setAuthTag(parts[1]);
|
|
39
|
+
var value = JSON.parse(Buffer.concat([cipher.update(parts[2]), cipher.final()]).toString('utf8'));
|
|
40
|
+
if (Array.isArray(value)) return { records: value, revision: null, migratedProjects: [], retired: [] };
|
|
41
|
+
return { records: Array.isArray(value.records) ? value.records : [], revision: value.revision || null, migratedProjects: Array.isArray(value.migratedProjects) ? value.migratedProjects : [], retired: value.retired || [] };
|
|
27
42
|
}
|
|
28
|
-
function
|
|
43
|
+
function encode(owner, state, expectedRevision) {
|
|
44
|
+
var current = decode(filename(owner), scope(owner));
|
|
45
|
+
if (expectedRevision !== undefined && current.revision !== expectedRevision) throw new Error('MCP connections changed. Please retry.');
|
|
46
|
+
var next = { records: state.records, revision: crypto.randomBytes(16).toString('hex'), migratedProjects: state.migratedProjects || [], retired: state.retired || [] };
|
|
29
47
|
var iv = crypto.randomBytes(12), cipher = crypto.createCipheriv('aes-256-gcm', key(), iv);
|
|
30
48
|
cipher.setAAD(Buffer.from(scope(owner)));
|
|
31
|
-
var bytes = Buffer.concat([cipher.update(JSON.stringify(
|
|
49
|
+
var bytes = Buffer.concat([cipher.update(JSON.stringify(next), 'utf8'), cipher.final()]);
|
|
32
50
|
var value = { encrypted: [iv, cipher.getAuthTag(), bytes].map(function (part) { return part.toString('base64'); }).join('.') };
|
|
33
51
|
var file = filename(owner), temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
|
|
34
52
|
try { fs.writeFileSync(temp, JSON.stringify(value), { mode: 0o600 }); fs.renameSync(temp, file); }
|
|
35
53
|
finally { try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
|
|
54
|
+
return next.revision;
|
|
36
55
|
}
|
|
37
|
-
return {
|
|
56
|
+
function visible(records) { if (!shared) return records; return records.filter(function (record) { return record.source !== 'computer' || record.project === project; }); }
|
|
57
|
+
function connectionConflict(left, right) { return left.id && left.id === right.id || (left.url && right.url && left.url === right.url); }
|
|
58
|
+
function migrate(owner, state) {
|
|
59
|
+
if (!shared || state.migratedProjects.indexOf(project) >= 0) return state;
|
|
60
|
+
var legacy = decode(legacyFilename(owner), JSON.stringify([project, owner])).records;
|
|
61
|
+
var merged = state.records.slice();
|
|
62
|
+
legacy.forEach(function (record) {
|
|
63
|
+
var copy = Object.assign({}, record);
|
|
64
|
+
if (copy.source === 'computer') copy.project = project;
|
|
65
|
+
else if ((state.retired || []).some(function (removed) { return connectionConflict(removed, copy); })) return;
|
|
66
|
+
var existing = merged.filter(function (current) { return current.source === copy.source && (copy.source === 'computer' ? current.project === project && current.name === copy.name : connectionConflict(current, copy)); });
|
|
67
|
+
if (existing.length && existing.some(function (current) { return !equal(current, copy); })) throw new Error('MCP connection migration conflict. Resolve the legacy project connection before retrying.');
|
|
68
|
+
if (!existing.length) merged.push(copy);
|
|
69
|
+
});
|
|
70
|
+
state.records = merged;
|
|
71
|
+
state.migratedProjects = state.migratedProjects.concat([project]);
|
|
72
|
+
return state;
|
|
73
|
+
}
|
|
74
|
+
function readUnlocked(owner) {
|
|
75
|
+
var state = decode(filename(owner), scope(owner));
|
|
76
|
+
if (shared) {
|
|
77
|
+
var unique = [];
|
|
78
|
+
state.records.forEach(function (record) {
|
|
79
|
+
var existing = unique.find(function (current) {
|
|
80
|
+
return record.source === 'computer' ? current.source === 'computer' && record.project === current.project && record.name === current.name : current.source !== 'computer' && connectionConflict(record, current);
|
|
81
|
+
});
|
|
82
|
+
if (existing && !equal(existing, record)) throw new Error('Conflicting duplicate MCP records require resolution.');
|
|
83
|
+
if (!existing) unique.push(record);
|
|
84
|
+
});
|
|
85
|
+
if (unique.length !== state.records.length) { state.records = unique; state.revision = encode(owner, state, state.revision); }
|
|
86
|
+
}
|
|
87
|
+
if (shared && state.migratedProjects.indexOf(project) < 0) { state = migrate(owner, state); state.revision = encode(owner, state, state.revision); }
|
|
88
|
+
return state;
|
|
89
|
+
}
|
|
90
|
+
// Fail closed on contention. The lock covers migration and the full CAS read/write,
|
|
91
|
+
// including callers in a second process using the same storage root.
|
|
92
|
+
function locked(owner, operation) {
|
|
93
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
94
|
+
var file = filename(owner) + '.lock', fd;
|
|
95
|
+
try { fd = fs.openSync(file, 'wx', 0o600); }
|
|
96
|
+
catch (error) { if (error.code === 'EEXIST') throw new Error('MCP connections are busy. Please retry.'); throw error; }
|
|
97
|
+
try { fs.writeFileSync(fd, String(process.pid)); return operation(); }
|
|
98
|
+
finally { fs.closeSync(fd); fs.unlinkSync(file); }
|
|
99
|
+
}
|
|
100
|
+
function snapshot(owner) {
|
|
101
|
+
return locked(owner, function () {
|
|
102
|
+
var state = readUnlocked(owner);
|
|
103
|
+
return { records: visible(state.records), revision: state.revision };
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function load(owner) { return snapshot(owner).records; }
|
|
107
|
+
function revision(owner) { return snapshot(owner).revision; }
|
|
108
|
+
function validate(records) {
|
|
109
|
+
if (!Array.isArray(records)) throw new Error('MCP connection snapshot must be an array.');
|
|
110
|
+
records.forEach(function (record, index) {
|
|
111
|
+
if (!record || typeof record !== 'object') throw new Error('Invalid MCP connection record.');
|
|
112
|
+
if (records.slice(0, index).some(function (previous) {
|
|
113
|
+
return record.id && previous.id === record.id ||
|
|
114
|
+
record.source === 'computer' && previous.source === 'computer' && record.name === previous.name ||
|
|
115
|
+
record.source !== 'computer' && previous.source !== 'computer' && record.url && record.url === previous.url;
|
|
116
|
+
})) throw new Error('Duplicate MCP connection in snapshot.');
|
|
117
|
+
if (shared && record.source === 'computer' && record.project && record.project !== project) throw new Error('Foreign project computer preference.');
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Remote rows are the complete owner snapshot. Computer rows replace only this
|
|
121
|
+
// project's preferences; omitted remote rows are deleted, never merged back.
|
|
122
|
+
function save(owner, records, expectedRevision) {
|
|
123
|
+
validate(records);
|
|
124
|
+
return locked(owner, function () {
|
|
125
|
+
var state = readUnlocked(owner);
|
|
126
|
+
if (expectedRevision !== undefined && state.revision !== expectedRevision) throw new Error('MCP connections changed. Please retry.');
|
|
127
|
+
if (!shared) return encode(owner, { records: records, migratedProjects: [] }, state.revision);
|
|
128
|
+
var incomingRemote = records.filter(function (record) { return record.source !== 'computer'; });
|
|
129
|
+
var currentComputer = records.filter(function (record) { return record.source === 'computer'; }).map(function (record) { return Object.assign({}, record, { project: project }); });
|
|
130
|
+
var otherComputer = state.records.filter(function (record) { return record.source === 'computer' && record.project !== project; });
|
|
131
|
+
var removed = state.records.filter(function (record) {
|
|
132
|
+
return record.source !== 'computer' && !incomingRemote.some(function (incoming) { return connectionConflict(record, incoming); });
|
|
133
|
+
}).map(function (record) { return { id: record.id, url: record.url }; });
|
|
134
|
+
state.retired = (state.retired || []).concat(removed).filter(function (record, index, all) {
|
|
135
|
+
return all.findIndex(function (other) { return other.id === record.id && other.url === record.url; }) === index;
|
|
136
|
+
});
|
|
137
|
+
state.records = incomingRemote.concat(otherComputer, currentComputer);
|
|
138
|
+
return encode(owner, state, state.revision);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return { load: load, save: save, revision: revision, snapshot: snapshot, namespace: fs.existsSync(root) ? fs.realpathSync(root) : path.resolve(root) };
|
|
38
142
|
}
|
|
39
143
|
module.exports = { createConnectionStore: createConnectionStore };
|