apex-code 0.0.1-alpha.9 → 0.0.4
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 +150 -2
- package/README.md +9 -8
- package/dist/cli/args.d.ts +25 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +43 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/mcp-command.d.ts +9 -0
- package/dist/cli/mcp-command.d.ts.map +1 -0
- package/dist/cli/mcp-command.js +53 -0
- package/dist/cli/mcp-command.js.map +1 -0
- package/dist/cli/project-trust.d.ts.map +1 -1
- package/dist/cli/project-trust.js +3 -1
- package/dist/cli/project-trust.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +2 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +94 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -5
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +157 -8
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +565 -22
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +7 -0
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/checkpoints/git-checkpoints.d.ts +39 -0
- package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/git-checkpoints.js +251 -0
- package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.js +36 -0
- package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +4 -3
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +5 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +19 -5
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/delegation/runtime.d.ts +6 -5
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +4 -3
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +5 -2
- package/dist/core/exec.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +8 -2
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +5 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +45 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/source-runtime.d.ts +14 -0
- package/dist/core/extensions/source-runtime.d.ts.map +1 -0
- package/dist/core/extensions/source-runtime.js +56 -0
- package/dist/core/extensions/source-runtime.js.map +1 -0
- package/dist/core/extensions/types.d.ts +22 -2
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/formatter-lifecycle.d.ts +69 -0
- package/dist/core/formatter-lifecycle.d.ts.map +1 -0
- package/dist/core/formatter-lifecycle.js +360 -0
- package/dist/core/formatter-lifecycle.js.map +1 -0
- package/dist/core/hooks/command-handler.d.ts +19 -0
- package/dist/core/hooks/command-handler.d.ts.map +1 -0
- package/dist/core/hooks/command-handler.js +112 -0
- package/dist/core/hooks/command-handler.js.map +1 -0
- package/dist/core/hooks/http-handler.d.ts +10 -0
- package/dist/core/hooks/http-handler.d.ts.map +1 -0
- package/dist/core/hooks/http-handler.js +36 -0
- package/dist/core/hooks/http-handler.js.map +1 -0
- package/dist/core/hooks/loader.d.ts +29 -0
- package/dist/core/hooks/loader.d.ts.map +1 -0
- package/dist/core/hooks/loader.js +139 -0
- package/dist/core/hooks/loader.js.map +1 -0
- package/dist/core/hooks/runtime.d.ts +13 -0
- package/dist/core/hooks/runtime.d.ts.map +1 -0
- package/dist/core/hooks/runtime.js +63 -0
- package/dist/core/hooks/runtime.js.map +1 -0
- package/dist/core/hooks/types.d.ts +83 -0
- package/dist/core/hooks/types.d.ts.map +1 -0
- package/dist/core/hooks/types.js +24 -0
- package/dist/core/hooks/types.js.map +1 -0
- package/dist/core/mcp/config.d.ts +16 -0
- package/dist/core/mcp/config.d.ts.map +1 -0
- package/dist/core/mcp/config.js +166 -0
- package/dist/core/mcp/config.js.map +1 -0
- package/dist/core/mcp/connector.d.ts +35 -0
- package/dist/core/mcp/connector.d.ts.map +1 -0
- package/dist/core/mcp/connector.js +105 -0
- package/dist/core/mcp/connector.js.map +1 -0
- package/dist/core/mcp/contract.d.ts +19 -0
- package/dist/core/mcp/contract.d.ts.map +1 -0
- package/dist/core/mcp/contract.js +89 -0
- package/dist/core/mcp/contract.js.map +1 -0
- package/dist/core/mcp/mcp-tool.d.ts +32 -0
- package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
- package/dist/core/mcp/mcp-tool.js +171 -0
- package/dist/core/mcp/mcp-tool.js.map +1 -0
- package/dist/core/mcp/metadata-cache.d.ts +29 -0
- package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
- package/dist/core/mcp/metadata-cache.js +99 -0
- package/dist/core/mcp/metadata-cache.js.map +1 -0
- package/dist/core/mcp/oauth/authorize.d.ts +21 -0
- package/dist/core/mcp/oauth/authorize.d.ts.map +1 -0
- package/dist/core/mcp/oauth/authorize.js +32 -0
- package/dist/core/mcp/oauth/authorize.js.map +1 -0
- package/dist/core/mcp/oauth/discover.d.ts +27 -0
- package/dist/core/mcp/oauth/discover.d.ts.map +1 -0
- package/dist/core/mcp/oauth/discover.js +116 -0
- package/dist/core/mcp/oauth/discover.js.map +1 -0
- package/dist/core/mcp/oauth/flow.d.ts +31 -0
- package/dist/core/mcp/oauth/flow.d.ts.map +1 -0
- package/dist/core/mcp/oauth/flow.js +175 -0
- package/dist/core/mcp/oauth/flow.js.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts +51 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.js +129 -0
- package/dist/core/mcp/oauth/mcp-token.js.map +1 -0
- package/dist/core/mcp/oauth/pkce.d.ts +12 -0
- package/dist/core/mcp/oauth/pkce.d.ts.map +1 -0
- package/dist/core/mcp/oauth/pkce.js +11 -0
- package/dist/core/mcp/oauth/pkce.js.map +1 -0
- package/dist/core/mcp/oauth/register.d.ts +16 -0
- package/dist/core/mcp/oauth/register.d.ts.map +1 -0
- package/dist/core/mcp/oauth/register.js +42 -0
- package/dist/core/mcp/oauth/register.js.map +1 -0
- package/dist/core/mcp/oauth/token-client.d.ts +33 -0
- package/dist/core/mcp/oauth/token-client.d.ts.map +1 -0
- package/dist/core/mcp/oauth/token-client.js +65 -0
- package/dist/core/mcp/oauth/token-client.js.map +1 -0
- package/dist/core/mcp/runtime.d.ts +20 -0
- package/dist/core/mcp/runtime.d.ts.map +1 -0
- package/dist/core/mcp/runtime.js +37 -0
- package/dist/core/mcp/runtime.js.map +1 -0
- package/dist/core/mcp/schema.d.ts +22 -0
- package/dist/core/mcp/schema.d.ts.map +1 -0
- package/dist/core/mcp/schema.js +20 -0
- package/dist/core/mcp/schema.js.map +1 -0
- package/dist/core/mcp/server-manager.d.ts +43 -0
- package/dist/core/mcp/server-manager.d.ts.map +1 -0
- package/dist/core/mcp/server-manager.js +142 -0
- package/dist/core/mcp/server-manager.js.map +1 -0
- package/dist/core/mcp/types.d.ts +100 -0
- package/dist/core/mcp/types.d.ts.map +1 -0
- package/dist/core/mcp/types.js +8 -0
- package/dist/core/mcp/types.js.map +1 -0
- package/dist/core/permissions/gate.d.ts.map +1 -1
- package/dist/core/permissions/gate.js +45 -4
- package/dist/core/permissions/gate.js.map +1 -1
- package/dist/core/permissions/operations.d.ts +20 -0
- package/dist/core/permissions/operations.d.ts.map +1 -0
- package/dist/core/permissions/operations.js +9 -0
- package/dist/core/permissions/operations.js.map +1 -0
- package/dist/core/permissions/policy-command.d.ts +42 -0
- package/dist/core/permissions/policy-command.d.ts.map +1 -0
- package/dist/core/permissions/policy-command.js +44 -0
- package/dist/core/permissions/policy-command.js.map +1 -0
- package/dist/core/permissions/responder.d.ts +49 -2
- package/dist/core/permissions/responder.d.ts.map +1 -1
- package/dist/core/permissions/responder.js +20 -8
- package/dist/core/permissions/responder.js.map +1 -1
- package/dist/core/permissions/rules.d.ts.map +1 -1
- package/dist/core/permissions/rules.js +11 -1
- package/dist/core/permissions/rules.js.map +1 -1
- package/dist/core/permissions/store.d.ts +18 -0
- package/dist/core/permissions/store.d.ts.map +1 -1
- package/dist/core/permissions/store.js +89 -8
- package/dist/core/permissions/store.js.map +1 -1
- package/dist/core/policy-executor.d.ts +47 -0
- package/dist/core/policy-executor.d.ts.map +1 -0
- package/dist/core/policy-executor.js +228 -0
- package/dist/core/policy-executor.js.map +1 -0
- package/dist/core/policy-loader.d.ts +72 -0
- package/dist/core/policy-loader.d.ts.map +1 -0
- package/dist/core/policy-loader.js +195 -0
- package/dist/core/policy-loader.js.map +1 -0
- package/dist/core/project-trust.d.ts +1 -1
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/sandbox/bwrap-arguments.d.ts +41 -0
- package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
- package/dist/core/sandbox/bwrap-arguments.js +136 -0
- package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
- package/dist/core/sandbox/child-entry.d.ts.map +1 -1
- package/dist/core/sandbox/child-entry.js +3 -1
- package/dist/core/sandbox/child-entry.js.map +1 -1
- package/dist/core/sandbox/cli-launch.d.ts +84 -4
- package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
- package/dist/core/sandbox/cli-launch.js +115 -16
- package/dist/core/sandbox/cli-launch.js.map +1 -1
- package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
- package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/cli-supervisor.js +50 -2
- package/dist/core/sandbox/cli-supervisor.js.map +1 -1
- package/dist/core/sandbox/full-access.d.ts +28 -0
- package/dist/core/sandbox/full-access.d.ts.map +1 -0
- package/dist/core/sandbox/full-access.js +63 -0
- package/dist/core/sandbox/full-access.js.map +1 -0
- package/dist/core/sandbox/git-identity.d.ts +57 -0
- package/dist/core/sandbox/git-identity.d.ts.map +1 -0
- package/dist/core/sandbox/git-identity.js +78 -0
- package/dist/core/sandbox/git-identity.js.map +1 -0
- package/dist/core/sandbox/host-approval.d.ts +42 -0
- package/dist/core/sandbox/host-approval.d.ts.map +1 -0
- package/dist/core/sandbox/host-approval.js +99 -0
- package/dist/core/sandbox/host-approval.js.map +1 -0
- package/dist/core/sandbox/linux-backend.d.ts +14 -0
- package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
- package/dist/core/sandbox/linux-backend.js +158 -80
- package/dist/core/sandbox/linux-backend.js.map +1 -1
- package/dist/core/sandbox/macos-backend.d.ts +5 -0
- package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
- package/dist/core/sandbox/macos-backend.js +164 -5
- package/dist/core/sandbox/macos-backend.js.map +1 -1
- package/dist/core/sandbox/network-proxy.d.ts +19 -0
- package/dist/core/sandbox/network-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/network-proxy.js +93 -19
- package/dist/core/sandbox/network-proxy.js.map +1 -1
- package/dist/core/sandbox/policy.d.ts +8 -0
- package/dist/core/sandbox/policy.d.ts.map +1 -1
- package/dist/core/sandbox/policy.js +29 -12
- package/dist/core/sandbox/policy.js.map +1 -1
- package/dist/core/sandbox/profiles.d.ts +25 -0
- package/dist/core/sandbox/profiles.d.ts.map +1 -0
- package/dist/core/sandbox/profiles.js +23 -0
- package/dist/core/sandbox/profiles.js.map +1 -0
- package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
- package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-client.js +86 -0
- package/dist/core/sandbox/rpc/command-client.js.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.js +120 -0
- package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
- package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/rpc/credential-proxy.js +2 -96
- package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -1
- package/dist/core/sandbox/rpc/framing.d.ts +35 -0
- package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/framing.js +114 -0
- package/dist/core/sandbox/rpc/framing.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts +47 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js +222 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +66 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js +209 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
- package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
- package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
- package/dist/core/sandbox/supervisor-temp.js +21 -0
- package/dist/core/sandbox/supervisor-temp.js.map +1 -0
- package/dist/core/sandbox/supervisor.d.ts +12 -1
- package/dist/core/sandbox/supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/supervisor.js +15 -1
- package/dist/core/sandbox/supervisor.js.map +1 -1
- package/dist/core/sandbox/terminal-handoff.d.ts +68 -0
- package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
- package/dist/core/sandbox/terminal-handoff.js +221 -0
- package/dist/core/sandbox/terminal-handoff.js.map +1 -0
- package/dist/core/sandbox/terminal-size.d.ts +13 -0
- package/dist/core/sandbox/terminal-size.d.ts.map +1 -1
- package/dist/core/sandbox/terminal-size.js +23 -2
- package/dist/core/sandbox/terminal-size.js.map +1 -1
- package/dist/core/sdk.d.ts +22 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +53 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +13 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +32 -5
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +163 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +107 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +2 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/background-shell.d.ts +39 -0
- package/dist/core/tools/background-shell.d.ts.map +1 -0
- package/dist/core/tools/background-shell.js +81 -0
- package/dist/core/tools/background-shell.js.map +1 -0
- package/dist/core/tools/bash-command-segments.d.ts.map +1 -1
- package/dist/core/tools/bash-command-segments.js +2 -0
- package/dist/core/tools/bash-command-segments.js.map +1 -1
- package/dist/core/tools/bash.d.ts +30 -15
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +228 -12
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/contract-snapshot.d.ts +58 -0
- package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
- package/dist/core/tools/contract-snapshot.js +51 -0
- package/dist/core/tools/contract-snapshot.js.map +1 -0
- package/dist/core/tools/contract.d.ts +53 -5
- package/dist/core/tools/contract.d.ts.map +1 -1
- package/dist/core/tools/contract.js +22 -2
- package/dist/core/tools/contract.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +6 -4
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +21 -11
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +48 -0
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +215 -3
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +8 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +53 -7
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +5 -2
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +5 -2
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +10 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +3 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +5 -2
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/lsp.d.ts +19 -1
- package/dist/core/tools/lsp.d.ts.map +1 -1
- package/dist/core/tools/lsp.js +163 -10
- package/dist/core/tools/lsp.js.map +1 -1
- package/dist/core/tools/path-permission.d.ts +3 -0
- package/dist/core/tools/path-permission.d.ts.map +1 -1
- package/dist/core/tools/path-permission.js +16 -4
- package/dist/core/tools/path-permission.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +27 -2
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +221 -4
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +20 -11
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/test.d.ts +54 -9
- package/dist/core/tools/test.d.ts.map +1 -1
- package/dist/core/tools/test.js +239 -19
- package/dist/core/tools/test.js.map +1 -1
- package/dist/core/tools/write.d.ts +7 -0
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +64 -9
- package/dist/core/tools/write.js.map +1 -1
- package/dist/core/verification-lifecycle.d.ts +77 -0
- package/dist/core/verification-lifecycle.d.ts.map +1 -0
- package/dist/core/verification-lifecycle.js +146 -0
- package/dist/core/verification-lifecycle.js.map +1 -0
- package/dist/core/workspace/artifacts.d.ts +45 -0
- package/dist/core/workspace/artifacts.d.ts.map +1 -0
- package/dist/core/workspace/artifacts.js +165 -0
- package/dist/core/workspace/artifacts.js.map +1 -0
- package/dist/core/workspace/comparison.d.ts +27 -0
- package/dist/core/workspace/comparison.d.ts.map +1 -0
- package/dist/core/workspace/comparison.js +94 -0
- package/dist/core/workspace/comparison.js.map +1 -0
- package/dist/core/workspace/git-observer.d.ts +48 -0
- package/dist/core/workspace/git-observer.d.ts.map +1 -0
- package/dist/core/workspace/git-observer.js +346 -0
- package/dist/core/workspace/git-observer.js.map +1 -0
- package/dist/core/workspace/projection.d.ts +3 -0
- package/dist/core/workspace/projection.d.ts.map +1 -0
- package/dist/core/workspace/projection.js +56 -0
- package/dist/core/workspace/projection.js.map +1 -0
- package/dist/core/workspace/state.d.ts +133 -0
- package/dist/core/workspace/state.d.ts.map +1 -0
- package/dist/core/workspace/state.js +96 -0
- package/dist/core/workspace/state.js.map +1 -0
- package/dist/extensions/llama/client.d.ts +6 -0
- package/dist/extensions/llama/client.d.ts.map +1 -1
- package/dist/extensions/llama/client.js +7 -0
- package/dist/extensions/llama/client.js.map +1 -1
- package/dist/extensions/llama/provider.d.ts +3 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +27 -6
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +3 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +41 -9
- package/dist/main.js.map +1 -1
- package/dist/modes/acp/server.d.ts +55 -0
- package/dist/modes/acp/server.d.ts.map +1 -0
- package/dist/modes/acp/server.js +244 -0
- package/dist/modes/acp/server.js.map +1 -0
- package/dist/modes/acp/translate.d.ts +37 -0
- package/dist/modes/acp/translate.d.ts.map +1 -0
- package/dist/modes/acp/translate.js +50 -0
- package/dist/modes/acp/translate.js.map +1 -0
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +182 -23
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +20 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +9 -4
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +20 -9
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +7 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +7 -3
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +19 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +70 -10
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +6 -6
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/permission-preview.d.ts +15 -0
- package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -0
- package/dist/modes/interactive/components/permission-preview.js +33 -0
- package/dist/modes/interactive/components/permission-preview.js.map +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +2 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +23 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +64 -10
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +34 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +231 -26
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +12 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +15 -5
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +27 -12
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +7 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +12 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +0 -6
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +6 -4
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +6 -4
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +4 -0
- package/docs/custom-provider.md +7 -7
- package/docs/environment-variables.md +1 -1
- package/docs/extensions.md +18 -0
- package/docs/keybindings.md +1 -1
- package/docs/quickstart.md +1 -1
- package/docs/rpc.md +23 -0
- package/docs/sdk.md +25 -0
- package/docs/settings.md +12 -4
- package/docs/terminal-setup.md +12 -0
- package/docs/usage.md +4 -3
- package/examples/extensions/built-in-tool-renderer.ts +8 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/git-checkpoint.ts +24 -36
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/minimal-mode.ts +2 -2
- package/examples/extensions/permission-gate.ts +1 -1
- package/examples/extensions/plan-mode/index.ts +1 -1
- package/examples/extensions/rpc-demo.ts +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +194 -46
- package/package.json +14 -12
- package/dist/server/create-harness.d.ts +0 -26
- package/dist/server/create-harness.d.ts.map +0 -1
- package/dist/server/create-harness.js +0 -107
- package/dist/server/create-harness.js.map +0 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook config parsing and runtime assembly. Parsing is strict and fails
|
|
3
|
+
* closed: an invalid `hooks` key throws `HookConfigError` with the offending
|
|
4
|
+
* path, and the session wiring lets that propagate -- a governance entry that
|
|
5
|
+
* silently loads as nothing is a policy hole, so a malformed key refuses to
|
|
6
|
+
* start the session instead (the permission-rule posture: a failed rule load
|
|
7
|
+
* blocks rather than falls open).
|
|
8
|
+
*/
|
|
9
|
+
import { type HookEventName, type HookHandler, type HookHandlerConfig, type HookRuntime, type HooksSettings } from "./types.ts";
|
|
10
|
+
export declare class HookConfigError extends Error {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* V1 matcher grammar: exact tool names joined by `|` or `,` with optional
|
|
15
|
+
* whitespace, mirroring permission-rule matching. Regex matchers are a
|
|
16
|
+
* recorded non-goal -- grammar creep is how config surfaces rot.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseMatcher(matcher: string): (toolName: string) => boolean;
|
|
19
|
+
/** Strictly validate raw settings JSON into `HooksSettings`, throwing `HookConfigError` on the first problem. */
|
|
20
|
+
export declare function parseHookSettings(raw: unknown): HooksSettings;
|
|
21
|
+
export type HookHandlerFactory = (config: HookHandlerConfig, event: HookEventName) => HookHandler;
|
|
22
|
+
/**
|
|
23
|
+
* Assemble the runtime, or return undefined when nothing is configured -- an
|
|
24
|
+
* absent key constructs nothing, so an unconfigured session spawns no
|
|
25
|
+
* subprocess and changes no behavior. `createHandler` is the seam tests use to
|
|
26
|
+
* inject scripted handlers instead of real command/HTTP execution.
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadHookRuntime(settings: HooksSettings | undefined, createHandler?: HookHandlerFactory): HookRuntime | undefined;
|
|
29
|
+
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/loader.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,EAEN,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,aAAa,EAElB,MAAM,YAAY,CAAC;AAEpB,qBAAa,eAAgB,SAAQ,KAAK;IACzC,YAAY,OAAO,EAAE,MAAM,EAG1B;CACD;AAID;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAU3E;AA8DD,iHAAiH;AACjH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAgB7D;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,aAAa,KAAK,WAAW,CAAC;AAMlG;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,QAAQ,EAAE,aAAa,GAAG,SAAS,EACnC,aAAa,GAAE,kBAA8C,GAC3D,WAAW,GAAG,SAAS,CAgBzB","sourcesContent":["/**\n * Hook config parsing and runtime assembly. Parsing is strict and fails\n * closed: an invalid `hooks` key throws `HookConfigError` with the offending\n * path, and the session wiring lets that propagate -- a governance entry that\n * silently loads as nothing is a policy hole, so a malformed key refuses to\n * start the session instead (the permission-rule posture: a failed rule load\n * blocks rather than falls open).\n */\n\nimport { commandHookHandler } from \"./command-handler.ts\";\nimport { httpHookHandler } from \"./http-handler.ts\";\nimport { createHookRuntime, type RuntimeHookEntry } from \"./runtime.ts\";\nimport {\n\tHOOK_EVENT_NAMES,\n\ttype HookEventName,\n\ttype HookHandler,\n\ttype HookHandlerConfig,\n\ttype HookRuntime,\n\ttype HooksSettings,\n\tMAX_HOOK_TIMEOUT_MS,\n} from \"./types.ts\";\n\nexport class HookConfigError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"HookConfigError\";\n\t}\n}\n\nconst MATCHER_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;\n\n/**\n * V1 matcher grammar: exact tool names joined by `|` or `,` with optional\n * whitespace, mirroring permission-rule matching. Regex matchers are a\n * recorded non-goal -- grammar creep is how config surfaces rot.\n */\nexport function parseMatcher(matcher: string): (toolName: string) => boolean {\n\tconst names = matcher\n\t\t.split(/[|,]/)\n\t\t.map((name) => name.trim())\n\t\t.filter((name) => name.length > 0);\n\tif (names.length === 0 || !names.every((name) => MATCHER_NAME_PATTERN.test(name))) {\n\t\tthrow new HookConfigError(`invalid matcher \"${matcher}\" (expected exact tool names joined by \"|\" or \",\")`);\n\t}\n\tconst accepted = new Set(names);\n\treturn (toolName) => accepted.has(toolName);\n}\n\nfunction parseHandlerConfig(event: HookEventName, index: number, raw: unknown): HookHandlerConfig {\n\tconst path = `hooks.${event}[${index}]`;\n\tif (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n\t\tthrow new HookConfigError(`${path}: expected an object`);\n\t}\n\tconst record = raw as Record<string, unknown>;\n\tconst type = record.type;\n\tif (type !== \"command\" && type !== \"http\") {\n\t\tthrow new HookConfigError(`${path}: \"type\" must be \"command\" or \"http\"`);\n\t}\n\tconst allowed =\n\t\ttype === \"command\" ? [\"type\", \"matcher\", \"command\", \"timeoutMs\"] : [\"type\", \"matcher\", \"url\", \"timeoutMs\"];\n\tfor (const key of Object.keys(record)) {\n\t\tif (!allowed.includes(key)) {\n\t\t\tthrow new HookConfigError(`${path}: unknown field \"${key}\" (a mistyped key would silently drop policy)`);\n\t\t}\n\t}\n\tconst matcherField = record.matcher;\n\tif (matcherField !== undefined) {\n\t\tif (typeof matcherField !== \"string\") {\n\t\t\tthrow new HookConfigError(`${path}: \"matcher\" must be a string`);\n\t\t}\n\t\t// Validate the grammar here, at load, so a bad matcher is rejected with\n\t\t// the config path even before any runtime is assembled.\n\t\tparseMatcher(matcherField);\n\t}\n\tconst timeoutField = record.timeoutMs;\n\tif (\n\t\ttimeoutField !== undefined &&\n\t\t(typeof timeoutField !== \"number\" ||\n\t\t\t!Number.isFinite(timeoutField) ||\n\t\t\ttimeoutField <= 0 ||\n\t\t\ttimeoutField > MAX_HOOK_TIMEOUT_MS)\n\t) {\n\t\tthrow new HookConfigError(`${path}: \"timeoutMs\" must be a number between 1 and ${MAX_HOOK_TIMEOUT_MS}`);\n\t}\n\tif (type === \"command\") {\n\t\tconst command = record.command;\n\t\tif (typeof command !== \"string\" || command.trim().length === 0) {\n\t\t\tthrow new HookConfigError(`${path}: \"command\" must be a non-empty string`);\n\t\t}\n\t\treturn {\n\t\t\ttype: \"command\",\n\t\t\tcommand,\n\t\t\t...(matcherField !== undefined ? { matcher: matcherField } : {}),\n\t\t\t...(timeoutField !== undefined ? { timeoutMs: timeoutField } : {}),\n\t\t};\n\t}\n\tconst urlField = record.url;\n\tif (typeof urlField !== \"string\" || !URL.canParse(urlField) || !/^https?:$/.test(new URL(urlField).protocol)) {\n\t\tthrow new HookConfigError(`${path}: \"url\" must be an http(s) URL`);\n\t}\n\treturn {\n\t\ttype: \"http\",\n\t\turl: urlField,\n\t\t...(matcherField !== undefined ? { matcher: matcherField } : {}),\n\t\t...(timeoutField !== undefined ? { timeoutMs: timeoutField } : {}),\n\t};\n}\n\n/** Strictly validate raw settings JSON into `HooksSettings`, throwing `HookConfigError` on the first problem. */\nexport function parseHookSettings(raw: unknown): HooksSettings {\n\tif (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n\t\tthrow new HookConfigError(\"hooks: expected an object keyed by event name\");\n\t}\n\tconst parsed: HooksSettings = {};\n\tfor (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n\t\tif (!(HOOK_EVENT_NAMES as readonly string[]).includes(key)) {\n\t\t\tthrow new HookConfigError(`hooks.\"${key}\": unknown hook event (v1 supports ${HOOK_EVENT_NAMES.join(\", \")})`);\n\t\t}\n\t\tconst event = key as HookEventName;\n\t\tif (!Array.isArray(value)) {\n\t\t\tthrow new HookConfigError(`hooks.${event}: expected an array of handler configs`);\n\t\t}\n\t\tparsed[event] = value.map((entry, index) => parseHandlerConfig(event, index, entry));\n\t}\n\treturn parsed;\n}\n\nexport type HookHandlerFactory = (config: HookHandlerConfig, event: HookEventName) => HookHandler;\n\nfunction defaultHookHandlerFactory(config: HookHandlerConfig): HookHandler {\n\treturn config.type === \"command\" ? commandHookHandler(config) : httpHookHandler(config);\n}\n\n/**\n * Assemble the runtime, or return undefined when nothing is configured -- an\n * absent key constructs nothing, so an unconfigured session spawns no\n * subprocess and changes no behavior. `createHandler` is the seam tests use to\n * inject scripted handlers instead of real command/HTTP execution.\n */\nexport function loadHookRuntime(\n\tsettings: HooksSettings | undefined,\n\tcreateHandler: HookHandlerFactory = defaultHookHandlerFactory,\n): HookRuntime | undefined {\n\tif (!settings) return undefined;\n\tconst entries: RuntimeHookEntry[] = [];\n\tfor (const event of HOOK_EVENT_NAMES) {\n\t\tconst configs = settings[event];\n\t\tif (!configs || configs.length === 0) continue;\n\t\tfor (const config of configs) {\n\t\t\tconst handler = createHandler(config, event);\n\t\t\tentries.push({\n\t\t\t\tevent,\n\t\t\t\thandler: config.matcher ? { ...handler, matchesTool: parseMatcher(config.matcher) } : handler,\n\t\t\t});\n\t\t}\n\t}\n\tif (entries.length === 0) return undefined;\n\treturn createHookRuntime(entries);\n}\n"]}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook config parsing and runtime assembly. Parsing is strict and fails
|
|
3
|
+
* closed: an invalid `hooks` key throws `HookConfigError` with the offending
|
|
4
|
+
* path, and the session wiring lets that propagate -- a governance entry that
|
|
5
|
+
* silently loads as nothing is a policy hole, so a malformed key refuses to
|
|
6
|
+
* start the session instead (the permission-rule posture: a failed rule load
|
|
7
|
+
* blocks rather than falls open).
|
|
8
|
+
*/
|
|
9
|
+
import { commandHookHandler } from "./command-handler.js";
|
|
10
|
+
import { httpHookHandler } from "./http-handler.js";
|
|
11
|
+
import { createHookRuntime } from "./runtime.js";
|
|
12
|
+
import { HOOK_EVENT_NAMES, MAX_HOOK_TIMEOUT_MS, } from "./types.js";
|
|
13
|
+
export class HookConfigError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "HookConfigError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const MATCHER_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
20
|
+
/**
|
|
21
|
+
* V1 matcher grammar: exact tool names joined by `|` or `,` with optional
|
|
22
|
+
* whitespace, mirroring permission-rule matching. Regex matchers are a
|
|
23
|
+
* recorded non-goal -- grammar creep is how config surfaces rot.
|
|
24
|
+
*/
|
|
25
|
+
export function parseMatcher(matcher) {
|
|
26
|
+
const names = matcher
|
|
27
|
+
.split(/[|,]/)
|
|
28
|
+
.map((name) => name.trim())
|
|
29
|
+
.filter((name) => name.length > 0);
|
|
30
|
+
if (names.length === 0 || !names.every((name) => MATCHER_NAME_PATTERN.test(name))) {
|
|
31
|
+
throw new HookConfigError(`invalid matcher "${matcher}" (expected exact tool names joined by "|" or ",")`);
|
|
32
|
+
}
|
|
33
|
+
const accepted = new Set(names);
|
|
34
|
+
return (toolName) => accepted.has(toolName);
|
|
35
|
+
}
|
|
36
|
+
function parseHandlerConfig(event, index, raw) {
|
|
37
|
+
const path = `hooks.${event}[${index}]`;
|
|
38
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
39
|
+
throw new HookConfigError(`${path}: expected an object`);
|
|
40
|
+
}
|
|
41
|
+
const record = raw;
|
|
42
|
+
const type = record.type;
|
|
43
|
+
if (type !== "command" && type !== "http") {
|
|
44
|
+
throw new HookConfigError(`${path}: "type" must be "command" or "http"`);
|
|
45
|
+
}
|
|
46
|
+
const allowed = type === "command" ? ["type", "matcher", "command", "timeoutMs"] : ["type", "matcher", "url", "timeoutMs"];
|
|
47
|
+
for (const key of Object.keys(record)) {
|
|
48
|
+
if (!allowed.includes(key)) {
|
|
49
|
+
throw new HookConfigError(`${path}: unknown field "${key}" (a mistyped key would silently drop policy)`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const matcherField = record.matcher;
|
|
53
|
+
if (matcherField !== undefined) {
|
|
54
|
+
if (typeof matcherField !== "string") {
|
|
55
|
+
throw new HookConfigError(`${path}: "matcher" must be a string`);
|
|
56
|
+
}
|
|
57
|
+
// Validate the grammar here, at load, so a bad matcher is rejected with
|
|
58
|
+
// the config path even before any runtime is assembled.
|
|
59
|
+
parseMatcher(matcherField);
|
|
60
|
+
}
|
|
61
|
+
const timeoutField = record.timeoutMs;
|
|
62
|
+
if (timeoutField !== undefined &&
|
|
63
|
+
(typeof timeoutField !== "number" ||
|
|
64
|
+
!Number.isFinite(timeoutField) ||
|
|
65
|
+
timeoutField <= 0 ||
|
|
66
|
+
timeoutField > MAX_HOOK_TIMEOUT_MS)) {
|
|
67
|
+
throw new HookConfigError(`${path}: "timeoutMs" must be a number between 1 and ${MAX_HOOK_TIMEOUT_MS}`);
|
|
68
|
+
}
|
|
69
|
+
if (type === "command") {
|
|
70
|
+
const command = record.command;
|
|
71
|
+
if (typeof command !== "string" || command.trim().length === 0) {
|
|
72
|
+
throw new HookConfigError(`${path}: "command" must be a non-empty string`);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
type: "command",
|
|
76
|
+
command,
|
|
77
|
+
...(matcherField !== undefined ? { matcher: matcherField } : {}),
|
|
78
|
+
...(timeoutField !== undefined ? { timeoutMs: timeoutField } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const urlField = record.url;
|
|
82
|
+
if (typeof urlField !== "string" || !URL.canParse(urlField) || !/^https?:$/.test(new URL(urlField).protocol)) {
|
|
83
|
+
throw new HookConfigError(`${path}: "url" must be an http(s) URL`);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
type: "http",
|
|
87
|
+
url: urlField,
|
|
88
|
+
...(matcherField !== undefined ? { matcher: matcherField } : {}),
|
|
89
|
+
...(timeoutField !== undefined ? { timeoutMs: timeoutField } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Strictly validate raw settings JSON into `HooksSettings`, throwing `HookConfigError` on the first problem. */
|
|
93
|
+
export function parseHookSettings(raw) {
|
|
94
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
95
|
+
throw new HookConfigError("hooks: expected an object keyed by event name");
|
|
96
|
+
}
|
|
97
|
+
const parsed = {};
|
|
98
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
99
|
+
if (!HOOK_EVENT_NAMES.includes(key)) {
|
|
100
|
+
throw new HookConfigError(`hooks."${key}": unknown hook event (v1 supports ${HOOK_EVENT_NAMES.join(", ")})`);
|
|
101
|
+
}
|
|
102
|
+
const event = key;
|
|
103
|
+
if (!Array.isArray(value)) {
|
|
104
|
+
throw new HookConfigError(`hooks.${event}: expected an array of handler configs`);
|
|
105
|
+
}
|
|
106
|
+
parsed[event] = value.map((entry, index) => parseHandlerConfig(event, index, entry));
|
|
107
|
+
}
|
|
108
|
+
return parsed;
|
|
109
|
+
}
|
|
110
|
+
function defaultHookHandlerFactory(config) {
|
|
111
|
+
return config.type === "command" ? commandHookHandler(config) : httpHookHandler(config);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Assemble the runtime, or return undefined when nothing is configured -- an
|
|
115
|
+
* absent key constructs nothing, so an unconfigured session spawns no
|
|
116
|
+
* subprocess and changes no behavior. `createHandler` is the seam tests use to
|
|
117
|
+
* inject scripted handlers instead of real command/HTTP execution.
|
|
118
|
+
*/
|
|
119
|
+
export function loadHookRuntime(settings, createHandler = defaultHookHandlerFactory) {
|
|
120
|
+
if (!settings)
|
|
121
|
+
return undefined;
|
|
122
|
+
const entries = [];
|
|
123
|
+
for (const event of HOOK_EVENT_NAMES) {
|
|
124
|
+
const configs = settings[event];
|
|
125
|
+
if (!configs || configs.length === 0)
|
|
126
|
+
continue;
|
|
127
|
+
for (const config of configs) {
|
|
128
|
+
const handler = createHandler(config, event);
|
|
129
|
+
entries.push({
|
|
130
|
+
event,
|
|
131
|
+
handler: config.matcher ? { ...handler, matchesTool: parseMatcher(config.matcher) } : handler,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (entries.length === 0)
|
|
136
|
+
return undefined;
|
|
137
|
+
return createHookRuntime(entries);
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/hooks/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook config parsing and runtime assembly. Parsing is strict and fails\n * closed: an invalid `hooks` key throws `HookConfigError` with the offending\n * path, and the session wiring lets that propagate -- a governance entry that\n * silently loads as nothing is a policy hole, so a malformed key refuses to\n * start the session instead (the permission-rule posture: a failed rule load\n * blocks rather than falls open).\n */\n\nimport { commandHookHandler } from \"./command-handler.ts\";\nimport { httpHookHandler } from \"./http-handler.ts\";\nimport { createHookRuntime, type RuntimeHookEntry } from \"./runtime.ts\";\nimport {\n\tHOOK_EVENT_NAMES,\n\ttype HookEventName,\n\ttype HookHandler,\n\ttype HookHandlerConfig,\n\ttype HookRuntime,\n\ttype HooksSettings,\n\tMAX_HOOK_TIMEOUT_MS,\n} from \"./types.ts\";\n\nexport class HookConfigError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"HookConfigError\";\n\t}\n}\n\nconst MATCHER_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;\n\n/**\n * V1 matcher grammar: exact tool names joined by `|` or `,` with optional\n * whitespace, mirroring permission-rule matching. Regex matchers are a\n * recorded non-goal -- grammar creep is how config surfaces rot.\n */\nexport function parseMatcher(matcher: string): (toolName: string) => boolean {\n\tconst names = matcher\n\t\t.split(/[|,]/)\n\t\t.map((name) => name.trim())\n\t\t.filter((name) => name.length > 0);\n\tif (names.length === 0 || !names.every((name) => MATCHER_NAME_PATTERN.test(name))) {\n\t\tthrow new HookConfigError(`invalid matcher \"${matcher}\" (expected exact tool names joined by \"|\" or \",\")`);\n\t}\n\tconst accepted = new Set(names);\n\treturn (toolName) => accepted.has(toolName);\n}\n\nfunction parseHandlerConfig(event: HookEventName, index: number, raw: unknown): HookHandlerConfig {\n\tconst path = `hooks.${event}[${index}]`;\n\tif (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n\t\tthrow new HookConfigError(`${path}: expected an object`);\n\t}\n\tconst record = raw as Record<string, unknown>;\n\tconst type = record.type;\n\tif (type !== \"command\" && type !== \"http\") {\n\t\tthrow new HookConfigError(`${path}: \"type\" must be \"command\" or \"http\"`);\n\t}\n\tconst allowed =\n\t\ttype === \"command\" ? [\"type\", \"matcher\", \"command\", \"timeoutMs\"] : [\"type\", \"matcher\", \"url\", \"timeoutMs\"];\n\tfor (const key of Object.keys(record)) {\n\t\tif (!allowed.includes(key)) {\n\t\t\tthrow new HookConfigError(`${path}: unknown field \"${key}\" (a mistyped key would silently drop policy)`);\n\t\t}\n\t}\n\tconst matcherField = record.matcher;\n\tif (matcherField !== undefined) {\n\t\tif (typeof matcherField !== \"string\") {\n\t\t\tthrow new HookConfigError(`${path}: \"matcher\" must be a string`);\n\t\t}\n\t\t// Validate the grammar here, at load, so a bad matcher is rejected with\n\t\t// the config path even before any runtime is assembled.\n\t\tparseMatcher(matcherField);\n\t}\n\tconst timeoutField = record.timeoutMs;\n\tif (\n\t\ttimeoutField !== undefined &&\n\t\t(typeof timeoutField !== \"number\" ||\n\t\t\t!Number.isFinite(timeoutField) ||\n\t\t\ttimeoutField <= 0 ||\n\t\t\ttimeoutField > MAX_HOOK_TIMEOUT_MS)\n\t) {\n\t\tthrow new HookConfigError(`${path}: \"timeoutMs\" must be a number between 1 and ${MAX_HOOK_TIMEOUT_MS}`);\n\t}\n\tif (type === \"command\") {\n\t\tconst command = record.command;\n\t\tif (typeof command !== \"string\" || command.trim().length === 0) {\n\t\t\tthrow new HookConfigError(`${path}: \"command\" must be a non-empty string`);\n\t\t}\n\t\treturn {\n\t\t\ttype: \"command\",\n\t\t\tcommand,\n\t\t\t...(matcherField !== undefined ? { matcher: matcherField } : {}),\n\t\t\t...(timeoutField !== undefined ? { timeoutMs: timeoutField } : {}),\n\t\t};\n\t}\n\tconst urlField = record.url;\n\tif (typeof urlField !== \"string\" || !URL.canParse(urlField) || !/^https?:$/.test(new URL(urlField).protocol)) {\n\t\tthrow new HookConfigError(`${path}: \"url\" must be an http(s) URL`);\n\t}\n\treturn {\n\t\ttype: \"http\",\n\t\turl: urlField,\n\t\t...(matcherField !== undefined ? { matcher: matcherField } : {}),\n\t\t...(timeoutField !== undefined ? { timeoutMs: timeoutField } : {}),\n\t};\n}\n\n/** Strictly validate raw settings JSON into `HooksSettings`, throwing `HookConfigError` on the first problem. */\nexport function parseHookSettings(raw: unknown): HooksSettings {\n\tif (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n\t\tthrow new HookConfigError(\"hooks: expected an object keyed by event name\");\n\t}\n\tconst parsed: HooksSettings = {};\n\tfor (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n\t\tif (!(HOOK_EVENT_NAMES as readonly string[]).includes(key)) {\n\t\t\tthrow new HookConfigError(`hooks.\"${key}\": unknown hook event (v1 supports ${HOOK_EVENT_NAMES.join(\", \")})`);\n\t\t}\n\t\tconst event = key as HookEventName;\n\t\tif (!Array.isArray(value)) {\n\t\t\tthrow new HookConfigError(`hooks.${event}: expected an array of handler configs`);\n\t\t}\n\t\tparsed[event] = value.map((entry, index) => parseHandlerConfig(event, index, entry));\n\t}\n\treturn parsed;\n}\n\nexport type HookHandlerFactory = (config: HookHandlerConfig, event: HookEventName) => HookHandler;\n\nfunction defaultHookHandlerFactory(config: HookHandlerConfig): HookHandler {\n\treturn config.type === \"command\" ? commandHookHandler(config) : httpHookHandler(config);\n}\n\n/**\n * Assemble the runtime, or return undefined when nothing is configured -- an\n * absent key constructs nothing, so an unconfigured session spawns no\n * subprocess and changes no behavior. `createHandler` is the seam tests use to\n * inject scripted handlers instead of real command/HTTP execution.\n */\nexport function loadHookRuntime(\n\tsettings: HooksSettings | undefined,\n\tcreateHandler: HookHandlerFactory = defaultHookHandlerFactory,\n): HookRuntime | undefined {\n\tif (!settings) return undefined;\n\tconst entries: RuntimeHookEntry[] = [];\n\tfor (const event of HOOK_EVENT_NAMES) {\n\t\tconst configs = settings[event];\n\t\tif (!configs || configs.length === 0) continue;\n\t\tfor (const config of configs) {\n\t\t\tconst handler = createHandler(config, event);\n\t\t\tentries.push({\n\t\t\t\tevent,\n\t\t\t\thandler: config.matcher ? { ...handler, matchesTool: parseMatcher(config.matcher) } : handler,\n\t\t\t});\n\t\t}\n\t}\n\tif (entries.length === 0) return undefined;\n\treturn createHookRuntime(entries);\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hook runtime: an immutable handler list keyed by event, plus the two
|
|
3
|
+
* emission shapes the session seam consumes. Decision logic lives here so the
|
|
4
|
+
* restriction-only rule is enforced in exactly one place -- `decideToolCall`
|
|
5
|
+
* returns a block or nothing, never an allow that could bypass the gate.
|
|
6
|
+
*/
|
|
7
|
+
import type { HookEventName, HookHandler, HookRuntime } from "./types.ts";
|
|
8
|
+
export interface RuntimeHookEntry {
|
|
9
|
+
event: HookEventName;
|
|
10
|
+
handler: HookHandler;
|
|
11
|
+
}
|
|
12
|
+
export declare function createHookRuntime(entries: RuntimeHookEntry[]): HookRuntime;
|
|
13
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/runtime.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE1E,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,WAAW,CAAC;CACrB;AAQD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,WAAW,CA6C1E","sourcesContent":["/**\n * The hook runtime: an immutable handler list keyed by event, plus the two\n * emission shapes the session seam consumes. Decision logic lives here so the\n * restriction-only rule is enforced in exactly one place -- `decideToolCall`\n * returns a block or nothing, never an allow that could bypass the gate.\n */\n\nimport type { HookEventName, HookHandler, HookRuntime } from \"./types.ts\";\n\nexport interface RuntimeHookEntry {\n\tevent: HookEventName;\n\thandler: HookHandler;\n}\n\nfunction accepted(handler: HookHandler, toolName: string | undefined): boolean {\n\tif (!handler.matchesTool) return true;\n\tif (toolName === undefined) return true;\n\treturn handler.matchesTool(toolName);\n}\n\nexport function createHookRuntime(entries: RuntimeHookEntry[]): HookRuntime {\n\tconst byEvent = new Map<HookEventName, HookHandler[]>();\n\tfor (const entry of entries) {\n\t\tconst handlers = byEvent.get(entry.event);\n\t\tif (handlers) {\n\t\t\thandlers.push(entry.handler);\n\t\t} else {\n\t\t\tbyEvent.set(entry.event, [entry.handler]);\n\t\t}\n\t}\n\n\treturn {\n\t\thasHandlers(event) {\n\t\t\treturn (byEvent.get(event)?.length ?? 0) > 0;\n\t\t},\n\t\thandlersFor(event) {\n\t\t\treturn byEvent.get(event) ?? [];\n\t\t},\n\t\tasync decideToolCall(toolName, payload) {\n\t\t\tfor (const handler of byEvent.get(\"tool_call\") ?? []) {\n\t\t\t\tif (!accepted(handler, toolName)) continue;\n\t\t\t\tconst outcome = await handler.execute(payload);\n\t\t\t\tif (!outcome.ok) {\n\t\t\t\t\t// Fail closed: a hook that cannot run must not widen authority.\n\t\t\t\t\t// This is the spawn-failure and timeout path too.\n\t\t\t\t\treturn { block: true, reason: outcome.warning };\n\t\t\t\t}\n\t\t\t\t// Restriction-only (spec): `block` short-circuits; `allow` and\n\t\t\t\t// `ask` fall through so the permission gate stays the last check.\n\t\t\t\tif (outcome.decision?.decision === \"block\") {\n\t\t\t\t\treturn { block: true, reason: outcome.decision.reason ?? \"blocked by a declarative hook\" };\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn undefined;\n\t\t},\n\t\tasync emitObserve(event, payload) {\n\t\t\tfor (const handler of byEvent.get(event) ?? []) {\n\t\t\t\tif (!accepted(handler, payload.toolName)) continue;\n\t\t\t\t// Observe-only: awaited for ordering, but a failure is swallowed\n\t\t\t\t// because no decision is read from these events.\n\t\t\t\tconst outcome = await handler.execute(payload);\n\t\t\t\tif (!outcome.ok) continue;\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hook runtime: an immutable handler list keyed by event, plus the two
|
|
3
|
+
* emission shapes the session seam consumes. Decision logic lives here so the
|
|
4
|
+
* restriction-only rule is enforced in exactly one place -- `decideToolCall`
|
|
5
|
+
* returns a block or nothing, never an allow that could bypass the gate.
|
|
6
|
+
*/
|
|
7
|
+
function accepted(handler, toolName) {
|
|
8
|
+
if (!handler.matchesTool)
|
|
9
|
+
return true;
|
|
10
|
+
if (toolName === undefined)
|
|
11
|
+
return true;
|
|
12
|
+
return handler.matchesTool(toolName);
|
|
13
|
+
}
|
|
14
|
+
export function createHookRuntime(entries) {
|
|
15
|
+
const byEvent = new Map();
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const handlers = byEvent.get(entry.event);
|
|
18
|
+
if (handlers) {
|
|
19
|
+
handlers.push(entry.handler);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
byEvent.set(entry.event, [entry.handler]);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
hasHandlers(event) {
|
|
27
|
+
return (byEvent.get(event)?.length ?? 0) > 0;
|
|
28
|
+
},
|
|
29
|
+
handlersFor(event) {
|
|
30
|
+
return byEvent.get(event) ?? [];
|
|
31
|
+
},
|
|
32
|
+
async decideToolCall(toolName, payload) {
|
|
33
|
+
for (const handler of byEvent.get("tool_call") ?? []) {
|
|
34
|
+
if (!accepted(handler, toolName))
|
|
35
|
+
continue;
|
|
36
|
+
const outcome = await handler.execute(payload);
|
|
37
|
+
if (!outcome.ok) {
|
|
38
|
+
// Fail closed: a hook that cannot run must not widen authority.
|
|
39
|
+
// This is the spawn-failure and timeout path too.
|
|
40
|
+
return { block: true, reason: outcome.warning };
|
|
41
|
+
}
|
|
42
|
+
// Restriction-only (spec): `block` short-circuits; `allow` and
|
|
43
|
+
// `ask` fall through so the permission gate stays the last check.
|
|
44
|
+
if (outcome.decision?.decision === "block") {
|
|
45
|
+
return { block: true, reason: outcome.decision.reason ?? "blocked by a declarative hook" };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
},
|
|
50
|
+
async emitObserve(event, payload) {
|
|
51
|
+
for (const handler of byEvent.get(event) ?? []) {
|
|
52
|
+
if (!accepted(handler, payload.toolName))
|
|
53
|
+
continue;
|
|
54
|
+
// Observe-only: awaited for ordering, but a failure is swallowed
|
|
55
|
+
// because no decision is read from these events.
|
|
56
|
+
const outcome = await handler.execute(payload);
|
|
57
|
+
if (!outcome.ok)
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/core/hooks/runtime.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,SAAS,QAAQ,CAAC,OAAoB,EAAE,QAA4B,EAAW;IAC9E,IAAI,CAAC,OAAO,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IACtC,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;AAAA,CACrC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAA2B,EAAe;IAC3E,MAAM,OAAO,GAAG,IAAI,GAAG,EAAgC,CAAC;IACxD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,QAAQ,EAAE,CAAC;YACd,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;aAAM,CAAC;YACP,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAC3C,CAAC;IACF,CAAC;IAED,OAAO;QACN,WAAW,CAAC,KAAK,EAAE;YAClB,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAAA,CAC7C;QACD,WAAW,CAAC,KAAK,EAAE;YAClB,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAAA,CAChC;QACD,KAAK,CAAC,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE;YACvC,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC;gBACtD,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;oBAAE,SAAS;gBAC3C,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;gBAC/C,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;oBACjB,gEAAgE;oBAChE,kDAAkD;oBAClD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;gBACjD,CAAC;gBACD,+DAA+D;gBAC/D,kEAAkE;gBAClE,IAAI,OAAO,CAAC,QAAQ,EAAE,QAAQ,KAAK,OAAO,EAAE,CAAC;oBAC5C,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,MAAM,IAAI,+BAA+B,EAAE,CAAC;gBAC5F,CAAC;YACF,CAAC;YACD,OAAO,SAAS,CAAC;QAAA,CACjB;QACD,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE;YACjC,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;gBAChD,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC;oBAAE,SAAS;gBACnD,iEAAiE;gBACjE,iDAAiD;gBACjD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;gBAC/C,IAAI,CAAC,OAAO,CAAC,EAAE;oBAAE,SAAS;YAC3B,CAAC;QAAA,CACD;KACD,CAAC;AAAA,CACF","sourcesContent":["/**\n * The hook runtime: an immutable handler list keyed by event, plus the two\n * emission shapes the session seam consumes. Decision logic lives here so the\n * restriction-only rule is enforced in exactly one place -- `decideToolCall`\n * returns a block or nothing, never an allow that could bypass the gate.\n */\n\nimport type { HookEventName, HookHandler, HookRuntime } from \"./types.ts\";\n\nexport interface RuntimeHookEntry {\n\tevent: HookEventName;\n\thandler: HookHandler;\n}\n\nfunction accepted(handler: HookHandler, toolName: string | undefined): boolean {\n\tif (!handler.matchesTool) return true;\n\tif (toolName === undefined) return true;\n\treturn handler.matchesTool(toolName);\n}\n\nexport function createHookRuntime(entries: RuntimeHookEntry[]): HookRuntime {\n\tconst byEvent = new Map<HookEventName, HookHandler[]>();\n\tfor (const entry of entries) {\n\t\tconst handlers = byEvent.get(entry.event);\n\t\tif (handlers) {\n\t\t\thandlers.push(entry.handler);\n\t\t} else {\n\t\t\tbyEvent.set(entry.event, [entry.handler]);\n\t\t}\n\t}\n\n\treturn {\n\t\thasHandlers(event) {\n\t\t\treturn (byEvent.get(event)?.length ?? 0) > 0;\n\t\t},\n\t\thandlersFor(event) {\n\t\t\treturn byEvent.get(event) ?? [];\n\t\t},\n\t\tasync decideToolCall(toolName, payload) {\n\t\t\tfor (const handler of byEvent.get(\"tool_call\") ?? []) {\n\t\t\t\tif (!accepted(handler, toolName)) continue;\n\t\t\t\tconst outcome = await handler.execute(payload);\n\t\t\t\tif (!outcome.ok) {\n\t\t\t\t\t// Fail closed: a hook that cannot run must not widen authority.\n\t\t\t\t\t// This is the spawn-failure and timeout path too.\n\t\t\t\t\treturn { block: true, reason: outcome.warning };\n\t\t\t\t}\n\t\t\t\t// Restriction-only (spec): `block` short-circuits; `allow` and\n\t\t\t\t// `ask` fall through so the permission gate stays the last check.\n\t\t\t\tif (outcome.decision?.decision === \"block\") {\n\t\t\t\t\treturn { block: true, reason: outcome.decision.reason ?? \"blocked by a declarative hook\" };\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn undefined;\n\t\t},\n\t\tasync emitObserve(event, payload) {\n\t\t\tfor (const handler of byEvent.get(event) ?? []) {\n\t\t\t\tif (!accepted(handler, payload.toolName)) continue;\n\t\t\t\t// Observe-only: awaited for ordering, but a failure is swallowed\n\t\t\t\t// because no decision is read from these events.\n\t\t\t\tconst outcome = await handler.execute(payload);\n\t\t\t\tif (!outcome.ok) continue;\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative hooks (spec 2026-08-31-declarative-hooks): a settings-file path
|
|
3
|
+
* onto the extension event catalog, for operators who will never author a
|
|
4
|
+
* TypeScript extension. Version one maps five events onto command and HTTP
|
|
5
|
+
* handlers, with decisions read only from `tool_call`.
|
|
6
|
+
*
|
|
7
|
+
* The decision vocabulary is restriction-only: `block` short-circuits, `ask`
|
|
8
|
+
* defers to the permission gate, and `allow` is recorded but never bypasses
|
|
9
|
+
* the gate. A hook can narrow what runs, never widen it -- which is what makes
|
|
10
|
+
* trust-gated project-scope hooks safe where project-sandbox profiles are not
|
|
11
|
+
* (ADR 0016).
|
|
12
|
+
*/
|
|
13
|
+
export declare const HOOK_EVENT_NAMES: readonly ["tool_call", "tool_result", "session_start", "turn_end", "session_before_compact"];
|
|
14
|
+
export type HookEventName = (typeof HOOK_EVENT_NAMES)[number];
|
|
15
|
+
export interface HookCommandHandlerConfig {
|
|
16
|
+
type: "command";
|
|
17
|
+
/** Tool-name exact or `|`/`,`-separated list; absent matches every tool. Only consulted for events that carry a tool name. */
|
|
18
|
+
matcher?: string;
|
|
19
|
+
/** Shell command; the event payload arrives as JSON on stdin, the decision as JSON on stdout. */
|
|
20
|
+
command: string;
|
|
21
|
+
/** Default 10s (DEFAULT_HOOK_TIMEOUT_MS). A handler that outlives its timeout fails closed on `tool_call`. */
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface HookHttpHandlerConfig {
|
|
25
|
+
type: "http";
|
|
26
|
+
matcher?: string;
|
|
27
|
+
/** The event payload is POSTed here; the decision is read from the 200 response body. URLs come from settings only -- never environment interpolation. */
|
|
28
|
+
url: string;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
}
|
|
31
|
+
export type HookHandlerConfig = HookCommandHandlerConfig | HookHttpHandlerConfig;
|
|
32
|
+
export type HooksSettings = Partial<Record<HookEventName, HookHandlerConfig[]>>;
|
|
33
|
+
export interface HookDecision {
|
|
34
|
+
decision: "allow" | "block" | "ask";
|
|
35
|
+
reason?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface HookEventPayload {
|
|
38
|
+
type: HookEventName;
|
|
39
|
+
toolName?: string;
|
|
40
|
+
toolCallId?: string;
|
|
41
|
+
input?: unknown;
|
|
42
|
+
reason?: string;
|
|
43
|
+
}
|
|
44
|
+
export type HookOutcome = {
|
|
45
|
+
ok: true;
|
|
46
|
+
decision?: HookDecision;
|
|
47
|
+
warning?: string;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
warning: string;
|
|
51
|
+
};
|
|
52
|
+
export interface HookHandler {
|
|
53
|
+
/** Absent on a handler means it matches every tool. */
|
|
54
|
+
matchesTool?(toolName: string): boolean;
|
|
55
|
+
execute(payload: HookEventPayload): Promise<HookOutcome>;
|
|
56
|
+
}
|
|
57
|
+
/** What a `tool_call` hook hands back to the session: the same shape the beforeToolCall seam already consumes. */
|
|
58
|
+
export interface HookToolCallBlock {
|
|
59
|
+
block: true;
|
|
60
|
+
reason: string;
|
|
61
|
+
}
|
|
62
|
+
export interface HookRuntime {
|
|
63
|
+
hasHandlers(event: HookEventName): boolean;
|
|
64
|
+
/** Test and diagnostic accessor; the runtime never mutates the handlers it holds. */
|
|
65
|
+
handlersFor(event: HookEventName): readonly HookHandler[];
|
|
66
|
+
/**
|
|
67
|
+
* `tool_call` decisions, restriction-only: `block` returns a block for the
|
|
68
|
+
* seam to consume, every other outcome (allow, ask, no decision) returns
|
|
69
|
+
* undefined so the permission gate still evaluates the call. A handler that
|
|
70
|
+
* fails or times out fails closed.
|
|
71
|
+
*/
|
|
72
|
+
decideToolCall(toolName: string, payload: HookEventPayload): Promise<HookToolCallBlock | undefined>;
|
|
73
|
+
/**
|
|
74
|
+
* Observe-only emission for `tool_result` and lifecycle events: handlers are
|
|
75
|
+
* awaited in order, failures are swallowed, and no decision is ever read.
|
|
76
|
+
*/
|
|
77
|
+
emitObserve(event: HookEventName, payload: HookEventPayload): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
/** Default per-handler timeout. A hung formatter must not stall the loop indefinitely. */
|
|
80
|
+
export declare const DEFAULT_HOOK_TIMEOUT_MS = 10000;
|
|
81
|
+
/** Hard cap on configured timeouts. */
|
|
82
|
+
export declare const MAX_HOOK_TIMEOUT_MS = 60000;
|
|
83
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,gBAAgB,8FAMnB,CAAC;AACX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,SAAS,CAAC;IAChB,8HAA8H;IAC9H,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iGAAiG;IACjG,OAAO,EAAE,MAAM,CAAC;IAChB,8GAA8G;IAC9G,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0JAA0J;IAC1J,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,iBAAiB,GAAG,wBAAwB,GAAG,qBAAqB,CAAC;AAEjF,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC;AAEhF,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,YAAY,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnH,MAAM,WAAW,WAAW;IAC3B,uDAAuD;IACvD,WAAW,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACxC,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACzD;AAED,kHAAkH;AAClH,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,IAAI,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC3B,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAC3C,qFAAqF;IACrF,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,WAAW,EAAE,CAAC;IAC1D;;;;;OAKG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IACpG;;;OAGG;IACH,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E;AAED,0FAA0F;AAC1F,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAC9C,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,QAAS,CAAC","sourcesContent":["/**\n * Declarative hooks (spec 2026-08-31-declarative-hooks): a settings-file path\n * onto the extension event catalog, for operators who will never author a\n * TypeScript extension. Version one maps five events onto command and HTTP\n * handlers, with decisions read only from `tool_call`.\n *\n * The decision vocabulary is restriction-only: `block` short-circuits, `ask`\n * defers to the permission gate, and `allow` is recorded but never bypasses\n * the gate. A hook can narrow what runs, never widen it -- which is what makes\n * trust-gated project-scope hooks safe where project-sandbox profiles are not\n * (ADR 0016).\n */\n\nexport const HOOK_EVENT_NAMES = [\n\t\"tool_call\",\n\t\"tool_result\",\n\t\"session_start\",\n\t\"turn_end\",\n\t\"session_before_compact\",\n] as const;\nexport type HookEventName = (typeof HOOK_EVENT_NAMES)[number];\n\nexport interface HookCommandHandlerConfig {\n\ttype: \"command\";\n\t/** Tool-name exact or `|`/`,`-separated list; absent matches every tool. Only consulted for events that carry a tool name. */\n\tmatcher?: string;\n\t/** Shell command; the event payload arrives as JSON on stdin, the decision as JSON on stdout. */\n\tcommand: string;\n\t/** Default 10s (DEFAULT_HOOK_TIMEOUT_MS). A handler that outlives its timeout fails closed on `tool_call`. */\n\ttimeoutMs?: number;\n}\n\nexport interface HookHttpHandlerConfig {\n\ttype: \"http\";\n\tmatcher?: string;\n\t/** The event payload is POSTed here; the decision is read from the 200 response body. URLs come from settings only -- never environment interpolation. */\n\turl: string;\n\ttimeoutMs?: number;\n}\n\nexport type HookHandlerConfig = HookCommandHandlerConfig | HookHttpHandlerConfig;\n\nexport type HooksSettings = Partial<Record<HookEventName, HookHandlerConfig[]>>;\n\nexport interface HookDecision {\n\tdecision: \"allow\" | \"block\" | \"ask\";\n\treason?: string;\n}\n\nexport interface HookEventPayload {\n\ttype: HookEventName;\n\ttoolName?: string;\n\ttoolCallId?: string;\n\tinput?: unknown;\n\treason?: string;\n}\n\nexport type HookOutcome = { ok: true; decision?: HookDecision; warning?: string } | { ok: false; warning: string };\n\nexport interface HookHandler {\n\t/** Absent on a handler means it matches every tool. */\n\tmatchesTool?(toolName: string): boolean;\n\texecute(payload: HookEventPayload): Promise<HookOutcome>;\n}\n\n/** What a `tool_call` hook hands back to the session: the same shape the beforeToolCall seam already consumes. */\nexport interface HookToolCallBlock {\n\tblock: true;\n\treason: string;\n}\n\nexport interface HookRuntime {\n\thasHandlers(event: HookEventName): boolean;\n\t/** Test and diagnostic accessor; the runtime never mutates the handlers it holds. */\n\thandlersFor(event: HookEventName): readonly HookHandler[];\n\t/**\n\t * `tool_call` decisions, restriction-only: `block` returns a block for the\n\t * seam to consume, every other outcome (allow, ask, no decision) returns\n\t * undefined so the permission gate still evaluates the call. A handler that\n\t * fails or times out fails closed.\n\t */\n\tdecideToolCall(toolName: string, payload: HookEventPayload): Promise<HookToolCallBlock | undefined>;\n\t/**\n\t * Observe-only emission for `tool_result` and lifecycle events: handlers are\n\t * awaited in order, failures are swallowed, and no decision is ever read.\n\t */\n\temitObserve(event: HookEventName, payload: HookEventPayload): Promise<void>;\n}\n\n/** Default per-handler timeout. A hung formatter must not stall the loop indefinitely. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 10_000;\n/** Hard cap on configured timeouts. */\nexport const MAX_HOOK_TIMEOUT_MS = 60_000;\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative hooks (spec 2026-08-31-declarative-hooks): a settings-file path
|
|
3
|
+
* onto the extension event catalog, for operators who will never author a
|
|
4
|
+
* TypeScript extension. Version one maps five events onto command and HTTP
|
|
5
|
+
* handlers, with decisions read only from `tool_call`.
|
|
6
|
+
*
|
|
7
|
+
* The decision vocabulary is restriction-only: `block` short-circuits, `ask`
|
|
8
|
+
* defers to the permission gate, and `allow` is recorded but never bypasses
|
|
9
|
+
* the gate. A hook can narrow what runs, never widen it -- which is what makes
|
|
10
|
+
* trust-gated project-scope hooks safe where project-sandbox profiles are not
|
|
11
|
+
* (ADR 0016).
|
|
12
|
+
*/
|
|
13
|
+
export const HOOK_EVENT_NAMES = [
|
|
14
|
+
"tool_call",
|
|
15
|
+
"tool_result",
|
|
16
|
+
"session_start",
|
|
17
|
+
"turn_end",
|
|
18
|
+
"session_before_compact",
|
|
19
|
+
];
|
|
20
|
+
/** Default per-handler timeout. A hung formatter must not stall the loop indefinitely. */
|
|
21
|
+
export const DEFAULT_HOOK_TIMEOUT_MS = 10_000;
|
|
22
|
+
/** Hard cap on configured timeouts. */
|
|
23
|
+
export const MAX_HOOK_TIMEOUT_MS = 60_000;
|
|
24
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,WAAW;IACX,aAAa;IACb,eAAe;IACf,UAAU;IACV,wBAAwB;CACf,CAAC;AAsEX,0FAA0F;AAC1F,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAC9C,uCAAuC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC","sourcesContent":["/**\n * Declarative hooks (spec 2026-08-31-declarative-hooks): a settings-file path\n * onto the extension event catalog, for operators who will never author a\n * TypeScript extension. Version one maps five events onto command and HTTP\n * handlers, with decisions read only from `tool_call`.\n *\n * The decision vocabulary is restriction-only: `block` short-circuits, `ask`\n * defers to the permission gate, and `allow` is recorded but never bypasses\n * the gate. A hook can narrow what runs, never widen it -- which is what makes\n * trust-gated project-scope hooks safe where project-sandbox profiles are not\n * (ADR 0016).\n */\n\nexport const HOOK_EVENT_NAMES = [\n\t\"tool_call\",\n\t\"tool_result\",\n\t\"session_start\",\n\t\"turn_end\",\n\t\"session_before_compact\",\n] as const;\nexport type HookEventName = (typeof HOOK_EVENT_NAMES)[number];\n\nexport interface HookCommandHandlerConfig {\n\ttype: \"command\";\n\t/** Tool-name exact or `|`/`,`-separated list; absent matches every tool. Only consulted for events that carry a tool name. */\n\tmatcher?: string;\n\t/** Shell command; the event payload arrives as JSON on stdin, the decision as JSON on stdout. */\n\tcommand: string;\n\t/** Default 10s (DEFAULT_HOOK_TIMEOUT_MS). A handler that outlives its timeout fails closed on `tool_call`. */\n\ttimeoutMs?: number;\n}\n\nexport interface HookHttpHandlerConfig {\n\ttype: \"http\";\n\tmatcher?: string;\n\t/** The event payload is POSTed here; the decision is read from the 200 response body. URLs come from settings only -- never environment interpolation. */\n\turl: string;\n\ttimeoutMs?: number;\n}\n\nexport type HookHandlerConfig = HookCommandHandlerConfig | HookHttpHandlerConfig;\n\nexport type HooksSettings = Partial<Record<HookEventName, HookHandlerConfig[]>>;\n\nexport interface HookDecision {\n\tdecision: \"allow\" | \"block\" | \"ask\";\n\treason?: string;\n}\n\nexport interface HookEventPayload {\n\ttype: HookEventName;\n\ttoolName?: string;\n\ttoolCallId?: string;\n\tinput?: unknown;\n\treason?: string;\n}\n\nexport type HookOutcome = { ok: true; decision?: HookDecision; warning?: string } | { ok: false; warning: string };\n\nexport interface HookHandler {\n\t/** Absent on a handler means it matches every tool. */\n\tmatchesTool?(toolName: string): boolean;\n\texecute(payload: HookEventPayload): Promise<HookOutcome>;\n}\n\n/** What a `tool_call` hook hands back to the session: the same shape the beforeToolCall seam already consumes. */\nexport interface HookToolCallBlock {\n\tblock: true;\n\treason: string;\n}\n\nexport interface HookRuntime {\n\thasHandlers(event: HookEventName): boolean;\n\t/** Test and diagnostic accessor; the runtime never mutates the handlers it holds. */\n\thandlersFor(event: HookEventName): readonly HookHandler[];\n\t/**\n\t * `tool_call` decisions, restriction-only: `block` returns a block for the\n\t * seam to consume, every other outcome (allow, ask, no decision) returns\n\t * undefined so the permission gate still evaluates the call. A handler that\n\t * fails or times out fails closed.\n\t */\n\tdecideToolCall(toolName: string, payload: HookEventPayload): Promise<HookToolCallBlock | undefined>;\n\t/**\n\t * Observe-only emission for `tool_result` and lifecycle events: handlers are\n\t * awaited in order, failures are swallowed, and no decision is ever read.\n\t */\n\temitObserve(event: HookEventName, payload: HookEventPayload): Promise<void>;\n}\n\n/** Default per-handler timeout. A hung formatter must not stall the loop indefinitely. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 10_000;\n/** Hard cap on configured timeouts. */\nexport const MAX_HOOK_TIMEOUT_MS = 60_000;\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the ecosystem-standard `{ "mcpServers": { … } }` shape from a project
|
|
3
|
+
* `.mcp.json` and a global `mcp.json`, project winning per server name.
|
|
4
|
+
*
|
|
5
|
+
* Every failure degrades. A malformed file yields no servers and one diagnostic; a
|
|
6
|
+
* malformed entry drops itself and leaves its siblings intact. A config file is the
|
|
7
|
+
* first thing a user hand-edits, so a parse error must never take the session down.
|
|
8
|
+
*/
|
|
9
|
+
import { type McpConfig } from "./types.ts";
|
|
10
|
+
export declare const PROJECT_CONFIG_FILENAME = ".mcp.json";
|
|
11
|
+
export declare function globalMcpConfigPath(): string;
|
|
12
|
+
export declare function loadMcpConfig(options?: {
|
|
13
|
+
projectPath?: string;
|
|
14
|
+
globalPath?: string;
|
|
15
|
+
}): McpConfig;
|
|
16
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,EAGN,KAAK,SAAS,EAKd,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,uBAAuB,cAAc,CAAC;AAEnD,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAgJD,wBAAgB,aAAa,CAAC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,SAAS,CAYpG","sourcesContent":["/**\n * Reads the ecosystem-standard `{ \"mcpServers\": { … } }` shape from a project\n * `.mcp.json` and a global `mcp.json`, project winning per server name.\n *\n * Every failure degrades. A malformed file yields no servers and one diagnostic; a\n * malformed entry drops itself and leaves its siblings intact. A config file is the\n * first thing a user hand-edits, so a parse error must never take the session down.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { CONFIG_DIR_NAME } from \"../../config.ts\";\nimport { ALL_CAPABILITIES, type Capability } from \"../tools/contract.ts\";\nimport {\n\tDEFAULT_IDLE_TIMEOUT_MINUTES,\n\tDEFAULT_LIFECYCLE,\n\ttype McpConfig,\n\ttype McpConfigDiagnostic,\n\ttype McpLifecycle,\n\ttype McpServerConfig,\n\ttype McpTransport,\n} from \"./types.ts\";\n\nconst LIFECYCLES: readonly McpLifecycle[] = [\"lazy\", \"eager\", \"keep-alive\", \"lazy-keep-alive\"];\n\nexport const PROJECT_CONFIG_FILENAME = \".mcp.json\";\n\nexport function globalMcpConfigPath(): string {\n\treturn join(homedir(), CONFIG_DIR_NAME, \"mcp.json\");\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction stringRecord(value: unknown): Record<string, string> {\n\tif (!isRecord(value)) return {};\n\tconst out: Record<string, string> = {};\n\tfor (const [key, item] of Object.entries(value)) {\n\t\tif (typeof item === \"string\") out[key] = item;\n\t}\n\treturn out;\n}\n\nfunction stringArray(value: unknown): string[] {\n\treturn Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\nfunction parseTransport(entry: Record<string, unknown>): McpTransport | string {\n\tconst command = typeof entry.command === \"string\" ? entry.command : undefined;\n\tconst url = typeof entry.url === \"string\" ? entry.url : undefined;\n\n\tif (command && url) return \"declares both command and url; exactly one is required\";\n\tif (command) {\n\t\treturn {\n\t\t\tkind: \"stdio\",\n\t\t\tcommand,\n\t\t\targs: stringArray(entry.args),\n\t\t\tenv: stringRecord(entry.env),\n\t\t\tcwd: typeof entry.cwd === \"string\" ? entry.cwd : undefined,\n\t\t};\n\t}\n\tif (url) {\n\t\treturn {\n\t\t\tkind: \"http\",\n\t\t\turl,\n\t\t\theaders: stringRecord(entry.headers),\n\t\t\tbearerTokenEnv: typeof entry.bearerTokenEnv === \"string\" ? entry.bearerTokenEnv : undefined,\n\t\t};\n\t}\n\treturn \"declares neither command nor url\";\n}\n\n/**\n * An absent `capabilities` is the common case, not an edge case: a config pasted from\n * another host carries no Apex-specific field. Those servers get the full set, which\n * keeps them classified (never `UNCLASSIFIED`) without narrowing anything the user\n * did not ask to narrow.\n */\nfunction parseCapabilities(entry: Record<string, unknown>, report: (message: string) => void): ReadonlySet<Capability> {\n\tif (!(\"capabilities\" in entry)) return ALL_CAPABILITIES;\n\tif (!Array.isArray(entry.capabilities)) {\n\t\treport(\"capabilities is not an array; falling back to the full capability set\");\n\t\treturn ALL_CAPABILITIES;\n\t}\n\n\tconst resolved = new Set<Capability>();\n\tconst unknown: string[] = [];\n\tfor (const candidate of entry.capabilities) {\n\t\tif (typeof candidate === \"string\" && ALL_CAPABILITIES.has(candidate as Capability)) {\n\t\t\tresolved.add(candidate as Capability);\n\t\t} else {\n\t\t\tunknown.push(String(candidate));\n\t\t}\n\t}\n\tif (unknown.length > 0) report(`unknown capabilities dropped: ${unknown.join(\", \")}`);\n\treturn resolved;\n}\n\nfunction parseLifecycle(entry: Record<string, unknown>, report: (message: string) => void): McpLifecycle {\n\tconst value = entry.lifecycle;\n\tif (value === undefined) return DEFAULT_LIFECYCLE;\n\tif (typeof value === \"string\" && LIFECYCLES.includes(value as McpLifecycle)) return value as McpLifecycle;\n\treport(`unknown lifecycle ${JSON.stringify(value)}; using ${DEFAULT_LIFECYCLE}`);\n\treturn DEFAULT_LIFECYCLE;\n}\n\nfunction parseIdleTimeout(entry: Record<string, unknown>): number {\n\tconst value = entry.idleTimeout;\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : DEFAULT_IDLE_TIMEOUT_MINUTES;\n}\n\nfunction parseAuth(\n\tentry: Record<string, unknown>,\n\ttransport: McpTransport,\n\treport: (message: string) => void,\n): \"oauth\" | undefined {\n\tconst value = entry.auth;\n\tif (value === undefined) return undefined;\n\tif (transport.kind !== \"http\") {\n\t\treport(\"auth is only supported on HTTP servers; ignoring\");\n\t\treturn undefined;\n\t}\n\tif (value === \"oauth\") return \"oauth\";\n\treport(`unknown auth ${JSON.stringify(value)}; only \"oauth\" is supported; ignoring`);\n\treturn undefined;\n}\n\nfunction readServers(path: string, diagnostics: McpConfigDiagnostic[]): Map<string, McpServerConfig> {\n\tconst servers = new Map<string, McpServerConfig>();\n\tif (!existsSync(path)) return servers;\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(path, \"utf-8\"));\n\t} catch (error) {\n\t\tdiagnostics.push({\n\t\t\tpath,\n\t\t\tserver: undefined,\n\t\t\tmessage: `could not be parsed as JSON: ${error instanceof Error ? error.message : String(error)}`,\n\t\t});\n\t\treturn servers;\n\t}\n\n\tif (!isRecord(parsed) || !isRecord(parsed.mcpServers)) {\n\t\tdiagnostics.push({ path, server: undefined, message: \"has no mcpServers object\" });\n\t\treturn servers;\n\t}\n\n\tfor (const [name, raw] of Object.entries(parsed.mcpServers)) {\n\t\tif (!isRecord(raw)) {\n\t\t\tdiagnostics.push({ path, server: name, message: \"entry is not an object\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst transport = parseTransport(raw);\n\t\tif (typeof transport === \"string\") {\n\t\t\tdiagnostics.push({ path, server: name, message: transport });\n\t\t\tcontinue;\n\t\t}\n\t\tconst report = (message: string) => diagnostics.push({ path, server: name, message });\n\t\tservers.set(name, {\n\t\t\tname,\n\t\t\ttransport,\n\t\t\tauth: parseAuth(raw, transport, report),\n\t\t\tcapabilities: parseCapabilities(raw, report),\n\t\t\tlifecycle: parseLifecycle(raw, report),\n\t\t\tidleTimeoutMinutes: parseIdleTimeout(raw),\n\t\t});\n\t}\n\n\treturn servers;\n}\n\nexport function loadMcpConfig(options: { projectPath?: string; globalPath?: string } = {}): McpConfig {\n\tconst diagnostics: McpConfigDiagnostic[] = [];\n\tconst globalPath = options.globalPath ?? globalMcpConfigPath();\n\n\tconst servers = readServers(globalPath, diagnostics);\n\tif (options.projectPath) {\n\t\tfor (const [name, config] of readServers(options.projectPath, diagnostics)) {\n\t\t\tservers.set(name, config);\n\t\t}\n\t}\n\n\treturn { servers, diagnostics };\n}\n"]}
|