@gajae-code/coding-agent 0.13.2 → 0.14.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/CHANGELOG.md +215 -8
- package/README.md +2 -0
- package/bin/gajaessi.js +4 -0
- package/dist/types/async/job-manager.d.ts +55 -2
- package/dist/types/async/support.d.ts +14 -0
- package/dist/types/capability/mcp.d.ts +6 -0
- package/dist/types/capability/types.d.ts +6 -0
- package/dist/types/cli/accounts-cli.d.ts +19 -0
- package/dist/types/cli/args.d.ts +3 -0
- package/dist/types/cli/config-cli.d.ts +6 -0
- package/dist/types/cli/crash-cli.d.ts +10 -0
- package/dist/types/cli/customize-doctor.d.ts +145 -0
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/mcp-cli.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/root-flags.d.ts +12 -0
- package/dist/types/cli/setup-cli.d.ts +3 -1
- package/dist/types/cli/skills-cli.d.ts +2 -1
- package/dist/types/cli/update-cli.d.ts +19 -1
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/accounts.d.ts +40 -0
- package/dist/types/commands/crash.d.ts +24 -0
- package/dist/types/commands/customize.d.ts +22 -0
- package/dist/types/commands/daemon.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/commands/skills.d.ts +6 -1
- package/dist/types/commands/update.d.ts +7 -0
- package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
- package/dist/types/config/config-schema-version.d.ts +10 -0
- package/dist/types/config/file-lock.d.ts +18 -0
- package/dist/types/config/model-profile-activation.d.ts +3 -1
- package/dist/types/config/model-registry.d.ts +10 -1
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +73 -9
- package/dist/types/config/settings.d.ts +63 -0
- package/dist/types/config/skill-settings-defaults.d.ts +26 -0
- package/dist/types/config/task-delegation.d.ts +39 -0
- package/dist/types/config/update-channel.d.ts +10 -0
- package/dist/types/coordinator-mcp/server.d.ts +5 -1
- package/dist/types/crash/index-store.d.ts +87 -0
- package/dist/types/crash/nudge.d.ts +50 -0
- package/dist/types/crash/record-loader.d.ts +17 -0
- package/dist/types/crash/report.d.ts +134 -0
- package/dist/types/crash/sanitize.d.ts +25 -0
- package/dist/types/cursor.d.ts +13 -2
- package/dist/types/customization/import.d.ts +37 -0
- package/dist/types/customization/index.d.ts +4 -0
- package/dist/types/customization/inventory.d.ts +25 -0
- package/dist/types/customization/mutations.d.ts +32 -0
- package/dist/types/customization/types.d.ts +115 -0
- package/dist/types/daemon/control-types.d.ts +8 -1
- package/dist/types/daemon/operator-contract.d.ts +4 -4
- package/dist/types/discovery/claude.d.ts +39 -1
- package/dist/types/discovery/codex.d.ts +39 -1
- package/dist/types/discovery/index.d.ts +2 -0
- package/dist/types/discovery/mcp-compat.d.ts +30 -0
- package/dist/types/edit/renderer.d.ts +23 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -4
- package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
- package/dist/types/extensibility/hooks/loader.d.ts +19 -5
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
- package/dist/types/extensibility/skill-management.d.ts +107 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
- package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
- package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
- package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
- package/dist/types/hooks/events.d.ts +60 -0
- package/dist/types/hooks/normalize.d.ts +83 -0
- package/dist/types/hooks/skill-state.d.ts +9 -2
- package/dist/types/main.d.ts +2 -1
- package/dist/types/modes/acp/abort-scope.d.ts +13 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -4
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
- package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
- package/dist/types/modes/components/customization/index.d.ts +1 -0
- package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
- package/dist/types/modes/components/gajae-pet-widget.d.ts +6 -28
- package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
- package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
- package/dist/types/modes/components/oauth-selector.d.ts +10 -4
- package/dist/types/modes/components/pet-capability.d.ts +7 -21
- package/dist/types/modes/components/settings-selector.d.ts +5 -0
- package/dist/types/modes/components/status-line/gh.d.ts +6 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
- package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/defaults/index.d.ts +118 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
- package/dist/types/runtime-mcp/config.d.ts +9 -0
- package/dist/types/runtime-mcp/manager.d.ts +26 -1
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
- package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
- package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
- package/dist/types/runtime-mcp/protocol.d.ts +278 -0
- package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
- package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
- package/dist/types/runtime-mcp/types.d.ts +102 -10
- package/dist/types/sdk/acp/adapter.d.ts +26 -5
- package/dist/types/sdk/broker/broker.d.ts +3 -55
- package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
- package/dist/types/sdk/broker/session-index.d.ts +14 -5
- package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
- package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
- package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
- package/dist/types/sdk/bus/index.d.ts +85 -99
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
- package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
- package/dist/types/sdk/bus/notification-service.d.ts +52 -0
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
- package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
- package/dist/types/sdk/bus/session-control.d.ts +25 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
- package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
- package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
- package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
- package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
- package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
- package/dist/types/sdk/cli/rows.d.ts +2 -11
- package/dist/types/sdk/cli/session-cli.d.ts +15 -25
- package/dist/types/sdk/client/client.d.ts +97 -1
- package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +18 -5
- package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
- package/dist/types/sdk/host/host.d.ts +15 -1
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/host/session-runtime.d.ts +127 -7
- package/dist/types/sdk/host/types.d.ts +1 -1
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +2 -1
- package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
- package/dist/types/sdk/lifecycle/client.d.ts +59 -0
- package/dist/types/sdk/lifecycle/index.d.ts +3 -0
- package/dist/types/sdk/lifecycle/service.d.ts +232 -0
- package/dist/types/sdk/mcp/server.d.ts +9 -6
- package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
- package/dist/types/sdk/prompt-status.d.ts +10 -3
- package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
- package/dist/types/sdk/receipt-state.d.ts +17 -0
- package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
- package/dist/types/sdk/router/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +152 -0
- package/dist/types/sdk/session-list.d.ts +22 -0
- package/dist/types/sdk/session-reconnect.d.ts +43 -1
- package/dist/types/sdk/session.d.ts +34 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
- package/dist/types/session/account-inventory.d.ts +86 -0
- package/dist/types/session/agent-session.d.ts +145 -6
- package/dist/types/session/agent-storage.d.ts +10 -0
- package/dist/types/session/coordinator-tool-label.d.ts +28 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
- package/dist/types/session/messages.d.ts +1 -1
- package/dist/types/session/secure-token-file.d.ts +3 -0
- package/dist/types/session/session-manager.d.ts +47 -1
- package/dist/types/session/startup-auth-config.d.ts +44 -0
- package/dist/types/session/terminal-abort.d.ts +378 -0
- package/dist/types/session/yield-queue.d.ts +7 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +88 -0
- package/dist/types/session-import/command.d.ts +35 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/provider-codex.d.ts +22 -0
- package/dist/types/session-import/provider-service.d.ts +38 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +47 -0
- package/dist/types/session-import/types.d.ts +120 -0
- package/dist/types/setup/paseo/check.d.ts +16 -0
- package/dist/types/setup/paseo/install-saga.d.ts +115 -0
- package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
- package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
- package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
- package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
- package/dist/types/setup/paseo/provider-config.d.ts +35 -0
- package/dist/types/setup/paseo/remove.d.ts +14 -0
- package/dist/types/setup/paseo/result-types.d.ts +59 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
- package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
- package/dist/types/skill-state/active-state.d.ts +5 -1
- package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
- package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
- package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
- package/dist/types/slash-commands/types.d.ts +7 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/tools/ask-answer-registry.d.ts +6 -3
- package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
- package/dist/types/tools/bash.d.ts +2 -6
- package/dist/types/tools/browser/attach.d.ts +11 -0
- package/dist/types/tools/browser/launch.d.ts +20 -0
- package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
- package/dist/types/tools/browser/render.d.ts +1 -0
- package/dist/types/tools/browser.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +10 -2
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/monitor.d.ts +1 -1
- package/dist/types/tools/skill-discovery.d.ts +8 -1
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
- package/dist/types/utils/herdr-pane.d.ts +76 -0
- package/package.json +19 -9
- package/scripts/build-binary.ts +2 -1
- package/scripts/build-sdk-package-smoke.ts +2 -13
- package/scripts/capture-sticky-viewport-showcase.ts +0 -1
- package/scripts/compile-args.ts +2 -0
- package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
- package/scripts/generate-sdk-operation-inventory.ts +28 -0
- package/scripts/generate-telegram-baseline-manifest.ts +1 -0
- package/scripts/generate-tool-catalog.ts +6 -4
- package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
- package/src/async/job-manager.ts +352 -17
- package/src/async/support.ts +25 -0
- package/src/autoresearch/dashboard.ts +28 -13
- package/src/capability/index.ts +3 -1
- package/src/capability/mcp.ts +6 -0
- package/src/capability/types.ts +6 -0
- package/src/cli/accounts-cli.ts +395 -0
- package/src/cli/args.ts +23 -0
- package/src/cli/auth-broker-cli.ts +136 -66
- package/src/cli/auth-gateway-cli.ts +94 -70
- package/src/cli/config-cli.ts +27 -2
- package/src/cli/crash-cli.ts +78 -0
- package/src/cli/customize-doctor.ts +1605 -0
- package/src/cli/daemon-cli.ts +12 -3
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/mcp-cli.ts +128 -60
- package/src/cli/notify-cli.ts +8 -14
- package/src/cli/root-flags.ts +8 -0
- package/src/cli/setup-cli.ts +145 -0
- package/src/cli/skills-cli.ts +38 -2
- package/src/cli/update-cli.ts +167 -12
- package/src/cli.ts +4 -0
- package/src/commands/accounts.ts +71 -0
- package/src/commands/crash.ts +35 -0
- package/src/commands/customize.ts +84 -0
- package/src/commands/daemon.ts +2 -2
- package/src/commands/deep-interview.ts +2 -0
- package/src/commands/launch.ts +1 -0
- package/src/commands/mcp.ts +12 -7
- package/src/commands/ralplan.ts +2 -0
- package/src/commands/sdk.ts +65 -32
- package/src/commands/setup.ts +5 -0
- package/src/commands/skills.ts +14 -4
- package/src/commands/ultragoal.ts +10 -1
- package/src/commands/update.ts +26 -15
- package/src/config/atomic-yaml-patch-worker.ts +57 -0
- package/src/config/atomic-yaml-patch.ts +769 -29
- package/src/config/config-schema-version.ts +10 -0
- package/src/config/file-lock.ts +70 -1
- package/src/config/mcp-schema.json +52 -14
- package/src/config/model-profile-activation.ts +23 -2
- package/src/config/model-profiles.ts +88 -21
- package/src/config/model-registry.ts +147 -21
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +77 -7
- package/src/config/settings.ts +4038 -199
- package/src/config/skill-settings-defaults.ts +41 -3
- package/src/config/task-delegation.ts +87 -0
- package/src/config/update-channel.ts +23 -0
- package/src/coordinator-mcp/server.ts +377 -308
- package/src/crash/index-store.ts +723 -0
- package/src/crash/nudge.ts +89 -0
- package/src/crash/record-loader.ts +149 -0
- package/src/crash/report.ts +551 -0
- package/src/crash/sanitize.ts +129 -0
- package/src/cursor.ts +36 -10
- package/src/customization/import.ts +946 -0
- package/src/customization/index.ts +4 -0
- package/src/customization/inventory.ts +335 -0
- package/src/customization/mutations.ts +141 -0
- package/src/customization/types.ts +223 -0
- package/src/daemon/control-types.ts +8 -1
- package/src/daemon/operator-contract.ts +6 -6
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
- package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
- package/src/discovery/claude-hooks.ts +38 -0
- package/src/discovery/claude.ts +142 -209
- package/src/discovery/codex-hooks.ts +38 -0
- package/src/discovery/codex.ts +132 -198
- package/src/discovery/helpers.ts +8 -1
- package/src/discovery/index.ts +2 -0
- package/src/discovery/mcp-compat.ts +234 -0
- package/src/discovery/mcp-json.ts +6 -0
- package/src/edit/renderer.ts +40 -0
- package/src/edit/streaming.ts +4 -2
- package/src/exec/bash-executor.ts +24 -20
- package/src/export/html/index.ts +51 -2
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +17 -4
- package/src/extensibility/extensions/types.ts +31 -6
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
- package/src/extensibility/gjc-plugins/installer.ts +9 -5
- package/src/extensibility/gjc-plugins/observability.ts +8 -2
- package/src/extensibility/gjc-plugins/registry.ts +13 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
- package/src/extensibility/gjc-plugins/schema.ts +222 -39
- package/src/extensibility/hooks/loader.ts +154 -10
- package/src/extensibility/runtime-skill-discovery.ts +257 -60
- package/src/extensibility/skill-management.ts +368 -0
- package/src/extensibility/skills.ts +52 -16
- package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
- package/src/gjc-runtime/launch-worktree.ts +56 -2
- package/src/gjc-runtime/linux-proc.ts +7 -1
- package/src/gjc-runtime/ralplan-runtime.ts +100 -164
- package/src/gjc-runtime/session-state-lock.ts +992 -0
- package/src/gjc-runtime/session-state-sidecar.ts +644 -142
- package/src/gjc-runtime/state-writer.ts +39 -14
- package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
- package/src/gjc-runtime/ultragoal-guard.ts +11 -4
- package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
- package/src/gjc-runtime/workflow-settings.ts +596 -0
- package/src/goals/tools/goal-tool.ts +6 -1
- package/src/hooks/events.ts +291 -0
- package/src/hooks/native-skill-hook.ts +4 -0
- package/src/hooks/normalize.ts +369 -0
- package/src/hooks/skill-state.ts +165 -46
- package/src/internal-urls/docs-index.generated.ts +41 -32
- package/src/main.ts +36 -5
- package/src/migrate/mcp-mapper.ts +25 -2
- package/src/modes/acp/abort-scope.ts +29 -0
- package/src/modes/acp/acp-agent.ts +423 -98
- package/src/modes/components/assistant-message.ts +6 -1
- package/src/modes/components/chrome.ts +4 -3
- package/src/modes/components/customization/customization-dashboard.ts +443 -0
- package/src/modes/components/customization/import-wizard.ts +372 -0
- package/src/modes/components/customization/index.ts +1 -0
- package/src/modes/components/dynamic-theme-text.ts +22 -0
- package/src/modes/components/gajae-pet-widget.ts +607 -193
- package/src/modes/components/irc-sidebar.ts +11 -0
- package/src/modes/components/iterm-pet-transport.ts +585 -0
- package/src/modes/components/model-selector.ts +1 -6
- package/src/modes/components/oauth-selector.ts +277 -86
- package/src/modes/components/pet-capability.ts +35 -31
- package/src/modes/components/settings-selector.ts +29 -5
- package/src/modes/components/status-line/gh.ts +45 -1
- package/src/modes/components/status-line/segments.ts +19 -4
- package/src/modes/components/tool-status-header.ts +13 -3
- package/src/modes/controllers/command-controller.ts +20 -8
- package/src/modes/controllers/extension-ui-controller.ts +56 -257
- package/src/modes/controllers/input-controller.ts +10 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
- package/src/modes/controllers/selector-controller.ts +238 -77
- package/src/modes/controllers/stt-controller.ts +3 -3
- package/src/modes/interactive-mode.ts +88 -16
- package/src/modes/print-mode.ts +35 -6
- package/src/modes/runtime-init.ts +21 -68
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/theme/defaults/ouroboros.json +118 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/injected-user-submission.ts +25 -0
- package/src/modes/utils/terminal-bell.ts +15 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/critic.md +1 -1
- package/src/prompts/system/rlm-research.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -2
- package/src/prompts/tools/bash.md +7 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +6 -6
- package/src/prompts/tools/read.md +0 -2
- package/src/prompts/tools/skill-discovery.md +4 -2
- package/src/runtime-mcp/client.ts +427 -23
- package/src/runtime-mcp/config.ts +37 -5
- package/src/runtime-mcp/manager-pool.test.ts +56 -0
- package/src/runtime-mcp/manager.ts +52 -2
- package/src/runtime-mcp/oauth-discovery.ts +13 -0
- package/src/runtime-mcp/oauth-flow.ts +46 -1
- package/src/runtime-mcp/pool-key.ts +5 -1
- package/src/runtime-mcp/pool.test.ts +9 -0
- package/src/runtime-mcp/protocol.test.ts +353 -0
- package/src/runtime-mcp/protocol.ts +624 -0
- package/src/runtime-mcp/scope-config.ts +78 -0
- package/src/runtime-mcp/tool-bridge.ts +44 -10
- package/src/runtime-mcp/transports/http.ts +84 -13
- package/src/runtime-mcp/types.ts +123 -12
- package/src/sdk/acp/adapter.ts +328 -97
- package/src/sdk/broker/broker.ts +160 -741
- package/src/sdk/broker/ensure.ts +9 -1
- package/src/sdk/broker/lifecycle.ts +510 -116
- package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
- package/src/sdk/broker/process-incarnation.ts +19 -0
- package/src/sdk/broker/session-index.ts +516 -117
- package/src/sdk/broker/startup-failure.ts +28 -5
- package/src/sdk/broker/transport.ts +1 -13
- package/src/sdk/bus/chat-daemon-cli.ts +4 -2
- package/src/sdk/bus/chat-daemon-control.ts +67 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
- package/src/sdk/bus/config.ts +13 -0
- package/src/sdk/bus/conversation-store.ts +59 -171
- package/src/sdk/bus/daemon-paths.ts +20 -0
- package/src/sdk/bus/discord-conversation.ts +2 -0
- package/src/sdk/bus/discord-daemon.ts +524 -126
- package/src/sdk/bus/discord-live-provider.ts +206 -79
- package/src/sdk/bus/existing-thread-readiness.ts +4 -97
- package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
- package/src/sdk/bus/index.ts +2480 -456
- package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
- package/src/sdk/bus/lifecycle-commands.ts +2 -2
- package/src/sdk/bus/managed-daemon.ts +2 -145
- package/src/sdk/bus/notification-service.ts +196 -0
- package/src/sdk/bus/operator-runtime.ts +1 -1
- package/src/sdk/bus/prompt-reconciliation.ts +24 -15
- package/src/sdk/bus/reconciliation-store.ts +553 -24
- package/src/sdk/bus/session-control.ts +49 -3
- package/src/sdk/bus/slack-conversation.ts +4 -0
- package/src/sdk/bus/slack-daemon.ts +733 -134
- package/src/sdk/bus/slack-live-provider.ts +65 -15
- package/src/sdk/bus/slack-provider.ts +4 -0
- package/src/sdk/bus/slack-thread-binding.ts +9 -104
- package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
- package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
- package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
- package/src/sdk/bus/telegram-daemon-control.ts +3 -13
- package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
- package/src/sdk/bus/telegram-daemon.ts +2552 -2280
- package/src/sdk/bus/telegram-reference.ts +1 -226
- package/src/sdk/bus/telegram-setup.ts +2 -2
- package/src/sdk/bus/threaded-render.ts +1 -0
- package/src/sdk/bus/topic-registry.ts +279 -146
- package/src/sdk/cli/rows.ts +2 -35
- package/src/sdk/cli/session-cli.ts +776 -700
- package/src/sdk/client/client.ts +892 -1
- package/src/sdk/host/control/dispatch.ts +93 -58
- package/src/sdk/host/control/index.ts +1 -1
- package/src/sdk/host/control/operations.ts +18 -10
- package/src/sdk/host/control/runtime-gate.ts +40 -0
- package/src/sdk/host/host.ts +63 -12
- package/src/sdk/host/query/handlers.ts +66 -7
- package/src/sdk/host/reverse-leases.ts +2 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +2787 -34
- package/src/sdk/host/session-runtime.ts +1489 -101
- package/src/sdk/host/surface-policy.ts +12 -8
- package/src/sdk/host/types.ts +1 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
- package/src/sdk/host/websocket-transport.ts +85 -50
- package/src/sdk/index.ts +2 -1
- package/src/sdk/lifecycle/broker-client.ts +53 -0
- package/src/sdk/lifecycle/client.ts +234 -0
- package/src/sdk/lifecycle/index.ts +3 -0
- package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
- package/src/sdk/lifecycle/service.ts +720 -0
- package/src/sdk/mcp/server.ts +226 -188
- package/src/sdk/prompt-deadline-lease.ts +55 -0
- package/src/sdk/prompt-deadline-manager.ts +148 -0
- package/src/sdk/prompt-status.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +272 -129
- package/src/sdk/protocol/operation-registry.ts +71 -8
- package/src/sdk/receipt-state.ts +43 -0
- package/src/sdk/reconciliation-extensions.ts +14 -0
- package/src/sdk/router/index.ts +1 -0
- package/src/sdk/router/session-router.ts +1658 -0
- package/src/sdk/session-list.ts +93 -0
- package/src/sdk/session-reconnect.ts +45 -1
- package/src/sdk/session.ts +716 -120
- package/src/sdk/transport/serve-cli.ts +18 -34
- package/src/session/account-inventory.ts +603 -0
- package/src/session/agent-session.ts +2873 -559
- package/src/session/agent-storage.ts +35 -0
- package/src/session/blob-store.ts +32 -1
- package/src/session/coordinator-tool-label.ts +40 -0
- package/src/session/internal/managed-session-scope.ts +4 -0
- package/src/session/internal/managed-session-storage.ts +199 -31
- package/src/session/messages.ts +1 -1
- package/src/session/secure-token-file.ts +80 -0
- package/src/session/session-manager.ts +757 -57
- package/src/session/session-storage.ts +25 -16
- package/src/session/startup-auth-config.ts +297 -0
- package/src/session/terminal-abort.ts +1253 -0
- package/src/session/yield-queue.ts +57 -15
- package/src/session-import/claude.ts +334 -0
- package/src/session-import/codex.ts +1033 -0
- package/src/session-import/command.ts +77 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +7 -0
- package/src/session-import/provider-codex.ts +327 -0
- package/src/session-import/provider-service.ts +457 -0
- package/src/session-import/redact.ts +128 -0
- package/src/session-import/service.ts +1264 -0
- package/src/session-import/types.ts +148 -0
- package/src/setup/paseo/check.ts +213 -0
- package/src/setup/paseo/install-saga.ts +293 -0
- package/src/setup/paseo/json-publisher.ts +263 -0
- package/src/setup/paseo/orchestration-preferences.ts +86 -0
- package/src/setup/paseo/paseo-ownership.ts +224 -0
- package/src/setup/paseo/paseo-setup.ts +253 -0
- package/src/setup/paseo/provider-config.ts +139 -0
- package/src/setup/paseo/remove.ts +183 -0
- package/src/setup/paseo/result-types.ts +79 -0
- package/src/setup/paseo/setup-deps.ts +175 -0
- package/src/setup/paseo/skills-bridge.ts +260 -0
- package/src/skill-state/active-state.ts +21 -11
- package/src/slash-commands/acp-builtins.ts +9 -1
- package/src/slash-commands/builtin-registry.ts +117 -19
- package/src/slash-commands/helpers/credential-switch.ts +143 -0
- package/src/slash-commands/helpers/mcp.ts +9 -3
- package/src/slash-commands/helpers/usage-report.ts +61 -56
- package/src/slash-commands/types.ts +7 -2
- package/src/task/executor.ts +30 -11
- package/src/task/index.ts +66 -7
- package/src/task/render.ts +23 -9
- package/src/tools/ask-answer-registry.ts +23 -7
- package/src/tools/ask.ts +8 -4
- package/src/tools/bash-sleep-advisory.ts +115 -0
- package/src/tools/bash.ts +98 -18
- package/src/tools/browser/attach.ts +71 -3
- package/src/tools/browser/launch.ts +86 -13
- package/src/tools/browser/profile-discovery.ts +57 -7
- package/src/tools/browser/render.ts +8 -1
- package/src/tools/browser.ts +130 -21
- package/src/tools/computer.enforcement.test.ts +1 -1
- package/src/tools/computer.ts +3 -2
- package/src/tools/cron.ts +12 -1
- package/src/tools/image-gen.ts +349 -142
- package/src/tools/index.ts +8 -0
- package/src/tools/job.ts +33 -6
- package/src/tools/monitor.ts +41 -2
- package/src/tools/skill-discovery.ts +21 -29
- package/src/tools/subagent.ts +142 -24
- package/src/tools/todo-contract.ts +55 -3
- package/src/tools/todo-write.ts +4 -3
- package/src/tools/tool-catalog.generated.ts +6 -6
- package/src/tools/ultragoal-ask-guard.ts +10 -3
- package/src/utils/herdr-pane.ts +345 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/scrapers/index.ts +6 -4
- package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
- package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
- package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
- package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
- package/dist/types/runtime/lazy-service.test.d.ts +0 -1
- package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
- package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
- package/dist/types/runtime-mcp/config.test.d.ts +0 -1
- package/dist/types/runtime-mcp/http.test.d.ts +0 -1
- package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
- package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
- package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
- package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
- package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
- package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
- package/dist/types/sdk/elevation/audit.d.ts +0 -93
- package/dist/types/sdk/elevation/capability.d.ts +0 -3
- package/dist/types/sdk/elevation/digest.d.ts +0 -72
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
- package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
- package/dist/types/sdk/elevation/owner.d.ts +0 -48
- package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
- package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
- package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
- package/dist/types/session/auth-broker-config.d.ts +0 -13
- package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
- package/dist/types/skills/skills.test.d.ts +0 -1
- package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
- package/dist/types/task/token-log.test.d.ts +0 -1
- package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
- package/dist/types/tools/descriptors.test.d.ts +0 -1
- package/scripts/g005-daemon-smoke.ts +0 -180
- package/scripts/g011-daemon-path-smoke.ts +0 -153
- package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
- package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
- package/src/sdk/bus/telegram-cli.ts +0 -212
- package/src/sdk/elevation/allowlist.ts +0 -54
- package/src/sdk/elevation/audit.ts +0 -208
- package/src/sdk/elevation/capability.ts +0 -41
- package/src/sdk/elevation/digest.ts +0 -129
- package/src/sdk/elevation/dispatch-receipt.ts +0 -184
- package/src/sdk/elevation/grant-ledger.ts +0 -1142
- package/src/sdk/elevation/owner.ts +0 -109
- package/src/session/auth-broker-config.ts +0 -109
- /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
- /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
- /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import flow for the `/extensions` umbrella customization surface (issue #4291).
|
|
3
|
+
*
|
|
4
|
+
* Source layouts are consumed through the sibling contracts instead of local
|
|
5
|
+
* reimplementations:
|
|
6
|
+
* - Skills: `listConventionSkillImportSources` (#4285) — Claude `.claude/skills`
|
|
7
|
+
* and Codex `.codex/skills` layouts, both scopes.
|
|
8
|
+
* - Hooks: Claude `.claude/hooks/<pre|post>/<file>` and Codex flat
|
|
9
|
+
* `.codex/hooks/<phase>-<tool>.ts|js` conventions, normalized through the
|
|
10
|
+
* canonical hook IR (`normalizeDirectoryHook`, #4286/#4289) and written to
|
|
11
|
+
* the runtime-discovered `<root>/hooks/<pre|post>/` layout.
|
|
12
|
+
* - MCPs: `normalizeClaudeMcpJson` / `normalizeCodexMcpToml` plus
|
|
13
|
+
* `validateMCPCompatServer` (#4284 bounded compatibility adapters), merged
|
|
14
|
+
* through the canonical atomic config writer.
|
|
15
|
+
*
|
|
16
|
+
* The preview DTO is redacted and serialization-safe; full file contents and
|
|
17
|
+
* secret values live only in the separate `ImportPlan`. `applyImport` runs as
|
|
18
|
+
* a validated transaction: every destination path is containment- and
|
|
19
|
+
* symlink-checked before any write, destinations are revalidated against the
|
|
20
|
+
* preview decisions, files are published with same-directory temp+rename, and
|
|
21
|
+
* any failure restores exactly the files this transaction wrote (pre-existing
|
|
22
|
+
* symlinks are never touched). Verification failures roll back too.
|
|
23
|
+
*/
|
|
24
|
+
import type { Dirent } from "node:fs";
|
|
25
|
+
import * as fs from "node:fs/promises";
|
|
26
|
+
import * as path from "node:path";
|
|
27
|
+
import { parseFrontmatter, tryParseJson } from "@gajae-code/utils";
|
|
28
|
+
import { YAML } from "bun";
|
|
29
|
+
import type { MCPServer } from "../capability/mcp";
|
|
30
|
+
import { normalizeClaudeMcpJson, normalizeCodexMcpToml, validateMCPCompatServer } from "../discovery/mcp-compat";
|
|
31
|
+
import { listConventionSkillImportSources } from "../extensibility/skill-management";
|
|
32
|
+
import { HookSourceConvention } from "../hooks/events";
|
|
33
|
+
import { normalizeDirectoryHook } from "../hooks/normalize";
|
|
34
|
+
import { readMCPConfigFile, writeMCPConfigFile } from "../runtime-mcp/config-writer";
|
|
35
|
+
import type { MCPServerConfig } from "../runtime-mcp/types";
|
|
36
|
+
import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/canonical-skills";
|
|
37
|
+
import { IMPORTED_FROM_FRONTMATTER_KEY } from "./inventory";
|
|
38
|
+
import type {
|
|
39
|
+
ImportCollisionPolicy,
|
|
40
|
+
ImportPlan,
|
|
41
|
+
ImportPreviewEntry,
|
|
42
|
+
ImportProduct,
|
|
43
|
+
ImportResult,
|
|
44
|
+
ImportResultEntry,
|
|
45
|
+
ImportSourceScope,
|
|
46
|
+
NormalizedPayload,
|
|
47
|
+
} from "./types";
|
|
48
|
+
import { productLabel, resolveScopePaths, sourceConfigDir, sourceScopeLabel } from "./types";
|
|
49
|
+
|
|
50
|
+
export interface BuildImportPreviewOptions {
|
|
51
|
+
product: ImportProduct;
|
|
52
|
+
sourceScope: ImportSourceScope;
|
|
53
|
+
destinationScope: "project" | "global";
|
|
54
|
+
surfaces?: readonly ("skills" | "hooks" | "mcps")[];
|
|
55
|
+
collisionPolicy: ImportCollisionPolicy;
|
|
56
|
+
cwd: string;
|
|
57
|
+
homeDir: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PROTECTED_SKILL_NAMES = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS);
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Structured filesystem reads — absent/value/error, never silent
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
type ReadResult = { kind: "absent" } | { kind: "value"; content: string } | { kind: "error"; message: string };
|
|
67
|
+
|
|
68
|
+
function isEnoent(error: unknown): boolean {
|
|
69
|
+
return (error as NodeJS.ErrnoException)?.code === "ENOENT";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Read a regular file. Symlinks and non-files are errors, never "absent". */
|
|
73
|
+
async function readStructured(filePath: string): Promise<ReadResult> {
|
|
74
|
+
let stat: Awaited<ReturnType<typeof fs.lstat>>;
|
|
75
|
+
try {
|
|
76
|
+
stat = await fs.lstat(filePath);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (isEnoent(error)) return { kind: "absent" };
|
|
79
|
+
return { kind: "error", message: `cannot stat ${filePath}: ${(error as Error).message}` };
|
|
80
|
+
}
|
|
81
|
+
if (stat.isSymbolicLink()) return { kind: "error", message: `refusing unsafe symlink: ${filePath}` };
|
|
82
|
+
if (!stat.isFile()) return { kind: "error", message: `not a regular file: ${filePath}` };
|
|
83
|
+
try {
|
|
84
|
+
return { kind: "value", content: await fs.readFile(filePath, "utf-8") };
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (isEnoent(error)) return { kind: "absent" };
|
|
87
|
+
return { kind: "error", message: `failed to read ${filePath}: ${(error as Error).message}` };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function listDirNames(dir: string, kind: "file" | "directory"): Promise<string[]> {
|
|
92
|
+
let entries: Dirent[];
|
|
93
|
+
try {
|
|
94
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
return entries
|
|
99
|
+
.filter(entry => (kind === "file" ? entry.isFile() : entry.isDirectory()) && !entry.name.startsWith("."))
|
|
100
|
+
.map(entry => entry.name)
|
|
101
|
+
.sort();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Name safety
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/** A destination name must be a single safe path segment. */
|
|
109
|
+
function isSafeName(name: string): boolean {
|
|
110
|
+
return (
|
|
111
|
+
name.length > 0 &&
|
|
112
|
+
name !== "." &&
|
|
113
|
+
name !== ".." &&
|
|
114
|
+
!name.includes("/") &&
|
|
115
|
+
!name.includes("\\") &&
|
|
116
|
+
!name.includes("\0") &&
|
|
117
|
+
!name.split("").some(ch => ch < " ")
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Provenance marker
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/** Inject the imported-from provenance key into a normalized SKILL.md. */
|
|
126
|
+
function withImportProvenance(content: string, product: ImportProduct): string {
|
|
127
|
+
const { frontmatter, body } = parseFrontmatter(content, { level: "off" });
|
|
128
|
+
// parseFrontmatter camelCases keys; drop a prior camelized marker so the
|
|
129
|
+
// canonical kebab-case key is written exactly once.
|
|
130
|
+
const { xGjcImportedFrom: _priorMarker, ...rest } = frontmatter;
|
|
131
|
+
const fm: Record<string, unknown> = { ...rest, [IMPORTED_FROM_FRONTMATTER_KEY]: product };
|
|
132
|
+
const yaml = YAML.stringify(fm).trimEnd();
|
|
133
|
+
return `---\n${yaml}\n---\n\n${body.trim()}\n`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Collision handling
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
/** Find a free `<base>-imported` / `<base>-imported-N` destination name. */
|
|
141
|
+
function renamedDestination(base: string, taken: (name: string) => boolean): string {
|
|
142
|
+
let candidate = `${base}-imported`;
|
|
143
|
+
let index = 2;
|
|
144
|
+
while (taken(candidate)) {
|
|
145
|
+
candidate = `${base}-imported-${index}`;
|
|
146
|
+
index += 1;
|
|
147
|
+
}
|
|
148
|
+
return candidate;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function applyCollision(
|
|
152
|
+
entry: Omit<ImportPreviewEntry, "status">,
|
|
153
|
+
existing: ReadResult,
|
|
154
|
+
identicalContent: string | undefined,
|
|
155
|
+
policy: ImportCollisionPolicy,
|
|
156
|
+
rename: (base: string) => string,
|
|
157
|
+
): ImportPreviewEntry {
|
|
158
|
+
if (existing.kind === "error") {
|
|
159
|
+
return { ...entry, status: "conflict", reason: `destination is unsafe or unreadable: ${existing.message}` };
|
|
160
|
+
}
|
|
161
|
+
const exists = existing.kind === "value";
|
|
162
|
+
if (!exists) return { ...entry, status: "add" };
|
|
163
|
+
if (identicalContent !== undefined && existing.content === identicalContent) {
|
|
164
|
+
return {
|
|
165
|
+
...entry,
|
|
166
|
+
status: "conflict",
|
|
167
|
+
reason: "identical content already present at destination (import is a no-op)",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
switch (policy) {
|
|
171
|
+
case "skip":
|
|
172
|
+
return { ...entry, status: "conflict", reason: "destination exists (collision policy: skip)" };
|
|
173
|
+
case "overwrite":
|
|
174
|
+
return { ...entry, status: "overwrite", reason: "destination exists (collision policy: overwrite)" };
|
|
175
|
+
case "rename": {
|
|
176
|
+
const destinationName = rename(entry.destinationName);
|
|
177
|
+
return {
|
|
178
|
+
...entry,
|
|
179
|
+
destinationName,
|
|
180
|
+
status: "add",
|
|
181
|
+
reason: `destination "${entry.destinationName}" exists; renamed under collision policy: rename`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Hook source candidates
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
const CLAUDE_HOOK_EXTENSIONS = [".sh", ".bash", ".zsh", ".fish", ".ts", ".js"] as const;
|
|
192
|
+
|
|
193
|
+
interface HookCandidate {
|
|
194
|
+
phase: "pre" | "post";
|
|
195
|
+
fileName: string;
|
|
196
|
+
filePath: string;
|
|
197
|
+
toolName: string;
|
|
198
|
+
sourceCategory: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function collectSourceHooks(
|
|
202
|
+
product: ImportProduct,
|
|
203
|
+
configDir: string,
|
|
204
|
+
): Promise<{ candidates: HookCandidate[]; warnings: string[] }> {
|
|
205
|
+
const candidates: HookCandidate[] = [];
|
|
206
|
+
const warnings: string[] = [];
|
|
207
|
+
if (product === "claude-code") {
|
|
208
|
+
// Canonical Claude layout: `.claude/hooks/<pre|post>/<file>`.
|
|
209
|
+
for (const phase of ["pre", "post"] as const) {
|
|
210
|
+
const dir = path.join(configDir, "hooks", phase);
|
|
211
|
+
for (const fileName of await listDirNames(dir, "file")) {
|
|
212
|
+
const ext = CLAUDE_HOOK_EXTENSIONS.find(e => fileName.endsWith(e));
|
|
213
|
+
if (!ext) {
|
|
214
|
+
warnings.push(`skipped ${phase} hook "${fileName}": unsupported extension`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
candidates.push({
|
|
218
|
+
phase,
|
|
219
|
+
fileName,
|
|
220
|
+
filePath: path.join(dir, fileName),
|
|
221
|
+
toolName: fileName.slice(0, -ext.length),
|
|
222
|
+
sourceCategory: `hook file (${phase}/)`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { candidates, warnings };
|
|
227
|
+
}
|
|
228
|
+
// Canonical Codex layout: flat `.codex/hooks/<phase>-<tool>.ts|js`.
|
|
229
|
+
const dir = path.join(configDir, "hooks");
|
|
230
|
+
for (const fileName of await listDirNames(dir, "file")) {
|
|
231
|
+
const match = fileName.match(/^(pre|post)-(.+)\.(ts|js)$/);
|
|
232
|
+
if (!match) {
|
|
233
|
+
warnings.push(
|
|
234
|
+
`skipped hook "${fileName}": filename must follow the pre-<tool>.ts|js or post-<tool>.ts|js convention`,
|
|
235
|
+
);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
candidates.push({
|
|
239
|
+
phase: match[1] as "pre" | "post",
|
|
240
|
+
fileName,
|
|
241
|
+
filePath: path.join(dir, fileName),
|
|
242
|
+
toolName: match[2],
|
|
243
|
+
sourceCategory: "hook file (flat)",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return { candidates, warnings };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// MCP source config path
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
function sourceMcpConfigPath(
|
|
254
|
+
product: ImportProduct,
|
|
255
|
+
sourceScope: ImportSourceScope,
|
|
256
|
+
cwd: string,
|
|
257
|
+
homeDir: string,
|
|
258
|
+
): string {
|
|
259
|
+
if (product === "claude-code") {
|
|
260
|
+
// Project-scope Claude MCP servers live in `<project>/.mcp.json`;
|
|
261
|
+
// user-global servers live in `~/.claude.json`.
|
|
262
|
+
return sourceScope === "project" ? path.join(cwd, ".mcp.json") : path.join(homeDir, ".claude.json");
|
|
263
|
+
}
|
|
264
|
+
return path.join(sourceConfigDir(product, sourceScope, cwd, homeDir), "config.toml");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Map a normalized compat MCPServer onto the canonical config writer shape. */
|
|
268
|
+
function toMCPServerConfig(server: MCPServer): MCPServerConfig {
|
|
269
|
+
const config: Record<string, unknown> = {};
|
|
270
|
+
if (server.transport) config.type = server.transport;
|
|
271
|
+
if (server.command !== undefined) config.command = server.command;
|
|
272
|
+
if (server.args !== undefined) config.args = server.args;
|
|
273
|
+
if (server.env !== undefined) config.env = server.env;
|
|
274
|
+
if (server.cwd !== undefined) config.cwd = server.cwd;
|
|
275
|
+
if (server.url !== undefined) config.url = server.url;
|
|
276
|
+
if (server.headers !== undefined) config.headers = server.headers;
|
|
277
|
+
if (server.enabled !== undefined) config.enabled = server.enabled;
|
|
278
|
+
if (server.autoload !== undefined) config.autoload = server.autoload;
|
|
279
|
+
if (server.timeout !== undefined) config.timeout = server.timeout;
|
|
280
|
+
if (server.sharing !== undefined) config.sharing = server.sharing;
|
|
281
|
+
if (server.noInheritEnv !== undefined) config.noInheritEnv = server.noInheritEnv;
|
|
282
|
+
return config as unknown as MCPServerConfig;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
286
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
interface DestinationMcpState {
|
|
290
|
+
/** Existing server entries, or null when the destination is unusable. */
|
|
291
|
+
servers: Record<string, unknown> | null;
|
|
292
|
+
/** Why the destination is unusable (malformed/unsafe), when servers is null. */
|
|
293
|
+
error?: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function nestedAuthServerNames(product: ImportProduct, content: string): Set<string> {
|
|
297
|
+
const names = new Set<string>();
|
|
298
|
+
if (product === "claude-code") {
|
|
299
|
+
const parsed = tryParseJson<Record<string, unknown>>(content);
|
|
300
|
+
const servers = parsed?.mcpServers;
|
|
301
|
+
if (!servers || !isRecord(servers)) return names;
|
|
302
|
+
for (const [name, value] of Object.entries(servers)) {
|
|
303
|
+
if (!isRecord(value)) continue;
|
|
304
|
+
if (value.auth !== undefined || value.oauth !== undefined) names.add(name);
|
|
305
|
+
}
|
|
306
|
+
return names;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const parsed = Bun.TOML.parse(content) as Record<string, unknown>;
|
|
310
|
+
const servers = parsed.mcp_servers;
|
|
311
|
+
if (!servers || !isRecord(servers)) return names;
|
|
312
|
+
for (const [name, value] of Object.entries(servers)) {
|
|
313
|
+
if (!isRecord(value)) continue;
|
|
314
|
+
if (value.auth !== undefined || value.oauth !== undefined) names.add(name);
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
// The canonical normalizer reports parse diagnostics.
|
|
318
|
+
}
|
|
319
|
+
return names;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function readDestinationMcpState(mcpConfigPath: string): Promise<DestinationMcpState> {
|
|
323
|
+
const read = await readStructured(mcpConfigPath);
|
|
324
|
+
if (read.kind === "absent") return { servers: {} };
|
|
325
|
+
if (read.kind === "error") return { servers: null, error: read.message };
|
|
326
|
+
const json = tryParseJson<Record<string, unknown>>(read.content);
|
|
327
|
+
if (!json || !isRecord(json)) {
|
|
328
|
+
return { servers: null, error: `destination ${mcpConfigPath} is malformed; MCP entries cannot be imported` };
|
|
329
|
+
}
|
|
330
|
+
if (json.mcpServers !== undefined && !isRecord(json.mcpServers)) {
|
|
331
|
+
return {
|
|
332
|
+
servers: null,
|
|
333
|
+
error: `destination ${mcpConfigPath} has a non-object mcpServers value; MCP entries cannot be imported`,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return { servers: (json.mcpServers as Record<string, unknown> | undefined) ?? {} };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Preview
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Read the selected source product/scope, normalize every candidate through
|
|
345
|
+
* the sibling contracts, and produce a redacted preview plus an opaque apply
|
|
346
|
+
* plan. Pure reads only — nothing is written until `applyImport`.
|
|
347
|
+
*/
|
|
348
|
+
export async function buildImportPreview(options: BuildImportPreviewOptions): Promise<ImportPlan> {
|
|
349
|
+
const surfaces = options.surfaces ?? (["skills", "hooks", "mcps"] as const);
|
|
350
|
+
const destination = resolveScopePaths(options.destinationScope, options.cwd);
|
|
351
|
+
const entries: ImportPreviewEntry[] = [];
|
|
352
|
+
const payloads: (NormalizedPayload | undefined)[] = [];
|
|
353
|
+
const warnings: string[] = [];
|
|
354
|
+
const productName = productLabel(options.product);
|
|
355
|
+
const sourceLabel = `${productName} ${sourceScopeLabel(options.sourceScope)}`;
|
|
356
|
+
|
|
357
|
+
const push = (entry: ImportPreviewEntry, payload?: NormalizedPayload): void => {
|
|
358
|
+
entries.push(entry);
|
|
359
|
+
payloads.push(payload);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// --- Skills -------------------------------------------------------------
|
|
363
|
+
if (surfaces.includes("skills")) {
|
|
364
|
+
const host = options.product === "claude-code" ? "claude" : "codex";
|
|
365
|
+
const sources = await listConventionSkillImportSources({ cwd: options.cwd, home: options.homeDir, host });
|
|
366
|
+
const scoped = sources.filter(source =>
|
|
367
|
+
options.sourceScope === "project" ? source.scope === "project" : source.scope === "user",
|
|
368
|
+
);
|
|
369
|
+
const takenSkillNames = new Set(await listDirNames(destination.skillsDir, "directory"));
|
|
370
|
+
for (const source of scoped) {
|
|
371
|
+
const slug = source.name;
|
|
372
|
+
if (PROTECTED_SKILL_NAMES.has(slug)) {
|
|
373
|
+
push({
|
|
374
|
+
surface: "skills",
|
|
375
|
+
sourceName: slug,
|
|
376
|
+
destinationName: slug,
|
|
377
|
+
status: "unsupported",
|
|
378
|
+
sourceCategory: "skill directory",
|
|
379
|
+
description: `import skill "${slug}"`,
|
|
380
|
+
reason: "protected bundled GJC workflow skill name; foreign copies cannot override bundled authority",
|
|
381
|
+
});
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (!isSafeName(slug)) {
|
|
385
|
+
push({
|
|
386
|
+
surface: "skills",
|
|
387
|
+
sourceName: slug,
|
|
388
|
+
destinationName: slug,
|
|
389
|
+
status: "unsupported",
|
|
390
|
+
sourceCategory: "skill directory",
|
|
391
|
+
description: `import skill "${slug}"`,
|
|
392
|
+
reason: "unsafe skill name (path separators, control characters, or traversal segments are not allowed)",
|
|
393
|
+
});
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const read = await readStructured(source.path);
|
|
397
|
+
if (read.kind !== "value") {
|
|
398
|
+
push({
|
|
399
|
+
surface: "skills",
|
|
400
|
+
sourceName: slug,
|
|
401
|
+
destinationName: slug,
|
|
402
|
+
status: "unsupported",
|
|
403
|
+
sourceCategory: "skill directory",
|
|
404
|
+
description: `import skill "${slug}"`,
|
|
405
|
+
reason: read.kind === "error" ? read.message : `no SKILL.md content at ${source.path}`,
|
|
406
|
+
});
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const content = withImportProvenance(read.content, options.product);
|
|
410
|
+
const { frontmatter } = parseFrontmatter(content, { level: "off" });
|
|
411
|
+
const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
|
|
412
|
+
if (!description) {
|
|
413
|
+
push({
|
|
414
|
+
surface: "skills",
|
|
415
|
+
sourceName: slug,
|
|
416
|
+
destinationName: slug,
|
|
417
|
+
status: "unsupported",
|
|
418
|
+
sourceCategory: "skill directory",
|
|
419
|
+
description: `import skill "${slug}"`,
|
|
420
|
+
reason: "skill frontmatter must include a non-empty description",
|
|
421
|
+
});
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const existing = await readStructured(path.join(destination.skillsDir, slug, "SKILL.md"));
|
|
425
|
+
const entry = applyCollision(
|
|
426
|
+
{
|
|
427
|
+
surface: "skills",
|
|
428
|
+
sourceName: slug,
|
|
429
|
+
destinationName: slug,
|
|
430
|
+
sourceCategory: "skill directory",
|
|
431
|
+
description: `import skill "${slug}" into ${destination.skillsDir}`,
|
|
432
|
+
},
|
|
433
|
+
existing,
|
|
434
|
+
content,
|
|
435
|
+
options.collisionPolicy,
|
|
436
|
+
base => renamedDestination(base, name => takenSkillNames.has(name)),
|
|
437
|
+
);
|
|
438
|
+
takenSkillNames.add(entry.destinationName);
|
|
439
|
+
let destinationContent = content;
|
|
440
|
+
if (entry.destinationName !== slug) {
|
|
441
|
+
const { frontmatter, body } = parseFrontmatter(content, { level: "off" });
|
|
442
|
+
destinationContent = `---\n${YAML.stringify({ ...frontmatter, name: entry.destinationName }).trimEnd()}\n---\n\n${body.trim()}\n`;
|
|
443
|
+
}
|
|
444
|
+
push(entry, { skill: { slug: entry.destinationName, content: destinationContent } });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// --- Hooks --------------------------------------------------------------
|
|
449
|
+
if (surfaces.includes("hooks")) {
|
|
450
|
+
const convention =
|
|
451
|
+
options.product === "claude-code" ? HookSourceConvention.ClaudeCode : HookSourceConvention.Codex;
|
|
452
|
+
const configDir = sourceConfigDir(options.product, options.sourceScope, options.cwd, options.homeDir);
|
|
453
|
+
const collected = await collectSourceHooks(options.product, configDir);
|
|
454
|
+
for (const warning of collected.warnings) warnings.push(`${sourceLabel} hooks: ${warning}`);
|
|
455
|
+
// Destination identity is the canonical phase-relative path.
|
|
456
|
+
const takenHookPaths = new Set<string>();
|
|
457
|
+
for (const phase of ["pre", "post"] as const) {
|
|
458
|
+
for (const fileName of await listDirNames(path.join(destination.hooksDir, phase), "file")) {
|
|
459
|
+
takenHookPaths.add(`${phase}/${fileName}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
for (const candidate of collected.candidates) {
|
|
463
|
+
const canonicalFileName =
|
|
464
|
+
options.product === "codex"
|
|
465
|
+
? `${candidate.toolName}${path.extname(candidate.fileName)}`
|
|
466
|
+
: candidate.fileName;
|
|
467
|
+
const relDest = `${candidate.phase}/${canonicalFileName}`;
|
|
468
|
+
const normalized = normalizeDirectoryHook({
|
|
469
|
+
convention,
|
|
470
|
+
phase: candidate.phase,
|
|
471
|
+
toolName: candidate.toolName,
|
|
472
|
+
source: candidate.filePath,
|
|
473
|
+
externalName: candidate.fileName,
|
|
474
|
+
});
|
|
475
|
+
if (!normalized.hook) {
|
|
476
|
+
push({
|
|
477
|
+
surface: "hooks",
|
|
478
|
+
sourceName: relDest,
|
|
479
|
+
destinationName: relDest,
|
|
480
|
+
status: "unsupported",
|
|
481
|
+
sourceCategory: candidate.sourceCategory,
|
|
482
|
+
description: `${candidate.phase} hook for ${candidate.toolName}`,
|
|
483
|
+
reason: normalized.diagnostics.map(d => `${d.code}: ${d.message}`).join("; "),
|
|
484
|
+
});
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const read = await readStructured(candidate.filePath);
|
|
488
|
+
if (read.kind !== "value") {
|
|
489
|
+
push({
|
|
490
|
+
surface: "hooks",
|
|
491
|
+
sourceName: relDest,
|
|
492
|
+
destinationName: relDest,
|
|
493
|
+
status: "unsupported",
|
|
494
|
+
sourceCategory: candidate.sourceCategory,
|
|
495
|
+
description: `${candidate.phase} hook for ${candidate.toolName}`,
|
|
496
|
+
reason: read.kind === "error" ? read.message : `no hook content at ${candidate.filePath}`,
|
|
497
|
+
});
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const existing = await readStructured(path.join(destination.hooksDir, candidate.phase, canonicalFileName));
|
|
501
|
+
const entry = applyCollision(
|
|
502
|
+
{
|
|
503
|
+
surface: "hooks",
|
|
504
|
+
sourceName: relDest,
|
|
505
|
+
destinationName: relDest,
|
|
506
|
+
sourceCategory: candidate.sourceCategory,
|
|
507
|
+
description: `${candidate.phase} hook for ${candidate.toolName} (${normalized.hook.contract.runtimeEvent})`,
|
|
508
|
+
},
|
|
509
|
+
existing,
|
|
510
|
+
read.content,
|
|
511
|
+
options.collisionPolicy,
|
|
512
|
+
base => {
|
|
513
|
+
const ext = path.extname(base);
|
|
514
|
+
const stem = base.slice(candidate.phase.length + 1, base.length - ext.length);
|
|
515
|
+
const renamed = renamedDestination(stem, name => takenHookPaths.has(`${candidate.phase}/${name}${ext}`));
|
|
516
|
+
return `${candidate.phase}/${renamed}${ext}`;
|
|
517
|
+
},
|
|
518
|
+
);
|
|
519
|
+
takenHookPaths.add(entry.destinationName);
|
|
520
|
+
push(entry, {
|
|
521
|
+
hook: {
|
|
522
|
+
phase: candidate.phase,
|
|
523
|
+
fileName: entry.destinationName.slice(candidate.phase.length + 1),
|
|
524
|
+
content: read.content,
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// --- MCPs ---------------------------------------------------------------
|
|
531
|
+
if (surfaces.includes("mcps")) {
|
|
532
|
+
const sourcePath = sourceMcpConfigPath(options.product, options.sourceScope, options.cwd, options.homeDir);
|
|
533
|
+
const read = await readStructured(sourcePath);
|
|
534
|
+
if (read.kind === "error") {
|
|
535
|
+
warnings.push(`${sourceLabel} MCPs: ${read.message}`);
|
|
536
|
+
}
|
|
537
|
+
const normalized =
|
|
538
|
+
read.kind === "value"
|
|
539
|
+
? options.product === "claude-code"
|
|
540
|
+
? normalizeClaudeMcpJson(read.content, sourcePath, options.sourceScope)
|
|
541
|
+
: normalizeCodexMcpToml(read.content, sourcePath, options.sourceScope)
|
|
542
|
+
: { items: [], warnings: [] as string[] };
|
|
543
|
+
for (const warning of normalized.warnings) warnings.push(`${sourceLabel} MCPs: ${warning}`);
|
|
544
|
+
const unsupportedNestedAuth =
|
|
545
|
+
read.kind === "value" ? nestedAuthServerNames(options.product, read.content) : new Set<string>();
|
|
546
|
+
if (normalized.items.length > 0) {
|
|
547
|
+
const destState = await readDestinationMcpState(destination.mcpConfigPath);
|
|
548
|
+
if (destState.servers === null) {
|
|
549
|
+
warnings.push(destState.error ?? `destination ${destination.mcpConfigPath} is unusable`);
|
|
550
|
+
}
|
|
551
|
+
const existingServers = destState.servers ?? {};
|
|
552
|
+
const takenMcpNames = new Set(Object.keys(existingServers));
|
|
553
|
+
for (const server of normalized.items) {
|
|
554
|
+
const name = server.name;
|
|
555
|
+
if (unsupportedNestedAuth.has(name)) {
|
|
556
|
+
push({
|
|
557
|
+
surface: "mcps",
|
|
558
|
+
sourceName: name,
|
|
559
|
+
destinationName: name,
|
|
560
|
+
status: "unsupported",
|
|
561
|
+
sourceCategory: "MCP server entry",
|
|
562
|
+
description: `MCP server "${name}" (credentials redacted)`,
|
|
563
|
+
reason:
|
|
564
|
+
"nested auth/oauth configuration is not supported by the canonical GJC MCP contract; import was skipped to avoid dropping credentials",
|
|
565
|
+
});
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const invalidReason = !isSafeName(name)
|
|
569
|
+
? "unsafe MCP server name (path separators, control characters, or traversal segments are not allowed)"
|
|
570
|
+
: validateMCPCompatServer(server);
|
|
571
|
+
if (invalidReason || destState.servers === null) {
|
|
572
|
+
push({
|
|
573
|
+
surface: "mcps",
|
|
574
|
+
sourceName: name,
|
|
575
|
+
destinationName: name,
|
|
576
|
+
status: "unsupported",
|
|
577
|
+
sourceCategory: "MCP server entry",
|
|
578
|
+
description: `MCP server "${name}"`,
|
|
579
|
+
reason: invalidReason ?? destState.error ?? "destination MCP config is unusable",
|
|
580
|
+
});
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const config = toMCPServerConfig(server);
|
|
584
|
+
const existing = existingServers[name];
|
|
585
|
+
const secretKeys = [
|
|
586
|
+
...(config && "env" in config && config.env ? Object.keys(config.env).map(k => `env:${k}`) : []),
|
|
587
|
+
...(config && "headers" in config && config.headers
|
|
588
|
+
? Object.keys(config.headers).map(k => `header:${k}`)
|
|
589
|
+
: []),
|
|
590
|
+
];
|
|
591
|
+
const entry = applyCollision(
|
|
592
|
+
{
|
|
593
|
+
surface: "mcps",
|
|
594
|
+
sourceName: name,
|
|
595
|
+
destinationName: name,
|
|
596
|
+
sourceCategory: "MCP server entry",
|
|
597
|
+
description:
|
|
598
|
+
config && "command" in config && typeof config.command === "string"
|
|
599
|
+
? `stdio MCP "${name}" (command redacted)`
|
|
600
|
+
: `${config && "type" in config ? String(config.type) : "http"} MCP "${name}" (endpoint redacted)`,
|
|
601
|
+
},
|
|
602
|
+
existing === undefined ? { kind: "absent" } : { kind: "value", content: JSON.stringify(existing) },
|
|
603
|
+
JSON.stringify(config),
|
|
604
|
+
options.collisionPolicy,
|
|
605
|
+
base => renamedDestination(base, candidate => takenMcpNames.has(candidate)),
|
|
606
|
+
);
|
|
607
|
+
takenMcpNames.add(entry.destinationName);
|
|
608
|
+
if (secretKeys.length > 0 && (entry.status === "add" || entry.status === "overwrite")) {
|
|
609
|
+
entry.reason = `${entry.reason ? `${entry.reason}; ` : ""}secret values hidden in preview (keys: ${secretKeys.join(", ")})`;
|
|
610
|
+
}
|
|
611
|
+
push(entry, { mcp: { name: entry.destinationName, config } });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
preview: {
|
|
618
|
+
product: options.product,
|
|
619
|
+
sourceScope: options.sourceScope,
|
|
620
|
+
destinationScope: options.destinationScope,
|
|
621
|
+
surfaces: [...surfaces],
|
|
622
|
+
entries,
|
|
623
|
+
warnings,
|
|
624
|
+
},
|
|
625
|
+
payloads,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ---------------------------------------------------------------------------
|
|
630
|
+
// Apply (validated transaction: staged atomic writes + journaled rollback)
|
|
631
|
+
// ---------------------------------------------------------------------------
|
|
632
|
+
|
|
633
|
+
interface FileSnapshot {
|
|
634
|
+
path: string;
|
|
635
|
+
/** Previous content, or null when the file did not exist. */
|
|
636
|
+
previous: string | null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
interface PlannedFileWrite {
|
|
640
|
+
path: string;
|
|
641
|
+
content: string;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Walk `root` itself and every existing component down to `target`'s parent
|
|
646
|
+
* directory; return the first symlinked component, or null. A target outside
|
|
647
|
+
* `root` is reported as the root itself (containment is checked separately).
|
|
648
|
+
*/
|
|
649
|
+
async function findSymlinkedAncestor(root: string, target: string): Promise<string | null> {
|
|
650
|
+
const relative = path.relative(root, path.dirname(target));
|
|
651
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) return root;
|
|
652
|
+
const segments = relative === "" ? [] : relative.split(path.sep);
|
|
653
|
+
let current = root;
|
|
654
|
+
try {
|
|
655
|
+
const stat = await fs.lstat(current);
|
|
656
|
+
if (stat.isSymbolicLink()) return current;
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (!isEnoent(error)) throw error;
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
for (const segment of segments) {
|
|
662
|
+
current = path.join(current, segment);
|
|
663
|
+
try {
|
|
664
|
+
const stat = await fs.lstat(current);
|
|
665
|
+
if (stat.isSymbolicLink()) return current;
|
|
666
|
+
} catch (error) {
|
|
667
|
+
if (isEnoent(error)) return null;
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** Remove stale staging temp files from an interrupted earlier apply. */
|
|
675
|
+
async function sweepStagingTemps(dir: string): Promise<void> {
|
|
676
|
+
let names: string[];
|
|
677
|
+
try {
|
|
678
|
+
names = await fs.readdir(dir);
|
|
679
|
+
} catch {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
for (const name of names) {
|
|
683
|
+
if (!name.includes(".gjc-import-") || !name.endsWith(".tmp")) continue;
|
|
684
|
+
await fs.rm(path.join(dir, name), { force: true }).catch(() => {});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Apply a confirmed plan. The transaction contract:
|
|
690
|
+
*
|
|
691
|
+
* 1. Pre-validation — every destination path is derived from validated plan
|
|
692
|
+
* identities (never raw payload names), containment-checked beneath the
|
|
693
|
+
* canonical scope directories, and walked for symlinked ancestors; the
|
|
694
|
+
* destination MCP config shape is validated; add/rename destinations are
|
|
695
|
+
* revalidated against the preview decision (stale previews fail closed).
|
|
696
|
+
* Any problem aborts the whole apply before a single write.
|
|
697
|
+
* 2. Publication — each skill/hook file is written to a same-directory temp
|
|
698
|
+
* file (0600) and atomically renamed into place; MCP entries merge into one
|
|
699
|
+
* in-memory config written once via the canonical atomic writer. Only
|
|
700
|
+
* files this transaction actually publishes enter the rollback journal.
|
|
701
|
+
* 3. Verification — persisted state is re-read inside the transaction; a
|
|
702
|
+
* verification mismatch rolls back exactly like a write failure.
|
|
703
|
+
* 4. Rollback — restores journal snapshots (or removes created files) and
|
|
704
|
+
* reports every restore failure honestly instead of claiming success.
|
|
705
|
+
*/
|
|
706
|
+
export async function applyImport(plan: ImportPlan, options: { cwd: string }): Promise<ImportResult> {
|
|
707
|
+
const { preview } = plan;
|
|
708
|
+
const destination = resolveScopePaths(preview.destinationScope, options.cwd);
|
|
709
|
+
const results: ImportResultEntry[] = [];
|
|
710
|
+
|
|
711
|
+
const writable: Array<{ entry: ImportPreviewEntry; payload: NormalizedPayload }> = [];
|
|
712
|
+
preview.entries.forEach((entry, index) => {
|
|
713
|
+
const payload = plan.payloads[index];
|
|
714
|
+
if (entry.status === "conflict" || entry.status === "unsupported") {
|
|
715
|
+
results.push({
|
|
716
|
+
surface: entry.surface,
|
|
717
|
+
sourceName: entry.sourceName,
|
|
718
|
+
destinationName: entry.destinationName,
|
|
719
|
+
outcome: "skipped",
|
|
720
|
+
reason: entry.reason ?? (entry.status === "unsupported" ? "unsupported semantics" : undefined),
|
|
721
|
+
});
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (!payload) {
|
|
725
|
+
results.push({
|
|
726
|
+
surface: entry.surface,
|
|
727
|
+
sourceName: entry.sourceName,
|
|
728
|
+
destinationName: entry.destinationName,
|
|
729
|
+
outcome: "failed",
|
|
730
|
+
reason: "internal error: writable preview entry has no plan payload",
|
|
731
|
+
});
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
writable.push({ entry, payload });
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
const failAll = (reason: string): ImportResult => {
|
|
738
|
+
for (const { entry } of writable) {
|
|
739
|
+
results.push({
|
|
740
|
+
surface: entry.surface,
|
|
741
|
+
sourceName: entry.sourceName,
|
|
742
|
+
destinationName: entry.destinationName,
|
|
743
|
+
outcome: "failed",
|
|
744
|
+
reason,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
return { entries: results, ok: false };
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// --- Phase 1: pre-validation -------------------------------------------
|
|
751
|
+
const fileWrites: PlannedFileWrite[] = [];
|
|
752
|
+
const mcpWrites: Array<{ entry: ImportPreviewEntry; name: string; config: MCPServerConfig }> = [];
|
|
753
|
+
let mcpConfig: Awaited<ReturnType<typeof readMCPConfigFile>> | null = null;
|
|
754
|
+
|
|
755
|
+
for (const { entry, payload } of writable) {
|
|
756
|
+
if (payload.skill) {
|
|
757
|
+
const slug = payload.skill.slug;
|
|
758
|
+
if (!isSafeName(slug)) return failAll(`refusing unsafe skill destination name: ${slug}`);
|
|
759
|
+
const target = path.join(destination.skillsDir, slug, "SKILL.md");
|
|
760
|
+
const relative = path.relative(destination.skillsDir, target);
|
|
761
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
762
|
+
return failAll(`refusing skill destination outside ${destination.skillsDir}: ${slug}`);
|
|
763
|
+
}
|
|
764
|
+
const symlinked = await findSymlinkedAncestor(destination.root, target).catch(error => {
|
|
765
|
+
return `error:${(error as Error).message}`;
|
|
766
|
+
});
|
|
767
|
+
if (typeof symlinked === "string") {
|
|
768
|
+
return failAll(
|
|
769
|
+
symlinked.startsWith("error:")
|
|
770
|
+
? `cannot validate destination ancestors: ${symlinked.slice(6)}`
|
|
771
|
+
: `refusing to write through symlinked ancestor: ${symlinked}`,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
const existing = await readStructured(target);
|
|
775
|
+
if (existing.kind === "error") return failAll(existing.message);
|
|
776
|
+
if (entry.status !== "overwrite" && existing.kind === "value" && existing.content !== payload.skill.content) {
|
|
777
|
+
return failAll(
|
|
778
|
+
`destination changed since preview for skill "${slug}"; rebuild the preview instead of overwriting silently`,
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
fileWrites.push({ path: target, content: payload.skill.content });
|
|
782
|
+
} else if (payload.hook) {
|
|
783
|
+
const { phase, fileName } = payload.hook;
|
|
784
|
+
if (phase !== "pre" && phase !== "post") return failAll(`refusing unknown hook phase: ${String(phase)}`);
|
|
785
|
+
if (!isSafeName(fileName)) return failAll(`refusing unsafe hook destination name: ${fileName}`);
|
|
786
|
+
const target = path.join(destination.hooksDir, phase, fileName);
|
|
787
|
+
const relative = path.relative(destination.hooksDir, target);
|
|
788
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
789
|
+
return failAll(`refusing hook destination outside ${destination.hooksDir}: ${fileName}`);
|
|
790
|
+
}
|
|
791
|
+
const symlinked = await findSymlinkedAncestor(destination.root, target).catch(error => {
|
|
792
|
+
return `error:${(error as Error).message}`;
|
|
793
|
+
});
|
|
794
|
+
if (typeof symlinked === "string") {
|
|
795
|
+
return failAll(
|
|
796
|
+
symlinked.startsWith("error:")
|
|
797
|
+
? `cannot validate destination ancestors: ${symlinked.slice(6)}`
|
|
798
|
+
: `refusing to write through symlinked ancestor: ${symlinked}`,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
const existing = await readStructured(target);
|
|
802
|
+
if (existing.kind === "error") return failAll(existing.message);
|
|
803
|
+
if (entry.status !== "overwrite" && existing.kind === "value" && existing.content !== payload.hook.content) {
|
|
804
|
+
return failAll(
|
|
805
|
+
`destination changed since preview for hook "${phase}/${fileName}"; rebuild the preview instead of overwriting silently`,
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
fileWrites.push({ path: target, content: payload.hook.content });
|
|
809
|
+
} else if (payload.mcp) {
|
|
810
|
+
mcpWrites.push({ entry, name: payload.mcp.name, config: payload.mcp.config });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (mcpWrites.length > 0) {
|
|
815
|
+
const mcpSymlink = await findSymlinkedAncestor(destination.root, destination.mcpConfigPath).catch(error => {
|
|
816
|
+
return `error:${(error as Error).message}`;
|
|
817
|
+
});
|
|
818
|
+
if (typeof mcpSymlink === "string") {
|
|
819
|
+
return failAll(
|
|
820
|
+
mcpSymlink.startsWith("error:")
|
|
821
|
+
? `cannot validate MCP destination ancestors: ${mcpSymlink.slice(6)}`
|
|
822
|
+
: `refusing to write MCP config through symlinked ancestor: ${mcpSymlink}`,
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
const destState = await readDestinationMcpState(destination.mcpConfigPath);
|
|
826
|
+
if (destState.servers === null) {
|
|
827
|
+
return failAll(destState.error ?? `destination ${destination.mcpConfigPath} is unusable`);
|
|
828
|
+
}
|
|
829
|
+
const raw = await readStructured(destination.mcpConfigPath);
|
|
830
|
+
mcpConfig =
|
|
831
|
+
raw.kind === "value"
|
|
832
|
+
? (tryParseJson<Record<string, unknown>>(raw.content) as Awaited<ReturnType<typeof readMCPConfigFile>>)
|
|
833
|
+
: { mcpServers: {} };
|
|
834
|
+
for (const write of mcpWrites) {
|
|
835
|
+
if (!isSafeName(write.name)) return failAll(`refusing unsafe MCP destination name: ${write.name}`);
|
|
836
|
+
const existing = destState.servers[write.name];
|
|
837
|
+
if (
|
|
838
|
+
write.entry.status !== "overwrite" &&
|
|
839
|
+
existing !== undefined &&
|
|
840
|
+
JSON.stringify(existing) !== JSON.stringify(write.config)
|
|
841
|
+
) {
|
|
842
|
+
return failAll(
|
|
843
|
+
`destination changed since preview for MCP "${write.name}"; rebuild the preview instead of overwriting silently`,
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// --- Phase 2: publication ------------------------------------------------
|
|
850
|
+
const snapshots: FileSnapshot[] = [];
|
|
851
|
+
for (const dir of [destination.skillsDir, destination.hooksDir]) {
|
|
852
|
+
await sweepStagingTemps(dir).catch(() => {});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
try {
|
|
856
|
+
for (const [index, write] of fileWrites.entries()) {
|
|
857
|
+
await fs.mkdir(path.dirname(write.path), { recursive: true, mode: 0o700 });
|
|
858
|
+
const stat = await fs.lstat(write.path).catch(() => null);
|
|
859
|
+
if (stat?.isSymbolicLink()) throw new Error(`refusing to write through symlink: ${write.path}`);
|
|
860
|
+
const prior = await readStructured(write.path);
|
|
861
|
+
if (prior.kind === "error") throw new Error(prior.message);
|
|
862
|
+
const staged = path.join(
|
|
863
|
+
path.dirname(write.path),
|
|
864
|
+
`.gjc-import-${process.pid}-${index}-${path.basename(write.path)}.tmp`,
|
|
865
|
+
);
|
|
866
|
+
await fs.writeFile(staged, write.content, { encoding: "utf-8", mode: 0o600 });
|
|
867
|
+
await fs.rename(staged, write.path);
|
|
868
|
+
snapshots.push({ path: write.path, previous: prior.kind === "value" ? prior.content : null });
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (mcpConfig && mcpWrites.length > 0) {
|
|
872
|
+
const mcpSymlink = await findSymlinkedAncestor(destination.root, destination.mcpConfigPath);
|
|
873
|
+
if (mcpSymlink) throw new Error(`refusing to write MCP config through symlinked ancestor: ${mcpSymlink}`);
|
|
874
|
+
const mcpStat = await fs.lstat(destination.mcpConfigPath).catch(() => null);
|
|
875
|
+
if (mcpStat?.isSymbolicLink()) {
|
|
876
|
+
throw new Error(`refusing to write through symlinked MCP config: ${destination.mcpConfigPath}`);
|
|
877
|
+
}
|
|
878
|
+
const priorRaw = await readStructured(destination.mcpConfigPath);
|
|
879
|
+
snapshots.push({
|
|
880
|
+
path: destination.mcpConfigPath,
|
|
881
|
+
previous: priorRaw.kind === "value" ? priorRaw.content : null,
|
|
882
|
+
});
|
|
883
|
+
const servers = { ...(mcpConfig.mcpServers ?? {}) };
|
|
884
|
+
for (const write of mcpWrites) {
|
|
885
|
+
servers[write.name] = write.config;
|
|
886
|
+
}
|
|
887
|
+
await writeMCPConfigFile(destination.mcpConfigPath, { ...mcpConfig, mcpServers: servers });
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// --- Phase 3: verification (inside the transaction) ------------------
|
|
891
|
+
for (const write of fileWrites) {
|
|
892
|
+
const persisted = await readStructured(write.path);
|
|
893
|
+
if (persisted.kind !== "value" || persisted.content !== write.content) {
|
|
894
|
+
throw new Error(`post-import verification failed: ${write.path} was not persisted`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (mcpWrites.length > 0) {
|
|
898
|
+
const persistedConfig = await readMCPConfigFile(destination.mcpConfigPath).catch(() => null);
|
|
899
|
+
for (const write of mcpWrites) {
|
|
900
|
+
if (!persistedConfig?.mcpServers?.[write.name]) {
|
|
901
|
+
throw new Error(`post-import verification failed: MCP "${write.name}" was not persisted`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
} catch (error) {
|
|
906
|
+
// --- Phase 4: rollback — restore exactly what this transaction wrote --
|
|
907
|
+
const reason = (error as Error).message;
|
|
908
|
+
const rollbackErrors: string[] = [];
|
|
909
|
+
for (const snapshot of snapshots.reverse()) {
|
|
910
|
+
try {
|
|
911
|
+
if (snapshot.previous === null) {
|
|
912
|
+
await fs.rm(snapshot.path, { force: true });
|
|
913
|
+
} else {
|
|
914
|
+
const staged = `${snapshot.path}.gjc-rollback-${process.pid}.tmp`;
|
|
915
|
+
await fs.writeFile(staged, snapshot.previous, { encoding: "utf-8", mode: 0o600 });
|
|
916
|
+
await fs.rename(staged, snapshot.path);
|
|
917
|
+
}
|
|
918
|
+
} catch (rollbackError) {
|
|
919
|
+
rollbackErrors.push(`${snapshot.path}: ${(rollbackError as Error).message}`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
for (const dir of new Set(fileWrites.map(write => path.dirname(write.path)))) {
|
|
923
|
+
await sweepStagingTemps(dir).catch(() => {});
|
|
924
|
+
}
|
|
925
|
+
return failAll(
|
|
926
|
+
`import failed and was rolled back: ${reason}` +
|
|
927
|
+
(rollbackErrors.length > 0 ? `; ROLLBACK ERRORS (manual repair needed): ${rollbackErrors.join("; ")}` : ""),
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
for (const { entry } of writable) {
|
|
932
|
+
const base = {
|
|
933
|
+
surface: entry.surface,
|
|
934
|
+
sourceName: entry.sourceName,
|
|
935
|
+
destinationName: entry.destinationName,
|
|
936
|
+
};
|
|
937
|
+
if (entry.status === "overwrite") {
|
|
938
|
+
results.push({ ...base, outcome: "overwritten" });
|
|
939
|
+
} else if (entry.destinationName !== entry.sourceName) {
|
|
940
|
+
results.push({ ...base, outcome: "renamed" });
|
|
941
|
+
} else {
|
|
942
|
+
results.push({ ...base, outcome: "imported" });
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return { entries: results, ok: true };
|
|
946
|
+
}
|