feat(agent): add Claude Code bridge integration (#60)

Add local daemon that routes inference through user's own Anthropic
API key with filesystem and terminal access. Includes WebSocket
transport, MCP tool adapter, and API key auth.

Key components:
- compass-bridge package: local daemon with tool registry
- WebSocket transport for agent communication
- MCP API key management with HMAC auth and scoped permissions
- Usage tracking (tool calls, duration, success/failure)
- Settings UI for Claude Code configuration
- Migration 0019: mcp_api_keys and mcp_usage tables
- Test suite for auth and transport layers

Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
This commit is contained in:
Nicholai 2026-02-09 00:29:00 -07:00 committed by GitHub
parent a7494397f2
commit dc0cd40b13
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 10625 additions and 16 deletions

View File

@ -149,6 +149,7 @@ each module contributes schema tables, server actions, components, and optionall
- **mobile**: capacitor webview wrapper. the web app must never break because of native code -- all capacitor imports are dynamic, gated behind `isNative()`. see [docs/modules/mobile.md](docs/modules/mobile.md) and [docs/architecture/native-mobile.md](docs/architecture/native-mobile.md). - **mobile**: capacitor webview wrapper. the web app must never break because of native code -- all capacitor imports are dynamic, gated behind `isNative()`. see [docs/modules/mobile.md](docs/modules/mobile.md) and [docs/architecture/native-mobile.md](docs/architecture/native-mobile.md).
- **themes**: per-user oklch color system, 10 presets, AI-generated custom themes. see [docs/development/theming.md](docs/development/theming.md). - **themes**: per-user oklch color system, 10 presets, AI-generated custom themes. see [docs/development/theming.md](docs/development/theming.md).
- **plugins/skills**: github-hosted SKILL.md files inject into agent system prompt. full plugins provide tools, components, actions. see [docs/development/plugins.md](docs/development/plugins.md). - **plugins/skills**: github-hosted SKILL.md files inject into agent system prompt. full plugins provide tools, components, actions. see [docs/development/plugins.md](docs/development/plugins.md).
- **claude code bridge**: local daemon that routes inference through your own Anthropic API key. WebSocket connection gives the agent filesystem + terminal access alongside Compass tools. API key auth with scoped permissions. see [docs/modules/claude-code.md](docs/modules/claude-code.md).
project structure project structure

153
bun.lock
View File

@ -110,6 +110,7 @@
"media-chrome": "^4.17.2", "media-chrome": "^4.17.2",
"tailwindcss": "^4", "tailwindcss": "^4",
"typescript": "^5", "typescript": "^5",
"vitest": "^4.0.18",
"wrangler": "^4.59.3", "wrangler": "^4.59.3",
}, },
}, },
@ -693,6 +694,56 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="], "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="],
@ -877,6 +928,8 @@
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/content-disposition": ["@types/content-disposition@0.5.9", "", {}, "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ=="], "@types/content-disposition": ["@types/content-disposition@0.5.9", "", {}, "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ=="],
@ -913,6 +966,8 @@
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
@ -1027,6 +1082,20 @@
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
"@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
"@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
"@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
"@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
"@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
"@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
"@workos-inc/authkit-nextjs": ["@workos-inc/authkit-nextjs@2.13.0", "", { "dependencies": { "@workos-inc/node": "^7.72.0", "iron-session": "^8.0.1", "jose": "^5.2.3", "path-to-regexp": "^6.2.2" }, "peerDependencies": { "next": "^13.5.9 || ^14.2.26 || ^15.2.3 || ^16", "react": "^18.0 || ^19.0.0", "react-dom": "^18.0 || ^19.0.0" } }, "sha512-ppxzhfakPumHPPggYSROaAlgxfS7viFMPmWPG76Tp6Rh9G7YqkBSp7xtvMtM6gXOFFMvvEJRcKEta6YHeercTQ=="], "@workos-inc/authkit-nextjs": ["@workos-inc/authkit-nextjs@2.13.0", "", { "dependencies": { "@workos-inc/node": "^7.72.0", "iron-session": "^8.0.1", "jose": "^5.2.3", "path-to-regexp": "^6.2.2" }, "peerDependencies": { "next": "^13.5.9 || ^14.2.26 || ^15.2.3 || ^16", "react": "^18.0 || ^19.0.0", "react-dom": "^18.0 || ^19.0.0" } }, "sha512-ppxzhfakPumHPPggYSROaAlgxfS7viFMPmWPG76Tp6Rh9G7YqkBSp7xtvMtM6gXOFFMvvEJRcKEta6YHeercTQ=="],
"@workos-inc/node": ["@workos-inc/node@8.1.0", "", { "dependencies": { "iron-webcrypto": "^2.0.0", "jose": "~6.1.0" } }, "sha512-Ep2QSP43y4ZdJIOuL4Hjaq5f0u8Z0qZe7QWzrrBV6cHc/kcicDBcB0AanMP6eB9x3x6FaHfevLbkbjPF4+TCYQ=="], "@workos-inc/node": ["@workos-inc/node@8.1.0", "", { "dependencies": { "iron-webcrypto": "^2.0.0", "jose": "~6.1.0" } }, "sha512-Ep2QSP43y4ZdJIOuL4Hjaq5f0u8Z0qZe7QWzrrBV6cHc/kcicDBcB0AanMP6eB9x3x6FaHfevLbkbjPF4+TCYQ=="],
@ -1081,6 +1150,8 @@
"asn1js": ["asn1js@3.0.7", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ=="], "asn1js": ["asn1js@3.0.7", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
@ -1141,6 +1212,8 @@
"ce-la-react": ["ce-la-react@0.3.2", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-QJ6k4lOD/btI08xG8jBPxRCGXvCnusGGkTsiXk0u3NqUu/W+BXRnFD4PYjwtqh8AWmGa5LDbGk0fLQsqr0nSMA=="], "ce-la-react": ["ce-la-react@0.3.2", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-QJ6k4lOD/btI08xG8jBPxRCGXvCnusGGkTsiXk0u3NqUu/W+BXRnFD4PYjwtqh8AWmGa5LDbGk0fLQsqr0nSMA=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
@ -1311,6 +1384,8 @@
"es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="], "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
@ -1361,6 +1436,8 @@
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
@ -1373,6 +1450,8 @@
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
@ -1869,6 +1948,8 @@
"obliterator": ["obliterator@1.6.1", "", {}, "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig=="], "obliterator": ["obliterator@1.6.1", "", {}, "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
@ -2017,6 +2098,8 @@
"rimraf": ["rimraf@6.1.2", "", { "dependencies": { "glob": "^13.0.0", "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g=="], "rimraf": ["rimraf@6.1.2", "", { "dependencies": { "glob": "^13.0.0", "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g=="],
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
@ -2065,6 +2148,8 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
@ -2085,8 +2170,12 @@
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"streamdown": ["streamdown@2.1.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.1.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-u9gWd0AmjKg1d+74P44XaPlGrMeC21oDOSIhjGNEYMAttDMzCzlJO6lpTyJ9JkSinQQF65YcK4eOd3q9iTvULw=="], "streamdown": ["streamdown@2.1.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.1.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-u9gWd0AmjKg1d+74P44XaPlGrMeC21oDOSIhjGNEYMAttDMzCzlJO6lpTyJ9JkSinQQF65YcK4eOd3q9iTvULw=="],
@ -2151,8 +2240,14 @@
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
@ -2251,6 +2346,10 @@
"victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
"vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
@ -2271,6 +2370,8 @@
"which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"workerd": ["workerd@1.20260116.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260116.0", "@cloudflare/workerd-darwin-arm64": "1.20260116.0", "@cloudflare/workerd-linux-64": "1.20260116.0", "@cloudflare/workerd-linux-arm64": "1.20260116.0", "@cloudflare/workerd-windows-64": "1.20260116.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-tVdBes3qkZKm9ntrgSDlvKzk4g2mcMp4bNM1+UgZMpTesb0x7e59vYYcKclbSNypmVkdLWpEc2TOpO0WF/rrZw=="], "workerd": ["workerd@1.20260116.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260116.0", "@cloudflare/workerd-darwin-arm64": "1.20260116.0", "@cloudflare/workerd-linux-64": "1.20260116.0", "@cloudflare/workerd-linux-arm64": "1.20260116.0", "@cloudflare/workerd-windows-64": "1.20260116.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-tVdBes3qkZKm9ntrgSDlvKzk4g2mcMp4bNM1+UgZMpTesb0x7e59vYYcKclbSNypmVkdLWpEc2TOpO0WF/rrZw=="],
@ -3047,6 +3148,8 @@
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"vite/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="],
"wrangler/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], "wrangler/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="],
"xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
@ -3573,6 +3676,56 @@
"rimraf/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="], "rimraf/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="],
"vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="],
"vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="],
"vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="],
"vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="],
"vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="],
"vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="],
"vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="],
"vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="],
"vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="],
"vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="],
"vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="],
"vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="],
"vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="],
"vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="],
"vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="],
"vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="],
"vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="],
"vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="],
"vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="],
"vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="],
"vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="],
"vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="],
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="],
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
"wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="],

View File

@ -31,6 +31,7 @@ The construction-specific modules that make up HPS Compass.
- [scheduling](modules/scheduling.md) -- Gantt charts, critical path analysis, dependency management, baselines, workday exceptions - [scheduling](modules/scheduling.md) -- Gantt charts, critical path analysis, dependency management, baselines, workday exceptions
- [financials](modules/financials.md) -- invoices, vendor bills, payments, credit memos, NetSuite sync tie-in - [financials](modules/financials.md) -- invoices, vendor bills, payments, credit memos, NetSuite sync tie-in
- [mobile](modules/mobile.md) -- Capacitor native app, offline photo queue, push notifications, biometric auth - [mobile](modules/mobile.md) -- Capacitor native app, offline photo queue, push notifications, biometric auth
- [claude code](modules/claude-code.md) -- local bridge daemon, own Anthropic API key, filesystem + terminal tools, WebSocket protocol
development development

272
docs/modules/claude-code.md Normal file
View File

@ -0,0 +1,272 @@
Claude Code Bridge
===
The Claude Code bridge lets you use your own Anthropic API key to power the Compass agent. Instead of routing through OpenRouter in the cloud, a local daemon runs on your machine, calls the Anthropic API directly, and relays responses back to the Compass UI through a WebSocket connection.
The daemon also gives the agent access to your local filesystem and terminal -- it can read and write files, search directories, and run shell commands. Combined with the Compass tools (query data, manage themes, save memories, etc.), this makes the agent genuinely useful for development work: it can look at your project files, run builds, and interact with Compass data in the same conversation.
The architecture is straightforward: the browser connects to a local WebSocket server (the daemon), which handles inference via the Anthropic API and routes tool calls to either Compass (via REST) or local handlers (filesystem, terminal).
```
Browser (Compass UI)
│ WebSocket (ws://localhost:18789)
compass-bridge daemon (Bun)
├── Anthropic API (inference)
├── Local tools (fs, terminal)
└── Compass REST API (data, themes, memories, skills)
```
how it works
---
The bridge has three phases: registration, connection, and inference.
**Registration.** When the daemon starts, it calls `POST /api/bridge/register` on your Compass instance with a Bearer token (the API key you generated in settings). Compass validates the key, looks up the user and their permissions, loads memories, dashboards, and installed skills, and returns the full context plus a list of available tools filtered by the key's scopes.
**Connection.** The browser's `ChatProvider` periodically probes `ws://localhost:18789` to detect whether the daemon is running. When it finds it, and the user has enabled the bridge in settings, the provider creates a `WebSocketChatTransport` that replaces the default HTTP transport. The transport authenticates by sending the API key over the socket and waiting for an `auth_ok` response.
**Inference.** When the user sends a message, the transport sends a `chat.send` message over WebSocket. The daemon acknowledges with a `chat.ack` (including a `runId`), then starts streaming the Anthropic API response. Text chunks are forwarded as `chunk` messages. Tool calls are executed -- either locally (filesystem, terminal) or remotely (Compass API) -- and their results are fed back into the model for the next turn. This loop continues until the model produces a final text response with no tool calls, then a `chat.done` message closes the stream.
The browser receives these chunks through the same `ReadableStream<UIMessageChunk>` interface that the default HTTP transport uses, so the chat UI doesn't know or care whether it's talking to OpenRouter or the local daemon.
setup
---
1. Open Settings in Compass and go to the "Claude Code" tab.
2. Generate an API key. Choose the scopes you want:
- **read** -- query data, recall memories, list themes/dashboards/skills
- **write** -- save memories, set themes, CRUD operations (includes read)
- **admin** -- install/uninstall skills (includes read + write)
Copy the key immediately. It's shown once and stored only as a SHA-256 hash.
3. Install the bridge daemon:
```bash
npm install -g compass-bridge
```
Or clone the repo and link it:
```bash
cd packages/compass-bridge
bun install
bun link
```
4. Run the interactive setup:
```bash
compass-bridge init
```
You'll be prompted for:
- Your Compass URL (e.g., `https://your-compass.example.com`)
- The API key you just generated (`ck_...`)
- Your Anthropic API key (`sk-ant-...`)
- Port (default 18789)
The config is saved to `~/.compass-bridge/config.json`.
5. Start the daemon:
```bash
compass-bridge start
```
6. Back in Compass, the "Claude Code" tab should show a green dot ("Bridge daemon detected"). Flip the switch to enable the bridge.
From this point, all chat messages route through the local daemon instead of OpenRouter.
wire protocol
---
The bridge uses a simple JSON-over-WebSocket protocol. Every message has a `type` field.
**Client to server (browser to daemon):**
| type | purpose |
|------|---------|
| `auth` | authenticate with Compass API key |
| `chat.send` | send a chat message (includes conversation context) |
| `chat.abort` | cancel an in-progress run by `runId` |
| `ping` | heartbeat |
**Server to client (daemon to browser):**
| type | purpose |
|------|---------|
| `auth_ok` | authentication succeeded (includes user info) |
| `auth_error` | authentication failed |
| `chat.ack` | message received, inference starting (includes `runId`) |
| `chunk` | streaming content (text deltas, tool inputs, tool results) |
| `chat.done` | inference complete |
| `chat.error` | inference failed |
| `pong` | heartbeat response |
The `chunk` message wraps `UIMessageChunk` objects from the AI SDK, so the browser's `ReadableStream` can consume them directly. Chunk subtypes include `text-delta` (streaming text), `tool-input-start` (tool call beginning), `tool-input-available` (tool call arguments), and `data-part-available` (tool result).
Types are defined in `src/lib/mcp/types.ts`.
available tools
---
Tools are split into two categories: Compass tools (executed remotely via the Compass REST API) and local tools (executed on the user's machine by the daemon).
**Compass tools** are gated by the API key's scopes:
| tool | scope | what it does |
|------|-------|-------------|
| `queryData` | read | query the database (customers, vendors, projects, invoices, etc.) |
| `recallMemory` | read | search the user's persistent memories |
| `listThemes` | read | list preset and custom visual themes |
| `listDashboards` | read | list saved custom dashboards |
| `listInstalledSkills` | read | list installed agent skills |
| `rememberContext` | write | save something to persistent memory |
| `setTheme` | write | switch the user's visual theme |
| `installSkill` | admin | install a skill from GitHub |
| `uninstallSkill` | admin | remove an installed skill |
| `toggleInstalledSkill` | admin | enable or disable a skill |
Scopes are hierarchical: `write` includes `read`, `admin` includes both.
**Local tools** are always available when the daemon is running:
| tool | what it does |
|------|-------------|
| `readFile` | read a file on the user's machine |
| `writeFile` | write content to a file |
| `listDirectory` | list files and directories |
| `searchFiles` | search for files by glob pattern |
| `runCommand` | execute a shell command (30s timeout, stdout capped at 50KB) |
tool adapter (server side)
---
When Compass receives a tool call via `POST /api/bridge/tools`, the tool adapter (`src/lib/mcp/tool-adapter.ts`) validates the request against the API key's scopes using a hierarchical permission model, then dispatches to the appropriate handler. Each handler is a function that takes `(userId, userRole, args)` and returns the result. Usage is logged to the `mcp_usage` table (fire-and-forget) for auditing.
API key system
---
API keys follow a straightforward design:
- Keys are prefixed with `ck_` and contain 20 random bytes (40 hex chars).
- Only the SHA-256 hash is stored in the database (`mcp_api_keys` table). The raw key is shown once at creation time and never again.
- Keys have scopes (JSON array: `["read", "write", "admin"]`), an optional expiry date, and an `isActive` flag for revocation.
- The `lastUsedAt` timestamp is updated on each validation (best-effort, non-blocking).
Server actions in `src/app/actions/mcp-keys.ts` handle CRUD: `createApiKey`, `listApiKeys`, `revokeApiKey`, `deleteApiKey`. All are authenticated and scoped to the current user.
schema
---
Two tables in `src/db/schema-mcp.ts`:
**`mcp_api_keys`** -- stores API keys for bridge authentication.
| column | type | notes |
|--------|------|-------|
| `id` | text (UUID) | primary key |
| `userId` | text | FK to users, cascade delete |
| `name` | text | human-readable label |
| `keyPrefix` | text | first 8 chars for identification |
| `keyHash` | text | SHA-256 of the full key |
| `scopes` | text | JSON array of scope strings |
| `lastUsedAt` | text | ISO 8601 timestamp |
| `createdAt` | text | ISO 8601 timestamp |
| `expiresAt` | text | optional expiry |
| `isActive` | integer (boolean) | soft revocation |
**`mcp_usage`** -- audit log for tool calls through the bridge.
| column | type | notes |
|--------|------|-------|
| `id` | text (UUID) | primary key |
| `apiKeyId` | text | FK to mcp_api_keys, cascade delete |
| `userId` | text | FK to users, cascade delete |
| `toolName` | text | which tool was called |
| `success` | integer (boolean) | whether the call succeeded |
| `errorMessage` | text | error detail if failed |
| `durationMs` | integer | execution time |
| `createdAt` | text | ISO 8601 timestamp |
security model
---
**API keys are hashed.** Raw keys are never stored. Compromise of the database doesn't leak usable keys.
**Scopes limit blast radius.** A key with `read` scope can't modify data. A key with `write` scope can't install skills. Admin actions also check the user's actual role in Compass.
**Local binding.** The daemon listens on `127.0.0.1` only. It's not reachable from the network.
**Auth on every request.** The WebSocket handshake requires a valid API key. The REST endpoints (`/api/bridge/register`, `/api/bridge/tools`) validate the Bearer token on every call.
**Expiry and revocation.** Keys can have an expiry date and can be revoked (soft-disabled) from the settings UI without deletion. Revoked keys fail validation immediately.
**Usage auditing.** Every tool call through the bridge is logged with the key ID, user ID, tool name, success/failure, and duration.
the daemon
---
The daemon is a standalone Bun process in `packages/compass-bridge/`. It has no dependency on the Compass web app at runtime -- it communicates purely via HTTP and WebSocket.
Key files:
```
packages/compass-bridge/src/
index.ts # CLI entry (init, start, status, help)
config.ts # config read/write (~/.compass-bridge/config.json)
auth.ts # registration + context refresh via Compass API
server.ts # Bun.serve WebSocket server
inference.ts # Anthropic API client + agentic tool loop
prompt.ts # system prompt builder (identity, tools, memories)
session.ts # in-memory conversation history (100 message cap)
tools/
registry.ts # tool routing (local vs compass)
compass.ts # remote tool execution via Compass REST
filesystem.ts # readFile, writeFile, listDirectory, searchFiles
terminal.ts # runCommand (30s timeout, output caps)
```
The inference loop (`inference.ts`) is a standard agentic pattern: call the Anthropic API with tools, stream the response, collect tool calls, execute them, feed results back, repeat until the model stops calling tools. It uses `claude-sonnet-4-5-20250929` with an 8192 token max.
Sessions are stored in memory (not persisted). The daemon holds the last 100 messages per conversation to keep context manageable.
UI integration
---
On the Compass side, the bridge integrates at two points:
**ChatProvider** (`src/components/agent/chat-provider.tsx`) manages bridge state through a `BridgeContext`. It polls for the daemon every 30 seconds when enabled, creates a `WebSocketChatTransport` when connected, and passes it to `useCompassChat()`. The hook uses the bridge transport when available, falling back to the default HTTP transport otherwise.
**Settings UI** (`src/components/settings/claude-code-tab.tsx`) provides the "Claude Code" tab where users generate API keys, see the daemon connection status, toggle the bridge on/off, and view setup instructions.
troubleshooting
---
**"Bridge daemon not running" in settings.** Make sure `compass-bridge start` is running in a terminal. Check that the port (default 18789) isn't blocked or in use. Run `compass-bridge status` to verify.
**"bridge auth timeout" in the chat.** The API key in the browser's localStorage doesn't match the one in the daemon's config. Re-run `compass-bridge init` with the correct key, or generate a new one in settings.
**Tool calls failing with "insufficient scope".** The API key doesn't have the required scope for that tool. Generate a new key with the needed scopes (read/write/admin).
**"Registration failed" on daemon start.** The Compass URL or API key is wrong. Check `~/.compass-bridge/config.json`. Make sure the Compass instance is reachable from your machine.
**Commands timing out.** The `runCommand` tool has a 30-second timeout. For long-running commands, break them into smaller steps or increase the timeout in the daemon source.
**No tool results appearing in chat.** The chunk messages might not be mapping correctly to the AI SDK's `UIMessageChunk` format. Check the browser console for WebSocket errors. The daemon logs tool execution to stdout.

View File

@ -45,6 +45,7 @@ Modules are the parts specific to HPS's construction business:
| Scheduling | Gantt charts, CPM, baseline tracking | `lib/schedule/`, `actions/schedule.ts`, `components/schedule/` | | Scheduling | Gantt charts, CPM, baseline tracking | `lib/schedule/`, `actions/schedule.ts`, `components/schedule/` |
| Financials | invoices, bills, payments, credit memos | `actions/invoices.ts`, `actions/vendor-bills.ts`, `components/financials/` | | Financials | invoices, bills, payments, credit memos | `actions/invoices.ts`, `actions/vendor-bills.ts`, `components/financials/` |
| Mobile | Capacitor native wrapper, offline photos, push | `lib/native/`, `lib/push/`, `hooks/use-native*.ts`, `components/native/` | | Mobile | Capacitor native wrapper, offline photos, push | `lib/native/`, `lib/push/`, `hooks/use-native*.ts`, `components/native/` |
| Claude Code | local bridge daemon, own API key, filesystem + terminal access | `lib/mcp/`, `lib/agent/ws-transport.ts`, `packages/compass-bridge/` |
Some tables blur the line. The `customers` and `vendors` tables in `schema.ts` are core entities used by multiple modules, but they have `netsuiteId` columns that only matter when the NetSuite module is active. The `projects` table has a `netsuiteJobId` column for the same reason. The `pushTokens` table lives in the core schema but is only meaningful to the mobile module. These are pragmatic compromises: splitting them into separate schemas would add complexity without real benefit at the current scale. Some tables blur the line. The `customers` and `vendors` tables in `schema.ts` are core entities used by multiple modules, but they have `netsuiteId` columns that only matter when the NetSuite module is active. The `projects` table has a `netsuiteJobId` column for the same reason. The `pushTokens` table lives in the core schema but is only meaningful to the mobile module. These are pragmatic compromises: splitting them into separate schemas would add complexity without real benefit at the current scale.

View File

@ -10,6 +10,7 @@ export default defineConfig({
"./src/db/schema-theme.ts", "./src/db/schema-theme.ts",
"./src/db/schema-google.ts", "./src/db/schema-google.ts",
"./src/db/schema-dashboards.ts", "./src/db/schema-dashboards.ts",
"./src/db/schema-mcp.ts",
], ],
out: "./drizzle", out: "./drizzle",
dialect: "sqlite", dialect: "sqlite",

View File

@ -0,0 +1,26 @@
CREATE TABLE `mcp_api_keys` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`name` text NOT NULL,
`key_prefix` text NOT NULL,
`key_hash` text NOT NULL,
`scopes` text NOT NULL,
`last_used_at` text,
`created_at` text NOT NULL,
`expires_at` text,
`is_active` integer DEFAULT true NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `mcp_usage` (
`id` text PRIMARY KEY NOT NULL,
`api_key_id` text NOT NULL,
`user_id` text NOT NULL,
`tool_name` text NOT NULL,
`success` integer NOT NULL,
`error_message` text,
`duration_ms` integer NOT NULL,
`created_at` text NOT NULL,
FOREIGN KEY (`api_key_id`) REFERENCES `mcp_api_keys`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);

File diff suppressed because it is too large Load Diff

View File

@ -134,6 +134,13 @@
"when": 1770471997491, "when": 1770471997491,
"tag": "0018_left_veda", "tag": "0018_left_veda",
"breakpoints": true "breakpoints": true
},
{
"idx": 19,
"version": "6",
"when": 1770522037142,
"tag": "0019_parched_thunderbird",
"breakpoints": true
} }
] ]
} }

View File

@ -125,6 +125,7 @@
"media-chrome": "^4.17.2", "media-chrome": "^4.17.2",
"tailwindcss": "^4", "tailwindcss": "^4",
"typescript": "^5", "typescript": "^5",
"vitest": "^4.0.18",
"wrangler": "^4.59.3" "wrangler": "^4.59.3"
} }
} }

View File

@ -0,0 +1,136 @@
compass-bridge
===
Local daemon that connects your Compass instance to the Anthropic API. Instead of routing through OpenRouter, you use your own API key for inference, and the daemon gives the agent access to your local filesystem and terminal alongside Compass data.
quick start
---
```bash
# install globally
npm install -g compass-bridge
# or from the repo
cd packages/compass-bridge && bun install && bun link
# interactive setup
compass-bridge init
# start the daemon
compass-bridge start
```
During `init`, you'll need:
- Your Compass instance URL
- A Compass API key (generate one in Settings > Claude Code)
- An Anthropic API key
commands
---
```
compass-bridge init Interactive setup (Compass URL, API keys, port)
compass-bridge start Start the WebSocket daemon
compass-bridge status Check config and daemon health
compass-bridge help Show usage
```
how it works
---
1. On startup, the daemon registers with Compass via `POST /api/bridge/register`. This validates the API key and returns the user context, available tools, memories, dashboards, and installed skills.
2. The daemon starts a WebSocket server on `127.0.0.1:18789` (configurable). Only local connections are accepted.
3. The Compass browser UI detects the daemon and routes chat messages through the WebSocket instead of the usual HTTP API.
4. When the user sends a message, the daemon calls the Anthropic API (`claude-sonnet-4-5-20250929`) with the full tool set:
- **Compass tools** (remote): queryData, recallMemory, rememberContext, listThemes, setTheme, listDashboards, listInstalledSkills, installSkill, uninstallSkill, toggleInstalledSkill
- **Local tools**: readFile, writeFile, listDirectory, searchFiles, runCommand
5. Tool calls are routed automatically: Compass tools go back to the Compass REST API, local tools execute directly on your machine.
6. Streaming text and tool results are relayed to the browser as WebSocket chunks.
configuration
---
Config lives at `~/.compass-bridge/config.json`:
```json
{
"compassUrl": "https://your-compass.example.com",
"apiKey": "ck_...",
"anthropicApiKey": "sk-ant-...",
"port": 18789,
"allowedOrigins": []
}
```
| field | description | default |
|-------|-------------|---------|
| `compassUrl` | your Compass instance URL | (required) |
| `apiKey` | Compass bridge API key | (required) |
| `anthropicApiKey` | your Anthropic API key | (required) |
| `port` | WebSocket server port | 18789 |
| `allowedOrigins` | CORS origins (unused currently) | [] |
local tools
---
The daemon provides five tools that run directly on your machine:
**readFile** -- Read file contents. Takes an absolute or relative path.
**writeFile** -- Write content to a file. Creates the file if it doesn't exist.
**listDirectory** -- List files and directories at a path. Returns name and type (file/directory) for each entry.
**searchFiles** -- Glob search. Takes a root directory, a pattern (e.g., `**/*.ts`), and an optional max results (default 50).
**runCommand** -- Execute a shell command via `sh -c`. 30-second timeout. Stdout is capped at 50KB, stderr at 10KB.
requirements
---
- [Bun](https://bun.sh) (runtime)
- A Compass instance with the bridge API enabled
- An Anthropic API key
project structure
---
```
src/
index.ts # CLI entry point
config.ts # config read/write
auth.ts # Compass registration + context refresh
server.ts # Bun.serve WebSocket server
inference.ts # Anthropic API streaming + tool loop
prompt.ts # system prompt builder
session.ts # in-memory conversation history
tools/
registry.ts # tool routing (local vs compass)
compass.ts # remote tool calls to Compass API
filesystem.ts # file read/write/list/search
terminal.ts # shell command execution
```
development
---
```bash
# run directly (no build needed with bun)
bun run src/index.ts start
# build a standalone binary
bun run build
```

View File

@ -0,0 +1,511 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "compass-bridge",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@mariozechner/pi-ai": "^0.52.8",
},
"devDependencies": {
"@types/bun": "latest",
},
},
},
"packages": {
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="],
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.985.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.7", "@aws-sdk/credential-provider-node": "^3.972.6", "@aws-sdk/eventstream-handler-node": "^3.972.5", "@aws-sdk/middleware-eventstream": "^3.972.3", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.7", "@aws-sdk/middleware-websocket": "^3.972.5", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/token-providers": "3.985.0", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.985.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.5", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.22.1", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-config-resolver": "^4.3.8", "@smithy/eventstream-serde-node": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.13", "@smithy/middleware-retry": "^4.4.30", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.9", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.29", "@smithy/util-defaults-mode-node": "^4.2.32", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-stream": "^4.5.11", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jkQ+G+b/6Z6gUsn8jNSjJsFVgxnA4HtyOjrpHfmp8nHWLRFTOIw3HfY2vAlDgg/uUJ7cezVG0/tmbwujFqX25A=="],
"@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.985.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.7", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.7", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.985.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.5", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.22.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.13", "@smithy/middleware-retry": "^4.4.30", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.9", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.29", "@smithy/util-defaults-mode-node": "^4.2.32", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-81J8iE8MuXhdbMfIz4sWFj64Pe41bFi/uqqmqOC5SlGv+kwoyLsyKS/rH2tW2t5buih4vTUxskRjxlqikTD4oQ=="],
"@aws-sdk/core": ["@aws-sdk/core@3.973.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.4", "@smithy/core": "^3.22.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wNZZQQNlJ+hzD49cKdo+PY6rsTDElO8yDImnrI69p2PLBa7QomeUKAJWYp9xnaR38nlHqWhMHZuYLCQ3oSX+xg=="],
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.5", "", { "dependencies": { "@aws-sdk/core": "^3.973.7", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-LxJ9PEO4gKPXzkufvIESUysykPIdrV7+Ocb9yAhbhJLE4TiAYqbCVUE+VuKP1leGR1bBfjWjYgSV5MxprlX3mQ=="],
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.7", "", { "dependencies": { "@aws-sdk/core": "^3.973.7", "@aws-sdk/types": "^3.973.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.9", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.11", "tslib": "^2.6.2" } }, "sha512-L2uOGtvp2x3bTcxFTpSM+GkwFIPd8pHfGWO1764icMbo7e5xJh0nfhx1UwkXLnwvocTNEf8A7jISZLYjUSNaTg=="],
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.5", "", { "dependencies": { "@aws-sdk/core": "^3.973.7", "@aws-sdk/credential-provider-env": "^3.972.5", "@aws-sdk/credential-provider-http": "^3.972.7", "@aws-sdk/credential-provider-login": "^3.972.5", "@aws-sdk/credential-provider-process": "^3.972.5", "@aws-sdk/credential-provider-sso": "^3.972.5", "@aws-sdk/credential-provider-web-identity": "^3.972.5", "@aws-sdk/nested-clients": "3.985.0", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-SdDTYE6jkARzOeL7+kudMIM4DaFnP5dZVeatzw849k4bSXDdErDS188bgeNzc/RA2WGrlEpsqHUKP6G7sVXhZg=="],
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.5", "", { "dependencies": { "@aws-sdk/core": "^3.973.7", "@aws-sdk/nested-clients": "3.985.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-uYq1ILyTSI6ZDCMY5+vUsRM0SOCVI7kaW4wBrehVVkhAxC6y+e9rvGtnoZqCOWL1gKjTMouvsf4Ilhc5NCg1Aw=="],
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.6", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.5", "@aws-sdk/credential-provider-http": "^3.972.7", "@aws-sdk/credential-provider-ini": "^3.972.5", "@aws-sdk/credential-provider-process": "^3.972.5", "@aws-sdk/credential-provider-sso": "^3.972.5", "@aws-sdk/credential-provider-web-identity": "^3.972.5", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DZ3CnAAtSVtVz+G+ogqecaErMLgzph4JH5nYbHoBMgBkwTUV+SUcjsjOJwdBJTHu3Dm6l5LBYekZoU2nDqQk2A=="],
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.5", "", { "dependencies": { "@aws-sdk/core": "^3.973.7", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-HDKF3mVbLnuqGg6dMnzBf1VUOywE12/N286msI9YaK9mEIzdsGCtLTvrDhe3Up0R9/hGFbB+9l21/TwF5L1C6g=="],
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.5", "", { "dependencies": { "@aws-sdk/client-sso": "3.985.0", "@aws-sdk/core": "^3.973.7", "@aws-sdk/token-providers": "3.985.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8urj3AoeNeQisjMmMBhFeiY2gxt6/7wQQbEGun0YV/OaOOiXrIudTIEYF8ZfD+NQI6X1FY5AkRsx6O/CaGiybA=="],
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.5", "", { "dependencies": { "@aws-sdk/core": "^3.973.7", "@aws-sdk/nested-clients": "3.985.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-OK3cULuJl6c+RcDZfPpaK5o3deTOnKZbxm7pzhFNGA3fI2hF9yDih17fGRazJzGGWaDVlR9ejZrpDef4DJCEsw=="],
"@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/eventstream-codec": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-xEmd3dnyn83K6t4AJxBJA63wpEoCD45ERFG0XMTViD2E/Ohls9TLxjOWPb1PAxR9/46cKy/TImez1GoqP6xVNQ=="],
"@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-pbvZ6Ye/Ks6BAZPa3RhsNjHrvxU9li25PMhSdDpbX0jzdpKpAkIR65gXSNKmA/REnSdEMWSD4vKUW+5eMFzB6w=="],
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA=="],
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA=="],
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q=="],
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.7", "", { "dependencies": { "@aws-sdk/core": "^3.973.7", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.985.0", "@smithy/core": "^3.22.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-HUD+geASjXSCyL/DHPQc/Ua7JhldTcIglVAoCV8kiVm99IaFSlAbTvEnyhZwdE6bdFyTL+uIaWLaCFSRsglZBQ=="],
"@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-format-url": "^3.972.3", "@smithy/eventstream-codec": "^4.2.8", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-BN4A9K71WRIlpQ3+IYGdBC2wVyobZ95g6ZomodmJ8Te772GWo0iDk2Mv6JIHdr842tOTgi1b3npLIFDUS4hl4g=="],
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.985.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.7", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.7", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.985.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.5", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.22.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.13", "@smithy/middleware-retry": "^4.4.30", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.9", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.29", "@smithy/util-defaults-mode-node": "^4.2.32", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-TsWwKzb/2WHafAY0CE7uXgLj0FmnkBTgfioG9HO+7z/zCPcl1+YU+i7dW4o0y+aFxFgxTMG+ExBQpqT/k2ao8g=="],
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow=="],
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.985.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.7", "@aws-sdk/nested-clients": "3.985.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-+hwpHZyEq8k+9JL2PkE60V93v2kNhUIv7STFt+EAez1UJsJOQDhc5LpzEX66pNjclI5OTwBROs/DhJjC/BtMjQ=="],
"@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="],
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.985.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA=="],
"@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-n7F2ycckcKFXa01vAsT/SJdjFHfKH9s96QHcs5gn8AaaigASICeME8WdUL9uBp8XV/OVwEt8+6gzn6KFUgQa8g=="],
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.4", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog=="],
"@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw=="],
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.972.5", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.7", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-GsUDF+rXyxDZkkJxUsDxnA67FG+kc5W1dnloCFLl6fWzceevsCYzJpASBzT+BPjwUgREE6FngfJYYYMQUY5fZQ=="],
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.4", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.4", "tslib": "^2.6.2" } }, "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q=="],
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="],
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
"@google/genai": ["@google/genai@1.40.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-fhIww8smT0QYRX78qWOiz/nIQhHMF5wXOrlXvj33HBrz3vKDBb+wibLcEmTA+L9dmPD4KmfNr7UF3LDQVTXNjA=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@mariozechner/pi-ai": ["@mariozechner/pi-ai@0.52.8", "", { "dependencies": { "@anthropic-ai/sdk": "^0.73.0", "@aws-sdk/client-bedrock-runtime": "^3.983.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "1.10.0", "@sinclair/typebox": "^0.34.41", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "chalk": "^5.6.2", "openai": "6.10.0", "partial-json": "^0.1.7", "proxy-agent": "^6.5.0", "undici": "^7.19.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-+aFCUbKJcskDJhr9wPcMBTy0x/xWio5v1dkxRYXUBPWp+Zt9DSdT5Kmd/IIQ+a0TOZDF4ajt4GY/oAw37X7XTw=="],
"@mistralai/mistralai": ["@mistralai/mistralai@1.10.0", "", { "dependencies": { "zod": "^3.20.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw=="],
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="],
"@smithy/core": ["@smithy/core@3.22.1", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.11", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.8", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw=="],
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.8", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw=="],
"@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ=="],
"@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.8", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A=="],
"@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.8", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA=="],
"@smithy/hash-node": ["@smithy/hash-node@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA=="],
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ=="],
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="],
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="],
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.13", "", { "dependencies": { "@smithy/core": "^3.22.1", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w=="],
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.30", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg=="],
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="],
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA=="],
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.8", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.9", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w=="],
"@smithy/property-provider": ["@smithy/property-provider@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w=="],
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ=="],
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw=="],
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA=="],
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0" } }, "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ=="],
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.3", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg=="],
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="],
"@smithy/smithy-client": ["@smithy/smithy-client@4.11.2", "", { "dependencies": { "@smithy/core": "^3.22.1", "@smithy/middleware-endpoint": "^4.4.13", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.11", "tslib": "^2.6.2" } }, "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A=="],
"@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="],
"@smithy/url-parser": ["@smithy/url-parser@4.2.8", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA=="],
"@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="],
"@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="],
"@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="],
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="],
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="],
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.29", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q=="],
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.32", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q=="],
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="],
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="],
"@smithy/util-middleware": ["@smithy/util-middleware@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A=="],
"@smithy/util-retry": ["@smithy/util-retry@4.2.8", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg=="],
"@smithy/util-stream": ["@smithy/util-stream@4.5.11", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.9", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA=="],
"@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="],
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
"@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="],
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
"@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"basic-ftp": ["basic-ftp@5.1.0", "", {}, "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"bowser": ["bowser@2.13.1", "", {}, "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw=="],
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fast-xml-parser": ["fast-xml-parser@5.3.4", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="],
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"openai": ["openai@6.10.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-ITxOGo7rO3XRMiKA5l7tQ43iNNu+iXGFAcf2t+aWVzzqRaS0i7m1K2BhxNdaveB+5eENhO0VY1FkiZzhBk4v3A=="],
"pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
"pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
"proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="],
"undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@mariozechner/pi-ai/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.73.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw=="],
"fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"gaxios/node-fetch/data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
}
}

View File

@ -0,0 +1,20 @@
{
"name": "compass-bridge",
"version": "0.1.0",
"description": "Local bridge daemon connecting Claude Code to Compass",
"type": "module",
"bin": {
"compass-bridge": "./src/index.ts"
},
"scripts": {
"start": "bun run src/index.ts start",
"build": "bun build src/index.ts --target=bun --outdir=dist --minify"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@mariozechner/pi-ai": "^0.52.8"
},
"devDependencies": {
"@types/bun": "latest"
}
}

View File

@ -0,0 +1,84 @@
import type { BridgeConfig } from "./config"
interface RegisterUser {
readonly id: string
readonly name: string
readonly email: string
readonly role: string
}
interface RegisterTool {
readonly name: string
readonly description: string
readonly scope: string
}
export interface RegisterResult {
readonly user: RegisterUser
readonly tools: ReadonlyArray<RegisterTool>
readonly memories: string
readonly dashboards: ReadonlyArray<{
readonly id: string
readonly name: string
readonly description: string
}>
readonly skills: ReadonlyArray<{
readonly id: string
readonly name: string
readonly enabled: boolean
}>
}
export async function registerWithCompass(
config: BridgeConfig,
): Promise<RegisterResult> {
const url = `${config.compassUrl}/api/bridge/register`
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${config.apiKey}`,
"Content-Type": "application/json",
},
})
if (!res.ok) {
const body = await res.text()
throw new Error(
`Registration failed (${res.status}): ${body}`,
)
}
return res.json() as Promise<RegisterResult>
}
export async function refreshContext(
config: BridgeConfig,
): Promise<{
readonly memories: string
readonly dashboards: ReadonlyArray<{
readonly id: string
readonly name: string
readonly description: string
}>
readonly skills: ReadonlyArray<{
readonly id: string
readonly name: string
readonly enabled: boolean
}>
}> {
const url = `${config.compassUrl}/api/bridge/context`
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${config.apiKey}`,
},
})
if (!res.ok) {
throw new Error(
`Context refresh failed (${res.status})`,
)
}
return res.json()
}

View File

@ -0,0 +1,283 @@
import { existsSync } from "fs"
import {
readFile,
writeFile,
mkdir,
chmod,
} from "fs/promises"
import { homedir } from "os"
import { join } from "path"
import { refreshAnthropicToken } from "@mariozechner/pi-ai"
const CONFIG_DIR = join(homedir(), ".compass-bridge")
const CONFIG_PATH = join(CONFIG_DIR, "config.json")
const DEBUG_AUTH =
process.env.COMPASS_BRIDGE_DEBUG_AUTH === "1"
export interface OAuthCredentials {
readonly access: string
readonly refresh: string
readonly expires: number
}
export interface BridgeConfig {
readonly compassUrl: string
readonly apiKey: string
readonly anthropicApiKey?: string
readonly oauthCredentials?: OAuthCredentials
readonly port: number
readonly allowedOrigins: ReadonlyArray<string>
}
const DEFAULT_CONFIG: BridgeConfig = {
compassUrl: "",
apiKey: "",
port: 18789,
allowedOrigins: [],
}
export async function loadConfig(): Promise<BridgeConfig> {
if (!existsSync(CONFIG_PATH)) {
return DEFAULT_CONFIG
}
const raw = await readFile(CONFIG_PATH, "utf-8")
const parsed = JSON.parse(raw) as Partial<BridgeConfig>
return {
...DEFAULT_CONFIG,
...parsed,
}
}
export async function saveConfig(
config: BridgeConfig,
): Promise<void> {
await mkdir(CONFIG_DIR, { recursive: true })
await chmod(CONFIG_DIR, 0o700)
await writeFile(
CONFIG_PATH,
JSON.stringify(config, null, 2),
"utf-8",
)
await chmod(CONFIG_PATH, 0o600)
}
export function isConfigured(
config: BridgeConfig,
): boolean {
return (
config.compassUrl.length > 0 &&
config.apiKey.length > 0
)
}
// -- Claude Code credential discovery --
const CLAUDE_CREDENTIALS_PATH = join(
homedir(),
".claude",
".credentials.json",
)
interface ClaudeOAuthCredentials {
readonly accessToken: string
readonly refreshToken: string
readonly expiresAt: number
readonly subscriptionType?: string
}
interface ClaudeCredentialsFile {
readonly claudeAiOauth?: ClaudeOAuthCredentials
}
export function loadClaudeCredentials():
ClaudeOAuthCredentials | undefined {
if (!existsSync(CLAUDE_CREDENTIALS_PATH)) {
return undefined
}
try {
const raw = require("fs").readFileSync(
CLAUDE_CREDENTIALS_PATH,
"utf-8",
)
const parsed =
JSON.parse(raw) as ClaudeCredentialsFile
return parsed.claudeAiOauth
} catch {
return undefined
}
}
export async function refreshClaudeToken(
refreshToken: string,
): Promise<ClaudeOAuthCredentials | undefined> {
try {
const res = await fetch(
"https://console.anthropic.com/v1/oauth/token",
{
method: "POST",
headers: {
"Content-Type":
"application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
}).toString(),
},
)
if (!res.ok) return undefined
const data = (await res.json()) as {
access_token: string
refresh_token: string
expires_in: number
}
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + data.expires_in * 1000,
}
} catch {
return undefined
}
}
export async function refreshOAuthToken(
refreshToken: string,
): Promise<OAuthCredentials | undefined> {
try {
const result =
await refreshAnthropicToken(refreshToken)
return {
access: result.access,
refresh: result.refresh,
expires: result.expires,
}
} catch {
return undefined
}
}
// -- anthropic auth resolution --
// priority: env var > config file > bridge oauth
export type AnthropicAuth =
| { readonly type: "apiKey"; readonly key: string }
| {
readonly type: "oauthToken"
readonly token: string
}
export function hasAnthropicKey(
config: BridgeConfig,
): boolean {
const envKey = process.env.ANTHROPIC_API_KEY
if (envKey && envKey.length > 0) return true
const configKey = config.anthropicApiKey
if (configKey && configKey.length > 0) return true
if (config.oauthCredentials?.access) return true
return false
}
// setup-tokens (sk-ant-oat01-) need Bearer auth,
// API keys (sk-ant-api...) need x-api-key header
function resolveKeyType(key: string): AnthropicAuth {
if (key.startsWith("sk-ant-oat")) {
return { type: "oauthToken", token: key }
}
return { type: "apiKey", key }
}
function describeToken(token: string): string {
if (token.startsWith("sk-ant-oat")) return "setup-token"
if (token.startsWith("sk-ant-api")) return "api-key"
if (token.startsWith("sk-ant-")) return "sk-ant"
if (token.startsWith("cc_") || token.startsWith("claude")) {
return "claude-code"
}
return "unknown"
}
function logAuth(source: string, auth: AnthropicAuth): void {
if (!DEBUG_AUTH) return
if (auth.type === "apiKey") {
console.log(
`[bridge] anthropic auth: ${source} (apiKey, ${describeToken(auth.key)})`,
)
return
}
console.log(
`[bridge] anthropic auth: ${source} (oauth, ${describeToken(auth.token)})`,
)
}
export async function getAnthropicAuth(
config: BridgeConfig,
): Promise<AnthropicAuth | undefined> {
// 1. env var (highest priority)
const envKey = process.env.ANTHROPIC_API_KEY
if (envKey && envKey.length > 0) {
const resolved = resolveKeyType(envKey)
logAuth("env", resolved)
return resolved
}
// 2. explicit key in bridge config
const configKey = config.anthropicApiKey
if (configKey && configKey.length > 0) {
const resolved = resolveKeyType(configKey)
logAuth("config", resolved)
return resolved
}
// 3. bridge OAuth credentials
if (config.oauthCredentials) {
const oauth = config.oauthCredentials
const isExpired =
Date.now() > oauth.expires - 5 * 60 * 1000
if (!isExpired) {
const auth: AnthropicAuth = {
type: "oauthToken",
token: oauth.access,
}
logAuth("oauth", auth)
return auth
}
// try to refresh
console.log(
"[bridge] OAuth token expired, refreshing...",
)
const refreshed = await refreshOAuthToken(
oauth.refresh,
)
if (refreshed) {
// update config with new tokens
const updated: BridgeConfig = {
...config,
oauthCredentials: refreshed,
}
await saveConfig(updated)
const auth: AnthropicAuth = {
type: "oauthToken",
token: refreshed.access,
}
logAuth("oauth-refresh", auth)
return auth
}
console.warn(
"[bridge] OAuth token refresh failed. " +
"Run 'compass-bridge login' to " +
"re-authenticate.",
)
}
// Claude Code credentials (~/.claude/.credentials.json)
// are NOT usable here -- those tokens are restricted
// to Claude Code only and will return 400 from bridge.
return undefined
}
export { CONFIG_DIR, CONFIG_PATH, CLAUDE_CREDENTIALS_PATH }

View File

@ -0,0 +1,407 @@
#!/usr/bin/env bun
// compass-bridge CLI entry point
import {
loadConfig,
saveConfig,
isConfigured,
hasAnthropicKey,
loadClaudeCredentials,
CONFIG_PATH,
type BridgeConfig,
} from "./config"
import { registerWithCompass } from "./auth"
import { startServer } from "./server"
import { login as oauthLogin } from "./oauth"
import { startProxy, PROXY_PORT } from "./proxy"
const args = process.argv.slice(2)
const command = args[0] ?? "help"
const flags = new Set(args.slice(1))
async function promptInput(
question: string,
): Promise<string> {
process.stdout.write(question)
for await (const line of console) {
return line.trim()
}
return ""
}
async function promptYesNo(
question: string,
defaultYes = true,
): Promise<boolean> {
const hint = defaultYes ? "Y/n" : "y/N"
const answer = await promptInput(
`${question} (${hint}): `,
)
if (answer === "") return defaultYes
return answer.toLowerCase().startsWith("y")
}
async function init(): Promise<void> {
console.log("compass-bridge setup\n")
const existing = await loadConfig()
const compassUrl = await promptInput(
`Compass URL [${existing.compassUrl || "https://your-compass.example.com"}]: `,
)
const apiKey = await promptInput(
"Compass API key (ck_...): ",
)
const portStr = await promptInput(
`Port [${existing.port || 18789}]: `,
)
const config: BridgeConfig = {
compassUrl:
compassUrl || existing.compassUrl || "",
apiKey: apiKey || existing.apiKey || "",
anthropicApiKey: existing.anthropicApiKey,
oauthCredentials: existing.oauthCredentials,
port: portStr
? parseInt(portStr, 10)
: existing.port || 18789,
allowedOrigins: existing.allowedOrigins,
}
await saveConfig(config)
console.log(`\nConfig saved to ${CONFIG_PATH}`)
// verify compass connection
if (isConfigured(config)) {
console.log("\nVerifying connection...")
try {
const result =
await registerWithCompass(config)
console.log(
`Connected as ${result.user.name} (${result.user.role})`,
)
console.log(
`${result.tools.length} tools available`,
)
} catch (err) {
const msg =
err instanceof Error
? err.message
: "unknown error"
console.error(`Connection failed: ${msg}`)
console.error(
"Check your Compass URL and API key.",
)
}
}
// anthropic auth
if (config.oauthCredentials) {
console.log(
"\nAnthropic: authenticated via OAuth",
)
} else if (config.anthropicApiKey) {
const kind = config.anthropicApiKey.startsWith(
"sk-ant-oat",
)
? "setup-token"
: "API key"
console.log(`\nAnthropic: ${kind} configured`)
} else if (!hasAnthropicKey(config)) {
console.log("")
const wantsAuth = await promptYesNo(
"Authenticate with Anthropic now?",
)
if (wantsAuth) {
await runLogin(config)
} else {
console.log(
"\nYou can authenticate later with " +
"'compass-bridge login'\n" +
"or set the ANTHROPIC_API_KEY env var.",
)
}
} else {
console.log(
"\nAnthropic: env var configured",
)
}
console.log(
"\nRun 'compass-bridge start' to launch the daemon.",
)
}
async function runLogin(
existingConfig?: BridgeConfig,
): Promise<void> {
const config =
existingConfig ?? (await loadConfig())
console.log("\nAuthenticate with Anthropic:\n")
console.log(
" 1. OAuth (browser login)" +
" - opens anthropic.com in your browser",
)
console.log(
" 2. Setup token" +
" - run 'claude setup-token' and paste",
)
console.log(
" 3. API key" +
" - paste key from console.anthropic.com",
)
const choice = await promptInput("\nChoice [1]: ")
const method = choice === "2"
? "token"
: choice === "3"
? "apikey"
: "oauth"
if (method === "oauth") {
try {
const result = await oauthLogin({
manual: flags.has("--manual"),
})
const updated: BridgeConfig = {
...config,
oauthCredentials: result,
// ensure OAuth takes effect by clearing stale keys
anthropicApiKey: undefined,
}
await saveConfig(updated)
console.log("\nauthenticated with Anthropic.")
console.log(
`token: ${result.access.slice(0, 15)}...`,
)
} catch (err) {
const msg =
err instanceof Error
? err.message
: "unknown error"
console.error(`\nOAuth login failed: ${msg}`)
console.error(
"try again with --manual, or use option " +
"2 (setup-token) or 3 (API key).",
)
}
return
}
if (method === "token") {
console.log(
"\nrun 'claude setup-token' in your " +
"terminal, then paste below.",
)
} else {
console.log(
"\npaste your API key from " +
"console.anthropic.com/settings/keys",
)
}
const key = await promptInput(
"\ntoken (sk-ant-...): ",
)
if (!key || !key.startsWith("sk-ant-")) {
console.error(
"invalid token. expected format: sk-ant-...",
)
return
}
const updated: BridgeConfig = {
...config,
anthropicApiKey: key,
// clear stale oauth credentials
oauthCredentials: undefined,
}
await saveConfig(updated)
const kind = key.startsWith("sk-ant-oat")
? "setup-token"
: "API key"
console.log(
`\n${kind} saved: ${key.slice(0, 15)}...`,
)
}
async function start(): Promise<void> {
const config = await loadConfig()
if (!isConfigured(config)) {
console.error(
"Not configured. " +
"Run 'compass-bridge init' first.",
)
process.exit(1)
}
if (!hasAnthropicKey(config)) {
console.error(
"No Anthropic API key. " +
"Run 'compass-bridge login' " +
"or set ANTHROPIC_API_KEY env var.",
)
process.exit(1)
}
if (flags.has("--proxy")) {
const portEnv = process.env.COMPASS_BRIDGE_PROXY_PORT
const port = portEnv
? parseInt(portEnv, 10)
: PROXY_PORT
const baseUrlEnv =
process.env.COMPASS_BRIDGE_ANTHROPIC_BASE_URL
if (!baseUrlEnv) {
process.env.COMPASS_BRIDGE_ANTHROPIC_BASE_URL =
`http://127.0.0.1:${port}`
}
startProxy(port)
console.log(
`[bridge] proxy enabled for start (port ${port})`,
)
}
console.log("[bridge] registering with Compass...")
try {
const registration =
await registerWithCompass(config)
startServer(config, registration)
} catch (err) {
const msg =
err instanceof Error
? err.message
: "unknown error"
console.error(
`[bridge] failed to start: ${msg}`,
)
process.exit(1)
}
}
async function status(): Promise<void> {
const config = await loadConfig()
if (!isConfigured(config)) {
console.log("Status: not configured")
console.log(
"Run 'compass-bridge init' to set up.",
)
return
}
console.log(`Compass URL: ${config.compassUrl}`)
console.log(`Port: ${config.port}`)
console.log(
`API key: ${config.apiKey.slice(0, 11)}...`,
)
const envKey = process.env.ANTHROPIC_API_KEY
const creds = loadClaudeCredentials()
if (envKey) {
console.log(
"Anthropic key: env var (ANTHROPIC_API_KEY)",
)
if (config.oauthCredentials) {
console.log(
" note: OAuth credentials present but env var takes precedence",
)
}
if (config.anthropicApiKey) {
console.log(
" note: bridge config key present but env var takes precedence",
)
}
} else if (config.anthropicApiKey) {
console.log("Anthropic key: bridge config")
if (config.oauthCredentials) {
console.log(
" note: OAuth credentials present but bridge config takes precedence",
)
}
} else if (config.oauthCredentials) {
const expired =
Date.now() > config.oauthCredentials.expires
console.log(
`Anthropic key: OAuth credentials` +
(expired
? " (token expired, will refresh)"
: ""),
)
} else if (creds) {
const expired = Date.now() > creds.expiresAt
console.log(
`Anthropic key: Claude Code credentials` +
(expired
? " (token expired, will refresh)"
: ""),
)
} else {
console.log("Anthropic key: not set")
}
// check if daemon is running
try {
const res = await fetch(
`http://127.0.0.1:${config.port}/health`,
)
if (res.ok) {
console.log("Daemon: running")
} else {
console.log("Daemon: not running")
}
} catch {
console.log("Daemon: not running")
}
}
async function proxy(): Promise<void> {
const portEnv = process.env.COMPASS_BRIDGE_PROXY_PORT
const port = portEnv
? parseInt(portEnv, 10)
: PROXY_PORT
startProxy(port)
}
switch (command) {
case "init":
await init()
break
case "login":
await runLogin()
break
case "start":
await start()
break
case "status":
await status()
break
case "proxy":
await proxy()
break
case "help":
default:
console.log(`compass-bridge - local daemon for Claude Code + Compass
Usage:
compass-bridge init Configure Compass URL and API keys
compass-bridge login Authenticate with Anthropic (OAuth, setup-token, or API key)
compass-bridge start Start the bridge daemon
compass-bridge status Check daemon status
compass-bridge proxy Start the Claude Code auth proxy
compass-bridge help Show this help message
Options:
--manual Use manual OAuth flow (for headless environments)
--proxy Start Claude Code proxy with the daemon
`)
break
}

View File

@ -0,0 +1,690 @@
// Anthropic API client + agentic tool-calling loop
import Anthropic from "@anthropic-ai/sdk"
import {
getAnthropicAuth,
type AnthropicAuth,
type BridgeConfig,
} from "./config"
import type { RegisterResult } from "./auth"
import {
executeTool,
localToolDefinitions,
} from "./tools/registry"
import { buildSystemPrompt } from "./prompt"
import { addMessage, getMessages } from "./session"
type WsSend = (data: string) => void
interface ChatRequest {
readonly messages: ReadonlyArray<{
readonly role: string
readonly parts?: ReadonlyArray<{
readonly type: string
readonly text?: string
}>
}>
readonly context: {
readonly conversationId: string
readonly currentPage: string
readonly timezone: string
}
readonly model?: string
readonly runId: string
}
export async function handleChatRequest(
config: BridgeConfig,
registration: RegisterResult,
request: ChatRequest,
send: WsSend,
abortSignal: AbortSignal,
): Promise<void> {
// determine auth mode
type AuthMode =
| { kind: "sdk"; client: Anthropic }
| { kind: "oauth"; token: string }
let authMode: AuthMode
const auth = await getAnthropicAuth(config)
if (!auth) {
send(
JSON.stringify({
type: "chat.error",
runId: request.runId,
error:
"no anthropic API key configured. " +
"run 'compass-bridge login' to authenticate, " +
"or set ANTHROPIC_API_KEY env var.",
}),
)
return
}
if (auth.type === "apiKey") {
authMode = {
kind: "sdk",
client: new Anthropic({ apiKey: auth.key }),
}
} else {
// oauth -- use raw fetch (SDK doesn't
// handle custom fetch correctly)
console.log("[bridge] using OAuth token")
authMode = { kind: "oauth", token: auth.token }
}
const systemPrompt = buildSystemPrompt(registration)
const conversationId = request.context.conversationId
// extract text from incoming messages
for (const msg of request.messages) {
const text = extractText(msg)
if (text) {
addMessage(
conversationId,
msg.role === "user" ? "user" : "assistant",
text,
)
}
}
const history = getMessages(conversationId)
const anthropicMessages: Anthropic.MessageParam[] =
history.map((m) => ({
role: m.role,
content: m.content,
}))
// build tool definitions
const compassToolDefs: Anthropic.Tool[] =
registration.tools.map((t) => ({
name: t.name,
description: t.description,
input_schema: {
type: "object" as const,
properties: {},
},
}))
const localToolDefs: Anthropic.Tool[] =
localToolDefinitions.map((t) => ({
name: t.name,
description: t.description,
input_schema: t.input_schema,
}))
const allTools = [...compassToolDefs, ...localToolDefs]
const ALLOWED_MODELS = new Set([
"claude-sonnet-4-5-20250929",
"claude-opus-4-6",
"claude-haiku-4-5-20251001",
])
const modelId =
request.model && ALLOWED_MODELS.has(request.model)
? request.model
: "claude-sonnet-4-5-20250929"
const MAX_TOOL_ROUNDS = 15
let round = 0
let currentMessages = anthropicMessages
if (authMode.kind === "sdk") {
// --- SDK path (apiKey or proxy) ---
while (
!abortSignal.aborted &&
round < MAX_TOOL_ROUNDS
) {
round++
const stream = authMode.client.messages.stream({
model: modelId,
max_tokens: 8192,
system: systemPrompt,
messages: currentMessages,
tools: allTools,
})
let fullText = ""
const toolUses: Array<{
id: string
name: string
input: Record<string, unknown>
}> = []
for await (const event of stream) {
if (abortSignal.aborted) break
if (event.type === "content_block_delta") {
const delta = event.delta
if (
"type" in delta &&
delta.type === "text_delta" &&
"text" in delta
) {
fullText += delta.text
send(
JSON.stringify({
type: "chunk",
runId: request.runId,
chunk: {
type: "text-delta",
textDelta: delta.text,
},
}),
)
}
}
if (event.type === "content_block_start") {
const block = event.content_block
if (block.type === "tool_use") {
send(
JSON.stringify({
type: "chunk",
runId: request.runId,
chunk: {
type: "tool-input-start",
toolName: block.name,
toolCallId: block.id,
},
}),
)
}
}
if (event.type === "message_stop") {
break
}
}
const finalMessage = await stream.finalMessage()
for (const block of finalMessage.content) {
if (block.type === "tool_use") {
toolUses.push({
id: block.id,
name: block.name,
input: block.input as Record<
string,
unknown
>,
})
}
}
if (toolUses.length === 0) {
if (fullText) {
addMessage(
conversationId,
"assistant",
fullText,
)
}
break
}
// execute tools
const toolResults: Anthropic.ToolResultBlockParam[] =
[]
for (const toolUse of toolUses) {
send(
JSON.stringify({
type: "chunk",
runId: request.runId,
chunk: {
type: "tool-input-available",
toolCallId: toolUse.id,
input: toolUse.input,
},
}),
)
const result = await executeTool(
config,
toolUse.name,
toolUse.input,
)
send(
JSON.stringify({
type: "chunk",
runId: request.runId,
chunk: {
type: "data-part-available",
id: toolUse.id,
data: { result },
},
}),
)
toolResults.push({
type: "tool_result",
tool_use_id: toolUse.id,
content: JSON.stringify(result),
})
}
currentMessages = [
...currentMessages,
{
role: "assistant" as const,
content: finalMessage.content,
},
{
role: "user" as const,
content: toolResults,
},
]
}
} else {
// --- OAuth raw fetch path ---
// bypass SDK entirely, make direct API calls
const baseUrl =
process.env.COMPASS_BRIDGE_ANTHROPIC_BASE_URL ??
"https://api.anthropic.com"
const isSetupToken = authMode.token.startsWith(
"sk-ant-oat",
)
if (isSetupToken && baseUrl === "https://api.anthropic.com") {
send(
JSON.stringify({
type: "chat.error",
runId: request.runId,
error:
"Claude Code setup-tokens require Claude Code headers. " +
"Start the bridge proxy and set COMPASS_BRIDGE_ANTHROPIC_BASE_URL, " +
"or use an Anthropic API key.",
}),
)
return
}
const oauthHeaders: Record<string, string> = {
"Content-Type": "application/json",
"anthropic-version": "2023-06-01",
authorization: `Bearer ${authMode.token}`,
"anthropic-beta":
"oauth-2025-04-20," +
"interleaved-thinking-2025-05-14",
"user-agent": "compass-bridge/1.0",
}
if (baseUrl !== "https://api.anthropic.com") {
oauthHeaders["x-compass-bridge"] = "true"
}
if (process.env.COMPASS_BRIDGE_DEBUG_AUTH === "1") {
console.log(
`[bridge] oauth request headers: ${Object.keys(oauthHeaders).join(", ")}`,
)
console.log(
`[bridge] oauth request base: ${baseUrl}`,
)
console.log(
"[bridge] oauth request endpoint: /v1/messages?beta=true",
)
}
while (
!abortSignal.aborted &&
round < MAX_TOOL_ROUNDS
) {
round++
// prefix tool names with mcp_ for oauth endpoint
const oauthTools = allTools.map((t) => ({
...t,
name: `mcp_${t.name}`,
}))
// prefix tool_use names in messages
const oauthMessages = currentMessages.map((msg) => {
if (!msg.content || !Array.isArray(msg.content)) {
return msg
}
const content = msg.content.map((block) => {
if (
block.type === "tool_use" &&
!block.name.startsWith("mcp_")
) {
return {
...block,
name: `mcp_${block.name}`,
}
}
return block
})
return {
...msg,
content,
}
})
const reqBody = JSON.stringify({
model: modelId,
max_tokens: 8192,
stream: true,
system: [
{ type: "text", text: systemPrompt },
],
messages: oauthMessages,
tools: oauthTools,
})
const requestUrl = new URL(
"/v1/messages?beta=true",
baseUrl,
)
const res = await fetch(requestUrl, {
method: "POST",
headers: oauthHeaders,
body: reqBody,
signal: abortSignal,
})
if (!res.ok) {
const errText = await res.text()
send(
JSON.stringify({
type: "chat.error",
runId: request.runId,
error: `anthropic API error (${res.status}): ${errText}`,
}),
)
return
}
if (!res.body) {
send(
JSON.stringify({
type: "chat.error",
runId: request.runId,
error: "no response body from anthropic",
}),
)
return
}
// parse SSE stream
let fullText = ""
const toolUses: Array<{
id: string
name: string
input: Record<string, unknown>
}> = []
const contentBlocks: Array<
Record<string, unknown> | undefined
> = []
// accumulate partial JSON for tool inputs
const partialInputs = new Map<number, string>()
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ""
while (true) {
if (abortSignal.aborted) break
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, {
stream: true,
})
// process complete SSE events
const lines = buffer.split("\n")
buffer = lines.pop() ?? ""
for (const line of lines) {
if (!line.startsWith("data: ")) continue
const data = line.slice(6).trim()
if (data === "[DONE]") continue
let event: Record<string, unknown>
try {
event = JSON.parse(data)
} catch {
continue
}
const eventType = event.type as string
if (eventType === "content_block_start") {
const block = event.content_block as Record<
string,
unknown
>
const index = event.index as number
// strip mcp_ prefix from tool names
if (
block.type === "tool_use" &&
typeof block.name === "string"
) {
block.name = block.name.replace(
/^mcp_/,
"",
)
partialInputs.set(index, "")
send(
JSON.stringify({
type: "chunk",
runId: request.runId,
chunk: {
type: "tool-input-start",
toolName: block.name,
toolCallId: block.id,
},
}),
)
}
contentBlocks[index] = block
}
if (eventType === "content_block_delta") {
const delta = event.delta as Record<
string,
unknown
>
const index = event.index as number
if (delta.type === "text_delta") {
const text = delta.text as string
fullText += text
send(
JSON.stringify({
type: "chunk",
runId: request.runId,
chunk: {
type: "text-delta",
textDelta: text,
},
}),
)
}
if (delta.type === "input_json_delta") {
const partial =
delta.partial_json as string
const existing =
partialInputs.get(index) ?? ""
partialInputs.set(
index,
existing + partial,
)
}
}
if (eventType === "content_block_stop") {
const index = event.index as number
const block = contentBlocks[index]
if (block?.type === "tool_use") {
const inputStr =
partialInputs.get(index) ?? "{}"
let input: Record<string, unknown> = {}
try {
input = JSON.parse(inputStr)
} catch {
// malformed input
}
toolUses.push({
id: block.id as string,
name: block.name as string,
input,
})
}
}
if (eventType === "message_stop") {
break
}
}
}
// if no tool calls, we're done
if (toolUses.length === 0) {
if (fullText) {
addMessage(
conversationId,
"assistant",
fullText,
)
}
break
}
// execute tools
const toolResultBlocks: Array<
Anthropic.ToolResultBlockParam
> = []
for (const toolUse of toolUses) {
send(
JSON.stringify({
type: "chunk",
runId: request.runId,
chunk: {
type: "tool-input-available",
toolCallId: toolUse.id,
input: toolUse.input,
},
}),
)
const result = await executeTool(
config,
toolUse.name,
toolUse.input,
)
send(
JSON.stringify({
type: "chunk",
runId: request.runId,
chunk: {
type: "data-part-available",
id: toolUse.id,
data: { result },
},
}),
)
toolResultBlocks.push({
type: "tool_result",
tool_use_id: toolUse.id,
content: JSON.stringify(result),
})
}
// build assistant content from blocks
const assistantContent: Array<
Anthropic.ContentBlockParam
> = []
for (const block of contentBlocks) {
if (!block) continue
if (isTextBlock(block)) {
assistantContent.push({
type: "text",
text: fullText,
})
} else if (isToolUseBlock(block)) {
const tu = toolUses.find(
(t) => t.id === block.id,
)
assistantContent.push({
type: "tool_use",
id: block.id,
name: block.name,
input: tu?.input ?? {},
})
}
}
currentMessages = [
...currentMessages,
{
role: "assistant" as const,
content: assistantContent,
},
{
role: "user" as const,
content: toolResultBlocks,
},
]
}
}
if (round >= MAX_TOOL_ROUNDS) {
send(
JSON.stringify({
type: "chunk",
runId: request.runId,
chunk: {
type: "text-delta",
textDelta:
"\n\n[Reached maximum tool call rounds " +
`(${MAX_TOOL_ROUNDS}). Stopping.]`,
},
}),
)
}
send(
JSON.stringify({
type: "chat.done",
runId: request.runId,
}),
)
}
function extractText(msg: {
readonly role: string
readonly parts?: ReadonlyArray<{
readonly type: string
readonly text?: string
}>
}): string {
if (!msg.parts) return ""
return msg.parts
.filter((p) => p.type === "text" && p.text)
.map((p) => p.text ?? "")
.join("")
}
function isTextBlock(
block: Record<string, unknown>,
): block is { type: "text"; text: string } {
return (
block.type === "text" &&
typeof block.text === "string"
)
}
function isToolUseBlock(
block: Record<string, unknown>,
): block is {
type: "tool_use"
id: string
name: string
} {
return (
block.type === "tool_use" &&
typeof block.id === "string" &&
typeof block.name === "string"
)
}

View File

@ -0,0 +1,57 @@
// OAuth login via pi-ai's Anthropic provider
import { loginAnthropic } from "@mariozechner/pi-ai"
export interface LoginOptions {
readonly manual?: boolean
}
// pi-ai credential shape
export interface OAuthResult {
readonly access: string
readonly refresh: string
readonly expires: number
}
export async function login(
options?: LoginOptions,
): Promise<OAuthResult> {
const result = await loginAnthropic(
(url) => {
console.log("\nopen this URL in your browser:\n")
console.log(url)
// try to open browser automatically
try {
const platform = process.platform
const cmd =
platform === "darwin"
? "open"
: platform === "win32"
? "start"
: "xdg-open"
Bun.spawn([cmd, url], {
stdout: "ignore",
stderr: "ignore",
})
} catch {
// user can open manually
}
},
async () => {
console.log(
"\nafter authorizing, paste the code below.",
)
process.stdout.write("\nauthorization code: ")
for await (const line of console) {
return line.trim()
}
return ""
},
)
return {
access: result.access,
refresh: result.refresh,
expires: result.expires,
}
}

View File

@ -0,0 +1,91 @@
// system prompt builder for the bridge daemon
import { hostname, platform, userInfo } from "os"
import type { RegisterResult } from "./auth"
export function buildSystemPrompt(
context: RegisterResult,
): string {
const user = context.user
const now = new Date()
const date = now.toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})
const sections: string[] = []
// anthropic's claude max oauth endpoint requires
// this prefix in the system prompt
sections.push(
"You are Claude Code, Anthropic's official CLI " +
"for Claude.\n\n" +
"You are Dr. Slab Diggems, the AI assistant for " +
"Compass -- a construction project management " +
"platform. You are running via the compass-bridge " +
"daemon on the user's local machine, giving you " +
"access to both Compass data and local files.",
)
sections.push(
`## User Context\n` +
`- Name: ${user.name}\n` +
`- Role: ${user.role}\n` +
`- Date: ${date}\n` +
`- Machine: ${userInfo().username}@${hostname()}\n` +
`- Platform: ${platform()}`,
)
if (context.memories) {
sections.push(
`## What You Remember About This User\n` +
context.memories,
)
}
// tool descriptions
const compassTools = context.tools
.map(
(t) =>
`- ${t.name} [${t.scope}]: ${t.description}`,
)
.join("\n")
const localTools = [
"- readFile: Read a file on the user's machine",
"- writeFile: Write content to a file",
"- listDirectory: List directory contents",
"- searchFiles: Search for files by glob pattern",
"- runCommand: Execute a shell command",
].join("\n")
sections.push(
`## Available Tools\n\n` +
`### Compass Tools (remote)\n${compassTools}\n\n` +
`### Local Tools\n${localTools}`,
)
if (context.dashboards.length > 0) {
const list = context.dashboards
.map((d) => `- ${d.name}: ${d.description}`)
.join("\n")
sections.push(`## Saved Dashboards\n${list}`)
}
sections.push(
"## Guidelines\n" +
"- You have access to the user's local file " +
"system and terminal. Use this to help with " +
"development tasks, file management, and builds.\n" +
"- For Compass data (projects, customers, " +
"invoices, etc.), use the Compass tools.\n" +
"- Be direct and helpful. If you need to run " +
"a command or read a file, just do it.\n" +
"- When running commands, show the output to " +
"the user.",
)
return sections.join("\n\n")
}

View File

@ -0,0 +1,128 @@
// HTTP proxy server that captures Claude Code auth
// and forwards API requests to Anthropic
export const PROXY_PORT = 18790
export interface CapturedAuth {
readonly fullHeaders: Headers
readonly capturedAt: number
}
let capturedAuth: CapturedAuth | null = null
export function getCapturedAuth(): CapturedAuth | null {
return capturedAuth
}
export function startProxy(port: number): void {
const server = Bun.serve({
hostname: "127.0.0.1",
port,
async fetch(req: Request): Promise<Response> {
const url = new URL(req.url)
// detect auth headers
const apiKey = req.headers.get("x-api-key")
const authHeader = req.headers.get("authorization")
const hasAuth = !!(apiKey || authHeader)
// detect bridge vs Claude Code request
const isBridgeRequest =
req.headers.get("x-compass-bridge") === "true"
// build forward request to api.anthropic.com
const targetUrl = new URL(
url.pathname + url.search,
"https://api.anthropic.com",
)
let forwardHeaders: Headers
if (!isBridgeRequest && hasAuth) {
// from Claude Code: capture headers and
// passthrough
capturedAuth = {
fullHeaders: new Headers(req.headers),
capturedAt: Date.now(),
}
console.log(
"[proxy] auth captured from Claude Code request"
)
forwardHeaders = new Headers(req.headers)
} else if (isBridgeRequest && capturedAuth) {
// from bridge: inject captured Claude Code
// headers
forwardHeaders = new Headers(
capturedAuth.fullHeaders
)
// keep content-length and content-type from
// actual request (body size/type differs)
const cl = req.headers.get("content-length")
if (cl) forwardHeaders.set("content-length", cl)
const ct = req.headers.get("content-type")
if (ct) forwardHeaders.set("content-type", ct)
// strip bridge marker before forwarding
forwardHeaders.delete("x-compass-bridge")
console.log(
"[proxy] injecting Claude Code headers for " +
"bridge request"
)
} else {
// no captured auth, forward as-is
forwardHeaders = new Headers(req.headers)
}
// update Host header to match target
forwardHeaders.set("host", "api.anthropic.com")
// forward request
const forwardReq = new Request(targetUrl, {
method: req.method,
headers: forwardHeaders,
body:
req.method !== "GET" && req.method !== "HEAD"
? req.body
: undefined,
})
const res = await fetch(forwardReq)
// strip compression headers (bun auto-decompressed)
const responseHeaders = new Headers(res.headers)
responseHeaders.delete("content-encoding")
responseHeaders.delete("content-length")
responseHeaders.delete("transfer-encoding")
// handle streaming responses (SSE)
const contentType = res.headers.get("content-type")
const isStream = contentType?.includes(
"text/event-stream",
)
if (isStream && res.body) {
// passthrough stream without buffering
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: responseHeaders,
})
}
// for non-streaming, just return the response
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: responseHeaders,
})
},
})
console.log(
`[bridge] proxy listening on http://127.0.0.1:${server.port}`,
)
console.log(
"[bridge] set ANTHROPIC_BASE_URL=http://localhost:" +
`${server.port} when running Claude Code`,
)
}

View File

@ -0,0 +1,249 @@
// WebSocket server -- Bun.serve native WS
import { timingSafeEqual } from "crypto"
import type { ServerWebSocket } from "bun"
import type { BridgeConfig } from "./config"
import type { RegisterResult } from "./auth"
import { handleChatRequest } from "./inference"
import { setCompassTools } from "./tools/registry"
interface WsData {
authenticated: boolean
abortControllers: Map<string, AbortController>
}
export function startServer(
config: BridgeConfig,
registration: RegisterResult,
): void {
// register compass tools from registration
setCompassTools(registration.tools.map((t) => t.name))
const server = Bun.serve<WsData>({
hostname: "127.0.0.1",
port: config.port,
fetch(req, server) {
const url = new URL(req.url)
// health check endpoint
if (url.pathname === "/health") {
return new Response("ok")
}
// upgrade to WebSocket
const upgraded = server.upgrade(req, {
data: {
authenticated: false,
abortControllers: new Map(),
},
})
if (!upgraded) {
return new Response("upgrade failed", {
status: 400,
})
}
return undefined as unknown as Response
},
websocket: {
open(ws: ServerWebSocket<WsData>) {
console.log("[bridge] client connected")
// auto-authenticate -- daemon is localhost-only
// and already authenticated with Compass at startup
ws.data.authenticated = true
ws.send(
JSON.stringify({
type: "auth_ok",
user: {
id: registration.user.id,
name: registration.user.name,
role: registration.user.role,
},
}),
)
},
message(
ws: ServerWebSocket<WsData>,
raw: string | Buffer,
) {
const text =
typeof raw === "string"
? raw
: raw.toString()
let msg: Record<string, unknown>
try {
msg = JSON.parse(text)
} catch {
ws.send(
JSON.stringify({
type: "auth_error",
message: "invalid JSON",
}),
)
return
}
const type = msg.type as string
// heartbeat
if (type === "ping") {
ws.send(JSON.stringify({ type: "pong" }))
return
}
// auth handshake
if (type === "auth") {
const apiKey =
typeof msg.apiKey === "string"
? msg.apiKey
: ""
const a = Buffer.from(apiKey)
const b = Buffer.from(config.apiKey)
const keysMatch =
a.length === b.length &&
timingSafeEqual(a, b)
if (keysMatch) {
ws.data.authenticated = true
ws.send(
JSON.stringify({
type: "auth_ok",
user: {
id: registration.user.id,
name: registration.user.name,
role: registration.user.role,
},
}),
)
} else {
ws.send(
JSON.stringify({
type: "auth_error",
message: "invalid API key",
}),
)
}
return
}
// require auth for everything else
if (!ws.data.authenticated) {
ws.send(
JSON.stringify({
type: "auth_error",
message: "not authenticated",
}),
)
return
}
// chat message
if (type === "chat.send") {
const id = msg.id as string
const runId = crypto.randomUUID()
// ack immediately
ws.send(
JSON.stringify({
type: "chat.ack",
id,
runId,
}),
)
const abortController = new AbortController()
ws.data.abortControllers.set(
runId,
abortController,
)
const context = msg.context as {
currentPage: string
timezone: string
conversationId: string
}
const messages =
msg.messages as ReadonlyArray<{
role: string
parts?: ReadonlyArray<{
type: string
text?: string
}>
}>
const model =
typeof msg.model === "string"
? msg.model
: undefined
handleChatRequest(
config,
registration,
{
messages,
context,
model,
runId,
},
(data: string) => ws.send(data),
abortController.signal,
).catch((err) => {
const errMsg =
err instanceof Error
? err.message
: "inference error"
ws.send(
JSON.stringify({
type: "chat.error",
runId,
error: errMsg,
}),
)
}).finally(() => {
ws.data.abortControllers.delete(runId)
})
return
}
// abort
if (type === "chat.abort") {
const runId = msg.runId as string
const controller =
ws.data.abortControllers.get(runId)
if (controller) {
controller.abort()
ws.data.abortControllers.delete(runId)
}
return
}
},
close(ws: ServerWebSocket<WsData>) {
console.log("[bridge] client disconnected")
// abort all active runs
for (const [, controller] of ws.data
.abortControllers) {
controller.abort()
}
ws.data.abortControllers.clear()
},
},
})
console.log(
`[bridge] listening on ws://127.0.0.1:${server.port}`,
)
console.log(
`[bridge] connected as ${registration.user.name} ` +
`(${registration.user.role})`,
)
console.log(
`[bridge] ${registration.tools.length} compass tools available`,
)
}

View File

@ -0,0 +1,89 @@
// conversation session management -- in-memory message history
interface Message {
readonly role: "user" | "assistant"
readonly content: string
}
interface Session {
id: string
messages: Message[]
createdAt: number
}
const MAX_MESSAGES = 100
const MAX_SESSIONS = 50
const SESSION_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
const sessions = new Map<string, Session>()
function cleanupSessions(): void {
const now = Date.now()
// remove expired sessions
for (const [id, session] of sessions) {
if (now - session.createdAt > SESSION_TTL_MS) {
sessions.delete(id)
}
}
// if still over limit, remove oldest
if (sessions.size > MAX_SESSIONS) {
let oldestId: string | null = null
let oldestTime = Infinity
for (const [id, session] of sessions) {
if (session.createdAt < oldestTime) {
oldestTime = session.createdAt
oldestId = id
}
}
if (oldestId) sessions.delete(oldestId)
}
}
export function getOrCreateSession(
conversationId: string,
): Session {
const existing = sessions.get(conversationId)
if (existing) return existing
cleanupSessions()
const session: Session = {
id: conversationId,
messages: [],
createdAt: Date.now(),
}
sessions.set(conversationId, session)
return session
}
export function addMessage(
conversationId: string,
role: "user" | "assistant",
content: string,
): void {
const session = getOrCreateSession(conversationId)
session.messages.push({ role, content })
// trim to keep last MAX_MESSAGES
if (session.messages.length > MAX_MESSAGES) {
session.messages = session.messages.slice(
-MAX_MESSAGES,
)
}
}
export function getMessages(
conversationId: string,
): ReadonlyArray<Message> {
const session = sessions.get(conversationId)
if (!session) return []
return session.messages
}
export function clearSession(
conversationId: string,
): void {
sessions.delete(conversationId)
}

View File

@ -0,0 +1,155 @@
#!/usr/bin/env bun
// diagnostic: test OAuth token with raw fetch
// exactly matching opencode-anthropic-auth plugin behavior
import { loadConfig } from "./config"
const config = await loadConfig()
console.log("=== compass-bridge auth diagnostic ===\n")
// check what auth is available
const envKey = process.env.ANTHROPIC_API_KEY
console.log(
`env ANTHROPIC_API_KEY: ${envKey ? "set" : "not set"}`,
)
console.log(
`config.anthropicApiKey: ${config.anthropicApiKey ? "set" : "not set"}`,
)
console.log(
`config.oauthCredentials: ${config.oauthCredentials ? "set" : "not set"}`,
)
if (config.oauthCredentials) {
const oauth = config.oauthCredentials
const expired =
Date.now() > oauth.expires
console.log(
` access: ${oauth.access.slice(0, 20)}...`,
)
console.log(
` refresh: ${oauth.refresh ? "set" : "not set"}`,
)
console.log(
` expires: ${new Date(oauth.expires).toISOString()}`,
)
console.log(` expired: ${expired}`)
}
// try raw fetch exactly matching opencode plugin
const token = config.oauthCredentials?.access
if (!token) {
console.error(
"\nno OAuth credentials found. run 'compass-bridge login' first.",
)
process.exit(1)
}
console.log("\n--- test 1: raw fetch to /v1/messages?beta=true ---\n")
const body = JSON.stringify({
model: "claude-sonnet-4-5-20250929",
max_tokens: 100,
system: [
{
type: "text",
text: "You are Claude Code, Anthropic's official CLI for Claude.",
},
],
messages: [
{
role: "user",
content: "Say hello in exactly 3 words.",
},
],
tools: [
{
name: "mcp_testTool",
description: "A test tool",
input_schema: {
type: "object",
properties: {},
},
},
],
})
const headers: Record<string, string> = {
"Content-Type": "application/json",
"anthropic-version": "2023-06-01",
authorization: `Bearer ${token}`,
"anthropic-beta":
"oauth-2025-04-20,interleaved-thinking-2025-05-14",
"user-agent": "compass-bridge/1.0",
}
console.log("request headers:")
for (const [k, v] of Object.entries(headers)) {
if (k === "authorization") {
console.log(` ${k}: Bearer ${token.slice(0, 20)}...`)
} else {
console.log(` ${k}: ${v}`)
}
}
console.log(`\nrequest URL: https://api.anthropic.com/v1/messages?beta=true`)
console.log(`request body model: claude-sonnet-4-5-20250929`)
try {
const res = await fetch(
"https://api.anthropic.com/v1/messages?beta=true",
{
method: "POST",
headers,
body,
},
)
console.log(`\nresponse status: ${res.status}`)
const text = await res.text()
console.log(`response body: ${text.slice(0, 500)}`)
if (res.ok) {
console.log("\n SUCCESS -- token works with raw fetch!")
} else {
console.log("\n FAILED -- token rejected by Anthropic")
// try without ?beta=true
console.log(
"\n--- test 2: without ?beta=true ---\n",
)
const res2 = await fetch(
"https://api.anthropic.com/v1/messages",
{
method: "POST",
headers,
body,
},
)
console.log(`response status: ${res2.status}`)
const text2 = await res2.text()
console.log(`response body: ${text2.slice(0, 500)}`)
// try with x-api-key instead of Bearer
console.log(
"\n--- test 3: with x-api-key instead of Bearer ---\n",
)
const headers3 = { ...headers }
delete headers3.authorization
headers3["x-api-key"] = token
const res3 = await fetch(
"https://api.anthropic.com/v1/messages?beta=true",
{
method: "POST",
headers: headers3,
body,
},
)
console.log(`response status: ${res3.status}`)
const text3 = await res3.text()
console.log(`response body: ${text3.slice(0, 500)}`)
}
} catch (err) {
console.error("fetch error:", err)
}

View File

@ -0,0 +1,34 @@
// remote compass tool execution -- calls back to Compass cloud
import type { BridgeConfig } from "../config"
interface ToolResult {
readonly success: boolean
readonly result?: unknown
readonly error?: string
}
export async function executeCompassTool(
config: BridgeConfig,
toolName: string,
args: Record<string, unknown>,
): Promise<ToolResult> {
const url = `${config.compassUrl}/api/bridge/tools`
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${config.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ tool: toolName, args }),
})
if (!res.ok) {
return {
success: false,
error: `Tool call failed (${res.status})`,
}
}
return res.json() as Promise<ToolResult>
}

View File

@ -0,0 +1,103 @@
// local filesystem tools -- give claude access to the user's machine
import { readFile, writeFile, readdir, stat } from "fs/promises"
import { existsSync } from "fs"
import { join, resolve } from "path"
import { Glob } from "bun"
export async function readLocalFile(
path: string,
): Promise<{ content: string } | { error: string }> {
const resolved = resolve(path)
if (!existsSync(resolved)) {
return { error: `file not found: ${resolved}` }
}
try {
const content = await readFile(resolved, "utf-8")
return { content }
} catch (err) {
const msg = err instanceof Error
? err.message
: "unknown error"
return { error: `failed to read: ${msg}` }
}
}
export async function writeLocalFile(
path: string,
content: string,
): Promise<{ success: true } | { error: string }> {
const resolved = resolve(path)
try {
await writeFile(resolved, content, "utf-8")
return { success: true }
} catch (err) {
const msg = err instanceof Error
? err.message
: "unknown error"
return { error: `failed to write: ${msg}` }
}
}
export async function listLocalDirectory(
path: string,
): Promise<
| { entries: ReadonlyArray<{ name: string; type: string }> }
| { error: string }
> {
const resolved = resolve(path)
if (!existsSync(resolved)) {
return { error: `directory not found: ${resolved}` }
}
try {
const entries = await readdir(resolved, {
withFileTypes: true,
})
return {
entries: entries.map((e) => ({
name: e.name,
type: e.isDirectory() ? "directory" : "file",
})),
}
} catch (err) {
const msg = err instanceof Error
? err.message
: "unknown error"
return { error: `failed to list: ${msg}` }
}
}
export async function searchLocalFiles(
directory: string,
pattern: string,
maxResults = 50,
): Promise<
| { files: ReadonlyArray<string> }
| { error: string }
> {
const resolved = resolve(directory)
if (!existsSync(resolved)) {
return { error: `directory not found: ${resolved}` }
}
try {
const glob = new Glob(pattern)
const matches: string[] = []
for await (const file of glob.scan({
cwd: resolved,
absolute: true,
})) {
matches.push(file)
if (matches.length >= maxResults) break
}
return { files: matches }
} catch (err) {
const msg = err instanceof Error
? err.message
: "unknown error"
return { error: `search failed: ${msg}` }
}
}

View File

@ -0,0 +1,251 @@
// tool registry -- routes tool calls to compass (remote) or local handlers
import type { BridgeConfig } from "../config"
import { executeCompassTool } from "./compass"
import {
readLocalFile,
writeLocalFile,
listLocalDirectory,
searchLocalFiles,
} from "./filesystem"
import { runCommand } from "./terminal"
interface ToolCallResult {
readonly success: boolean
readonly result?: unknown
readonly error?: string
}
// compass tools are discovered at registration time
let compassToolNames = new Set<string>()
export function setCompassTools(
names: ReadonlyArray<string>,
): void {
compassToolNames = new Set(names)
}
// local tool definitions for the Anthropic API
export const localToolDefinitions = [
{
name: "readFile",
description:
"Read the contents of a file on the user's machine.",
input_schema: {
type: "object" as const,
properties: {
path: {
type: "string",
description: "Absolute or relative file path",
},
},
required: ["path"],
},
},
{
name: "writeFile",
description:
"Write content to a file on the user's machine.",
input_schema: {
type: "object" as const,
properties: {
path: {
type: "string",
description: "Absolute or relative file path",
},
content: {
type: "string",
description: "File content to write",
},
},
required: ["path", "content"],
},
},
{
name: "listDirectory",
description:
"List files and directories in a path.",
input_schema: {
type: "object" as const,
properties: {
path: {
type: "string",
description: "Directory path to list",
},
},
required: ["path"],
},
},
{
name: "searchFiles",
description:
"Search for files matching a glob pattern.",
input_schema: {
type: "object" as const,
properties: {
directory: {
type: "string",
description: "Root directory to search from",
},
pattern: {
type: "string",
description:
"Glob pattern (e.g. '**/*.ts')",
},
maxResults: {
type: "number",
description:
"Maximum results to return (default 50)",
},
},
required: ["directory", "pattern"],
},
},
{
name: "runCommand",
description:
"Execute a shell command on the user's machine. " +
"Use for git, npm, build tools, etc.",
input_schema: {
type: "object" as const,
properties: {
command: {
type: "string",
description: "Shell command to execute",
},
cwd: {
type: "string",
description: "Working directory (optional)",
},
},
required: ["command"],
},
},
]
const LOCAL_TOOL_NAMES = new Set(
localToolDefinitions.map((t) => t.name),
)
export async function executeTool(
config: BridgeConfig,
toolName: string,
args: Record<string, unknown>,
): Promise<ToolCallResult> {
// check local tools first
if (LOCAL_TOOL_NAMES.has(toolName)) {
return executeLocalTool(toolName, args)
}
// fall back to compass tools
if (compassToolNames.has(toolName)) {
return executeCompassTool(config, toolName, args)
}
return {
success: false,
error: `unknown tool: ${toolName}`,
}
}
function getString(
args: Record<string, unknown>,
key: string,
): string | undefined {
const v = args[key]
return typeof v === "string" ? v : undefined
}
function getNumber(
args: Record<string, unknown>,
key: string,
): number | undefined {
const v = args[key]
return typeof v === "number" ? v : undefined
}
async function executeLocalTool(
toolName: string,
args: Record<string, unknown>,
): Promise<ToolCallResult> {
switch (toolName) {
case "readFile": {
const path = getString(args, "path")
if (!path) {
return { success: false, error: "path required" }
}
const result = await readLocalFile(path)
if ("error" in result) {
return { success: false, error: result.error }
}
return { success: true, result }
}
case "writeFile": {
const path = getString(args, "path")
const content = getString(args, "content")
if (!path || content === undefined) {
return {
success: false,
error: "path and content required",
}
}
const result = await writeLocalFile(path, content)
if ("error" in result) {
return { success: false, error: result.error }
}
return { success: true, result }
}
case "listDirectory": {
const path = getString(args, "path")
if (!path) {
return { success: false, error: "path required" }
}
const result = await listLocalDirectory(path)
if ("error" in result) {
return { success: false, error: result.error }
}
return { success: true, result }
}
case "searchFiles": {
const directory = getString(args, "directory")
const pattern = getString(args, "pattern")
if (!directory || !pattern) {
return {
success: false,
error: "directory and pattern required",
}
}
const maxResults =
getNumber(args, "maxResults") ?? 50
const result = await searchLocalFiles(
directory,
pattern,
maxResults,
)
if ("error" in result) {
return { success: false, error: result.error }
}
return { success: true, result }
}
case "runCommand": {
const command = getString(args, "command")
if (!command) {
return {
success: false,
error: "command required",
}
}
const cwd = getString(args, "cwd")
const result = await runCommand(command, cwd)
if ("error" in result) {
return { success: false, error: result.error }
}
return { success: true, result }
}
default:
return {
success: false,
error: `unknown local tool: ${toolName}`,
}
}
}

View File

@ -0,0 +1,75 @@
// safe local command execution
const DEFAULT_TIMEOUT = 30_000
const BLOCKED_PATTERNS: ReadonlyArray<RegExp> = [
/\brm\s+-rf\s+[\/~]/i,
/\bdd\s+/i,
/\bmkfs\b/i,
/\bformat\b/i,
/\b:\(\)\s*\{/,
/\bchmod\s+-R\s+777/i,
/\bchown\s+-R/i,
/>\s*\/dev\/sd/i,
/\bcurl\b.*\|\s*sh/i,
/\bwget\b.*\|\s*sh/i,
]
function isBlockedCommand(command: string): boolean {
return BLOCKED_PATTERNS.some((p) => p.test(command))
}
export async function runCommand(
command: string,
cwd?: string,
timeout = DEFAULT_TIMEOUT,
): Promise<
| {
stdout: string
stderr: string
exitCode: number
}
| { error: string }
> {
if (isBlockedCommand(command)) {
return {
error:
"command blocked: matches a restricted pattern",
}
}
try {
const proc = Bun.spawn(["sh", "-c", command], {
cwd: cwd ?? process.cwd(),
stdout: "pipe",
stderr: "pipe",
})
// timeout handling
const timer = setTimeout(() => {
proc.kill()
}, timeout)
const stdout = await new Response(
proc.stdout,
).text()
const stderr = await new Response(
proc.stderr,
).text()
const exitCode = await proc.exited
clearTimeout(timer)
return {
stdout: stdout.slice(0, 50_000),
stderr: stderr.slice(0, 10_000),
exitCode,
}
} catch (err) {
const msg =
err instanceof Error
? err.message
: "unknown error"
return { error: `command failed: ${msg}` }
}
}

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2024",
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"skipLibCheck": true,
"types": ["bun-types"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}

218
src/app/actions/mcp-keys.ts Normal file
View File

@ -0,0 +1,218 @@
"use server"
import { getCloudflareContext } from "@opennextjs/cloudflare"
import { getDb } from "@/db"
import { mcpApiKeys } from "@/db/schema-mcp"
import { eq, and } from "drizzle-orm"
import { getCurrentUser } from "@/lib/auth"
import {
generateApiKey,
hashApiKey,
} from "@/lib/mcp/auth"
import { revalidatePath } from "next/cache"
export async function createApiKey(
name: string,
scopes: ReadonlyArray<string>
): Promise<
| { success: true; key: string }
| { success: false; error: string }
> {
try {
const user = await getCurrentUser()
if (!user) {
return { success: false, error: "Unauthorized" }
}
const { env } = await getCloudflareContext()
const db = getDb(env.DB)
const now = new Date().toISOString()
const { key, keyPrefix } = generateApiKey()
const keyHash = await hashApiKey(key)
await db
.insert(mcpApiKeys)
.values({
id: crypto.randomUUID(),
userId: user.id,
name,
keyPrefix,
keyHash,
scopes: JSON.stringify(scopes),
createdAt: now,
isActive: true,
})
.run()
revalidatePath("/dashboard")
return { success: true, key }
} catch (error) {
console.error("Failed to create API key:", error)
return {
success: false,
error:
error instanceof Error
? error.message
: "Unknown error",
}
}
}
export async function listApiKeys(): Promise<
| {
success: true
data: ReadonlyArray<{
readonly id: string
readonly name: string
readonly keyPrefix: string
readonly scopes: ReadonlyArray<string>
readonly lastUsedAt: string | null
readonly createdAt: string
readonly expiresAt: string | null
readonly isActive: boolean
}>
}
| { success: false; error: string }
> {
try {
const user = await getCurrentUser()
if (!user) {
return { success: false, error: "Unauthorized" }
}
const { env } = await getCloudflareContext()
const db = getDb(env.DB)
const rows = await db
.select({
id: mcpApiKeys.id,
name: mcpApiKeys.name,
keyPrefix: mcpApiKeys.keyPrefix,
scopes: mcpApiKeys.scopes,
lastUsedAt: mcpApiKeys.lastUsedAt,
createdAt: mcpApiKeys.createdAt,
expiresAt: mcpApiKeys.expiresAt,
isActive: mcpApiKeys.isActive,
})
.from(mcpApiKeys)
.where(eq(mcpApiKeys.userId, user.id))
.all()
const data = rows.map((row) => ({
...row,
scopes: JSON.parse(row.scopes) as ReadonlyArray<string>,
}))
return { success: true, data }
} catch (error) {
console.error("Failed to list API keys:", error)
return {
success: false,
error:
error instanceof Error
? error.message
: "Unknown error",
}
}
}
export async function revokeApiKey(
keyId: string
): Promise<
{ success: true } | { success: false; error: string }
> {
try {
const user = await getCurrentUser()
if (!user) {
return { success: false, error: "Unauthorized" }
}
const { env } = await getCloudflareContext()
const db = getDb(env.DB)
const existing = await db
.select({ userId: mcpApiKeys.userId })
.from(mcpApiKeys)
.where(
and(
eq(mcpApiKeys.id, keyId),
eq(mcpApiKeys.userId, user.id)
)
)
.get()
if (!existing) {
return { success: false, error: "Key not found" }
}
await db
.update(mcpApiKeys)
.set({ isActive: false })
.where(eq(mcpApiKeys.id, keyId))
.run()
revalidatePath("/dashboard")
return { success: true }
} catch (error) {
console.error("Failed to revoke API key:", error)
return {
success: false,
error:
error instanceof Error
? error.message
: "Unknown error",
}
}
}
export async function deleteApiKey(
keyId: string
): Promise<
{ success: true } | { success: false; error: string }
> {
try {
const user = await getCurrentUser()
if (!user) {
return { success: false, error: "Unauthorized" }
}
const { env } = await getCloudflareContext()
const db = getDb(env.DB)
const existing = await db
.select({ userId: mcpApiKeys.userId })
.from(mcpApiKeys)
.where(
and(
eq(mcpApiKeys.id, keyId),
eq(mcpApiKeys.userId, user.id)
)
)
.get()
if (!existing) {
return { success: false, error: "Key not found" }
}
await db
.delete(mcpApiKeys)
.where(eq(mcpApiKeys.id, keyId))
.run()
revalidatePath("/dashboard")
return { success: true }
} catch (error) {
console.error("Failed to delete API key:", error)
return {
success: false,
error:
error instanceof Error
? error.message
: "Unknown error",
}
}
}

View File

@ -0,0 +1,68 @@
import { NextRequest, NextResponse } from "next/server"
import { getCloudflareContext } from "@opennextjs/cloudflare"
import { getDb } from "@/db"
import { validateApiKey } from "@/lib/mcp/auth"
import { loadMemoriesForPrompt } from "@/lib/agent/memory"
import {
getCustomDashboards,
} from "@/app/actions/dashboards"
import {
getInstalledSkills as getInstalledSkillsAction,
} from "@/app/actions/plugins"
import type { BridgeContextResponse } from "@/lib/mcp/types"
function extractBearer(
req: NextRequest,
): string | null {
const header = req.headers.get("authorization")
if (!header) return null
const parts = header.split(" ")
if (parts.length !== 2 || parts[0] !== "Bearer") {
return null
}
return parts[1] ?? null
}
export async function GET(
req: NextRequest,
): Promise<NextResponse> {
const token = extractBearer(req)
if (!token) {
return NextResponse.json(
{ error: "missing or malformed authorization" },
{ status: 401 },
)
}
const { env } = await getCloudflareContext()
const db = getDb(env.DB)
const authResult = await validateApiKey(db, token)
if (!authResult.valid) {
return NextResponse.json(
{ error: authResult.error },
{ status: 401 },
)
}
const [memories, dashboardResult, skillsResult] =
await Promise.all([
loadMemoriesForPrompt(db, authResult.userId),
getCustomDashboards(),
getInstalledSkillsAction(),
])
const response: BridgeContextResponse = {
memories: memories
? memories.split("\n").filter(Boolean)
: [],
dashboards: dashboardResult.success
? dashboardResult.data
: [],
skills: skillsResult.success
? skillsResult.skills
: [],
}
return NextResponse.json(response)
}

View File

@ -0,0 +1,96 @@
import { NextRequest, NextResponse } from "next/server"
import { eq } from "drizzle-orm"
import { getCloudflareContext } from "@opennextjs/cloudflare"
import { getDb } from "@/db"
import { users } from "@/db/schema"
import { validateApiKey } from "@/lib/mcp/auth"
import { loadMemoriesForPrompt } from "@/lib/agent/memory"
import { getAvailableTools } from "@/lib/mcp/tool-adapter"
import {
getCustomDashboards,
} from "@/app/actions/dashboards"
import {
getInstalledSkills as getInstalledSkillsAction,
} from "@/app/actions/plugins"
import type { BridgeRegisterResponse } from "@/lib/mcp/types"
function extractBearer(
req: NextRequest,
): string | null {
const header = req.headers.get("authorization")
if (!header) return null
const parts = header.split(" ")
if (parts.length !== 2 || parts[0] !== "Bearer") {
return null
}
return parts[1] ?? null
}
export async function POST(
req: NextRequest,
): Promise<NextResponse> {
const token = extractBearer(req)
if (!token) {
return NextResponse.json(
{ error: "missing or malformed authorization" },
{ status: 401 },
)
}
const { env } = await getCloudflareContext()
const db = getDb(env.DB)
const result = await validateApiKey(db, token)
if (!result.valid) {
return NextResponse.json(
{ error: result.error },
{ status: 401 },
)
}
const userRow = await db
.select()
.from(users)
.where(eq(users.id, result.userId))
.get()
if (!userRow) {
return NextResponse.json(
{ error: "user not found" },
{ status: 404 },
)
}
const [memories, dashboardResult, skillsResult] =
await Promise.all([
loadMemoriesForPrompt(db, result.userId),
getCustomDashboards(),
getInstalledSkillsAction(),
])
const tools = getAvailableTools(result.scopes)
const response: BridgeRegisterResponse = {
user: {
id: userRow.id,
name:
userRow.displayName ??
userRow.email.split("@")[0] ??
"User",
email: userRow.email,
role: userRow.role,
},
tools,
memories: memories
? memories.split("\n").filter(Boolean)
: [],
dashboards: dashboardResult.success
? dashboardResult.data
: [],
skills: skillsResult.success
? skillsResult.skills
: [],
}
return NextResponse.json(response)
}

View File

@ -0,0 +1,123 @@
import { NextRequest, NextResponse } from "next/server"
import { eq } from "drizzle-orm"
import { getCloudflareContext } from "@opennextjs/cloudflare"
import { getDb } from "@/db"
import { users } from "@/db/schema"
import { mcpUsage } from "@/db/schema-mcp"
import {
validateApiKey,
checkRateLimit,
} from "@/lib/mcp/auth"
import { executeBridgeTool } from "@/lib/mcp/tool-adapter"
import type { BridgeToolRequest } from "@/lib/mcp/types"
function extractBearer(
req: NextRequest,
): string | null {
const header = req.headers.get("authorization")
if (!header) return null
const parts = header.split(" ")
if (parts.length !== 2 || parts[0] !== "Bearer") {
return null
}
return parts[1] ?? null
}
export async function POST(
req: NextRequest,
): Promise<NextResponse> {
const token = extractBearer(req)
if (!token) {
return NextResponse.json(
{ error: "missing or malformed authorization" },
{ status: 401 },
)
}
const { env } = await getCloudflareContext()
const db = getDb(env.DB)
const authResult = await validateApiKey(db, token)
if (!authResult.valid) {
return NextResponse.json(
{ error: authResult.error },
{ status: 401 },
)
}
const withinLimit = await checkRateLimit(
db,
authResult.keyId,
)
if (!withinLimit) {
return NextResponse.json(
{ error: "rate limit exceeded" },
{ status: 429 },
)
}
let body: BridgeToolRequest
try {
body = (await req.json()) as BridgeToolRequest
} catch {
return NextResponse.json(
{ error: "invalid JSON body" },
{ status: 400 },
)
}
if (
typeof body.tool !== "string" ||
typeof body.args !== "object" ||
body.args === null
) {
return NextResponse.json(
{
error:
"body must include tool (string) " +
"and args (object)",
},
{ status: 400 },
)
}
// look up user role
const userRow = await db
.select({ role: users.role })
.from(users)
.where(eq(users.id, authResult.userId))
.get()
const userRole = userRow?.role ?? "office"
const startMs = Date.now()
const result = await executeBridgeTool(
body.tool,
authResult.userId,
userRole,
body.args,
authResult.scopes,
)
const durationMs = Date.now() - startMs
// log usage (fire-and-forget)
db.insert(mcpUsage)
.values({
id: crypto.randomUUID(),
apiKeyId: authResult.keyId,
userId: authResult.userId,
toolName: body.tool,
success: result.success,
errorMessage: result.success
? null
: result.error,
durationMs,
createdAt: new Date().toISOString(),
})
.run()
.catch(() => {
// non-critical: best-effort usage logging
})
return NextResponse.json(result)
}

View File

@ -11,6 +11,16 @@ import {
} from "@/app/actions/agent" } from "@/app/actions/agent"
import { getTextFromParts } from "@/lib/agent/chat-adapter" import { getTextFromParts } from "@/lib/agent/chat-adapter"
import { useCompassChat } from "@/hooks/use-compass-chat" import { useCompassChat } from "@/hooks/use-compass-chat"
import {
WebSocketChatTransport,
detectBridge,
} from "@/lib/agent/ws-transport"
import {
getBridgeSnapshot,
subscribeBridge,
setBridgeConnected as storeBridgeConnected,
setBridgeEnabled as storeBridgeEnabled,
} from "@/lib/agent/bridge-store"
// --- Panel context (open/close sidebar) --- // --- Panel context (open/close sidebar) ---
@ -97,6 +107,30 @@ export function useRenderState(): RenderContextValue {
return ctx return ctx
} }
// --- Bridge state (module-level store, works anywhere) ---
export interface BridgeContextValue {
readonly bridgeConnected: boolean
readonly bridgeEnabled: boolean
setBridgeEnabled: (enabled: boolean) => void
}
export function useBridgeState(): BridgeContextValue {
const snapshot = React.useSyncExternalStore(
subscribeBridge,
getBridgeSnapshot,
getBridgeSnapshot
)
return React.useMemo(
() => ({
bridgeConnected: snapshot.connected,
bridgeEnabled: snapshot.enabled,
setBridgeEnabled: storeBridgeEnabled,
}),
[snapshot]
)
}
// --- Backward compat aliases --- // --- Backward compat aliases ---
export function useAgent(): PanelContextValue { export function useAgent(): PanelContextValue {
@ -181,9 +215,35 @@ export function ChatProvider({
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
// --- Bridge daemon state (reads from module store) ---
const bridge = useBridgeState()
// detect bridge on interval, write to store
React.useEffect(() => {
let cancelled = false
const check = async () => {
const connected = await detectBridge()
if (!cancelled) storeBridgeConnected(connected)
}
check()
const interval = setInterval(check, 15000)
return () => {
cancelled = true
clearInterval(interval)
}
}, [])
const bridgeTransport = React.useMemo(() => {
if (bridge.bridgeConnected && bridge.bridgeEnabled) {
return new WebSocketChatTransport()
}
return null
}, [bridge.bridgeConnected, bridge.bridgeEnabled])
const chat = useCompassChat({ const chat = useCompassChat({
conversationId, conversationId,
openPanel: () => setIsOpen(true), openPanel: () => setIsOpen(true),
bridgeTransport,
onFinish: async ({ messages: finalMessages }) => { onFinish: async ({ messages: finalMessages }) => {
if (finalMessages.length === 0) return if (finalMessages.length === 0) return

View File

@ -6,6 +6,7 @@ import {
Check, Check,
Search, Search,
Loader2, Loader2,
Zap,
} from "lucide-react" } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
@ -23,6 +24,7 @@ import {
} from "@/components/ui/tooltip" } from "@/components/ui/tooltip"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ProviderIcon, hasLogo } from "./provider-icon" import { ProviderIcon, hasLogo } from "./provider-icon"
import { useBridgeState } from "./chat-provider"
import { import {
getActiveModel, getActiveModel,
getModelList, getModelList,
@ -34,6 +36,27 @@ const DEFAULT_MODEL_ID = "qwen/qwen3-coder-next"
const DEFAULT_MODEL_NAME = "Qwen3 Coder" const DEFAULT_MODEL_NAME = "Qwen3 Coder"
const DEFAULT_PROVIDER = "Alibaba (Qwen)" const DEFAULT_PROVIDER = "Alibaba (Qwen)"
// anthropic models available through the bridge
const BRIDGE_MODELS = [
{
id: "claude-sonnet-4-5-20250929",
name: "Claude Sonnet 4.5",
provider: "Anthropic",
},
{
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
provider: "Anthropic",
},
{
id: "claude-haiku-4-5-20251001",
name: "Claude Haiku 4.5",
provider: "Anthropic",
},
] as const
const DEFAULT_BRIDGE_MODEL = BRIDGE_MODELS[0]
// --- shared state so all instances stay in sync --- // --- shared state so all instances stay in sync ---
interface SharedState { interface SharedState {
@ -47,6 +70,11 @@ interface SharedState {
readonly name: string readonly name: string
readonly provider: string readonly provider: string
} }
readonly bridgeModel: {
readonly id: string
readonly name: string
readonly provider: string
}
readonly allowUserSelection: boolean readonly allowUserSelection: boolean
readonly isAdmin: boolean readonly isAdmin: boolean
readonly maxCostPerMillion: string | null readonly maxCostPerMillion: string | null
@ -64,6 +92,11 @@ let shared: SharedState = {
name: DEFAULT_MODEL_NAME, name: DEFAULT_MODEL_NAME,
provider: DEFAULT_PROVIDER, provider: DEFAULT_PROVIDER,
}, },
bridgeModel: {
id: DEFAULT_BRIDGE_MODEL.id,
name: DEFAULT_BRIDGE_MODEL.name,
provider: DEFAULT_BRIDGE_MODEL.provider,
},
allowUserSelection: true, allowUserSelection: true,
isAdmin: false, isAdmin: false,
maxCostPerMillion: null, maxCostPerMillion: null,
@ -141,10 +174,33 @@ export function ModelDropdown(): React.JSX.Element {
const [activeProvider, setActiveProvider] = const [activeProvider, setActiveProvider] =
React.useState<string | null>(null) React.useState<string | null>(null)
const bridge = useBridgeState()
const bridgeActive =
bridge.bridgeConnected && bridge.bridgeEnabled
React.useEffect(() => { React.useEffect(() => {
if (state.configLoaded) return if (state.configLoaded) return
setShared({ configLoaded: true }) setShared({ configLoaded: true })
// restore bridge model preference from localStorage
const storedBridge = localStorage.getItem(
"compass-bridge-model"
)
if (storedBridge) {
const found = BRIDGE_MODELS.find(
(m) => m.id === storedBridge
)
if (found) {
setShared({
bridgeModel: {
id: found.id,
name: found.name,
provider: found.provider,
},
})
}
}
Promise.all([ Promise.all([
getActiveModel(), getActiveModel(),
getUserModelPreference(), getUserModelPreference(),
@ -222,7 +278,7 @@ export function ModelDropdown(): React.JSX.Element {
}, [state.configLoaded]) }, [state.configLoaded])
React.useEffect(() => { React.useEffect(() => {
if (!open || listLoaded) return if (!open || listLoaded || bridgeActive) return
setLoading(true) setLoading(true)
getModelList().then((result) => { getModelList().then((result) => {
if (result.success) { if (result.success) {
@ -252,7 +308,7 @@ export function ModelDropdown(): React.JSX.Element {
setListLoaded(true) setListLoaded(true)
setLoading(false) setLoading(false)
}) })
}, [open, listLoaded]) }, [open, listLoaded, bridgeActive])
// reset provider filter when popover closes // reset provider filter when popover closes
React.useEffect(() => { React.useEffect(() => {
@ -358,6 +414,91 @@ export function ModelDropdown(): React.JSX.Element {
} }
} }
const handleBridgeModelSelect = (
model: typeof BRIDGE_MODELS[number]
): void => {
setShared({
bridgeModel: {
id: model.id,
name: model.name,
provider: model.provider,
},
})
localStorage.setItem(
"compass-bridge-model",
model.id
)
toast.success(`Bridge model: ${model.name}`)
setOpen(false)
}
// bridge active: show bridge model selector
if (bridgeActive) {
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
className={cn(
"flex items-center gap-1.5 rounded-md px-2 py-1 text-xs",
"hover:bg-muted hover:text-foreground transition-colors",
"text-emerald-600 dark:text-emerald-400",
open && "bg-muted text-foreground"
)}
>
<Zap className="h-3 w-3" />
<span className="max-w-32 truncate">
{state.bridgeModel.name}
</span>
<ChevronDown className="h-3 w-3 opacity-50" />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
side="top"
className="w-64 p-1"
>
<div className="px-2 py-1.5 mb-1">
<p className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
Claude Code Bridge
</p>
</div>
{BRIDGE_MODELS.map((model) => {
const isActive =
model.id === state.bridgeModel.id
return (
<button
key={model.id}
type="button"
onClick={() =>
handleBridgeModelSelect(model)
}
className={cn(
"w-full text-left rounded-lg px-2.5 py-2 flex items-center gap-2.5 transition-all",
isActive
? "bg-primary/10 ring-1 ring-primary/30"
: "hover:bg-muted/70"
)}
>
<ProviderIcon
provider="Anthropic"
size={20}
className="shrink-0"
/>
<span className="text-xs font-medium flex-1">
{model.name}
</span>
{isActive && (
<Check className="h-3 w-3 text-primary shrink-0" />
)}
</button>
)
})}
</PopoverContent>
</Popover>
)
}
if (!state.allowUserSelection && !state.isAdmin) { if (!state.allowUserSelection && !state.isAdmin) {
return ( return (
<div className="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground"> <div className="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground">

View File

@ -29,6 +29,7 @@ import { MemoriesTable } from "@/components/agent/memories-table"
import { SkillsTab } from "@/components/settings/skills-tab" import { SkillsTab } from "@/components/settings/skills-tab"
import { AIModelTab } from "@/components/settings/ai-model-tab" import { AIModelTab } from "@/components/settings/ai-model-tab"
import { AppearanceTab } from "@/components/settings/appearance-tab" import { AppearanceTab } from "@/components/settings/appearance-tab"
import { ClaudeCodeTab } from "@/components/settings/claude-code-tab"
import { useNative } from "@/hooks/use-native" import { useNative } from "@/hooks/use-native"
import { useBiometricAuth } from "@/hooks/use-biometric-auth" import { useBiometricAuth } from "@/hooks/use-biometric-auth"
@ -139,6 +140,8 @@ export function SettingsModal({
<Separator /> <Separator />
<NetSuiteConnectionStatus /> <NetSuiteConnectionStatus />
<SyncControls /> <SyncControls />
<Separator />
<ClaudeCodeTab />
</> </>
) )
@ -305,6 +308,8 @@ export function SettingsModal({
<Separator /> <Separator />
<NetSuiteConnectionStatus /> <NetSuiteConnectionStatus />
<SyncControls /> <SyncControls />
<Separator />
<ClaudeCodeTab />
</TabsContent> </TabsContent>
<TabsContent <TabsContent

View File

@ -0,0 +1,526 @@
"use client"
import * as React from "react"
import {
Key,
Copy,
Trash2,
Check,
Loader2,
Circle,
Terminal,
ChevronDown,
ChevronRight,
Plus,
ShieldCheck,
} from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Separator } from "@/components/ui/separator"
import { Checkbox } from "@/components/ui/checkbox"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
createApiKey,
listApiKeys,
revokeApiKey,
deleteApiKey,
} from "@/app/actions/mcp-keys"
import { useBridgeState } from "@/components/agent/chat-provider"
interface ApiKeyRow {
readonly id: string
readonly name: string
readonly keyPrefix: string
readonly scopes: ReadonlyArray<string>
readonly lastUsedAt: string | null
readonly createdAt: string
readonly expiresAt: string | null
readonly isActive: boolean
}
function CopyButton({
text,
}: {
readonly text: string
}) {
const [copied, setCopied] = React.useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 shrink-0"
onClick={handleCopy}
>
{copied ? (
<Check className="h-3.5 w-3.5 text-green-500" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
</Button>
)
}
function SetupInstructions() {
const [expanded, setExpanded] =
React.useState(false)
const steps = [
{
label: "Generate an API key above",
code: null,
},
{
label: "Install the bridge daemon",
code: "npm install -g compass-bridge",
},
{
label: "Initialize with your Compass URL",
code: "compass-bridge init",
},
{
label: "Paste your API key when prompted",
code: null,
},
{
label: "Start the daemon",
code: "compass-bridge start",
},
]
return (
<div className="space-y-2">
<button
type="button"
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setExpanded((v) => !v)}
>
{expanded ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
Setup instructions
</button>
{expanded && (
<div className="space-y-2 pl-4">
{steps.map((step, i) => (
<div key={i} className="space-y-1">
<p className="text-xs">
<span className="text-muted-foreground">
{i + 1}.
</span>{" "}
{step.label}
</p>
{step.code && (
<div className="flex items-center gap-1 bg-muted rounded px-2 py-1.5">
<Terminal className="h-3 w-3 text-muted-foreground shrink-0" />
<code className="text-[11px] flex-1 font-mono">
{step.code}
</code>
<CopyButton text={step.code} />
</div>
)}
</div>
))}
</div>
)}
</div>
)
}
function CreateKeyDialog({
onCreated,
}: {
readonly onCreated: () => void
}) {
const [open, setOpen] = React.useState(false)
const [name, setName] = React.useState("")
const [scopes, setScopes] = React.useState<
Set<string>
>(new Set(["read", "write"]))
const [creating, setCreating] = React.useState(false)
const [newKey, setNewKey] = React.useState<
string | null
>(null)
const toggleScope = (scope: string) => {
setScopes((prev) => {
const next = new Set(prev)
if (next.has(scope)) next.delete(scope)
else next.add(scope)
return next
})
}
const handleCreate = async () => {
if (!name.trim()) return
setCreating(true)
const result = await createApiKey(
name.trim(),
Array.from(scopes),
)
setCreating(false)
if (result.success) {
setNewKey(result.key)
onCreated()
} else {
toast.error(result.error)
}
}
const handleClose = () => {
setOpen(false)
setName("")
setScopes(new Set(["read", "write"]))
setNewKey(null)
}
return (
<Dialog open={open} onOpenChange={(v) => {
if (!v) handleClose()
else setOpen(true)
}}>
<DialogTrigger asChild>
<Button size="sm" className="h-8">
<Plus className="h-3.5 w-3.5 mr-1" />
Generate Key
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-sm">
{newKey
? "Key Created"
: "Generate API Key"}
</DialogTitle>
<DialogDescription className="text-xs">
{newKey
? "Copy this key now. It won't be shown again."
: "Create a key for the compass-bridge daemon."}
</DialogDescription>
</DialogHeader>
{newKey ? (
<div className="space-y-3">
<div className="flex items-center gap-2 bg-muted rounded-md px-3 py-2">
<code className="text-xs font-mono flex-1 break-all select-all">
{newKey}
</code>
<CopyButton text={newKey} />
</div>
<div className="flex items-start gap-2 text-amber-600 dark:text-amber-400">
<ShieldCheck className="h-4 w-4 shrink-0 mt-0.5" />
<p className="text-xs">
Store this key securely. It cannot be
retrieved after closing this dialog.
</p>
</div>
</div>
) : (
<div className="space-y-3">
<div className="space-y-1.5">
<Label htmlFor="key-name" className="text-xs">
Name
</Label>
<Input
id="key-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="My workstation"
className="h-9 text-sm"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Scopes</Label>
<div className="space-y-2">
{[
{
id: "read",
label: "Read",
desc: "Query data, recall memories, list themes",
},
{
id: "write",
label: "Write",
desc: "Save memories, set themes, CRUD operations",
},
{
id: "admin",
label: "Admin",
desc: "Install/uninstall skills",
},
].map((scope) => (
<label
key={scope.id}
className="flex items-start gap-2 cursor-pointer"
>
<Checkbox
checked={scopes.has(scope.id)}
onCheckedChange={() =>
toggleScope(scope.id)
}
className="mt-0.5"
/>
<div>
<p className="text-xs font-medium">
{scope.label}
</p>
<p className="text-[10px] text-muted-foreground">
{scope.desc}
</p>
</div>
</label>
))}
</div>
</div>
</div>
)}
<DialogFooter>
{newKey ? (
<Button
size="sm"
className="h-8"
onClick={handleClose}
>
Done
</Button>
) : (
<Button
size="sm"
className="h-8"
onClick={handleCreate}
disabled={
creating ||
!name.trim() ||
scopes.size === 0
}
>
{creating && (
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1" />
)}
Generate
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
)
}
function KeyRow({
apiKey,
onRevoke,
onDelete,
}: {
readonly apiKey: ApiKeyRow
readonly onRevoke: (id: string) => void
readonly onDelete: (id: string) => void
}) {
const scopes = apiKey.scopes
return (
<div className="flex items-center gap-3 rounded-md border px-3 py-2">
<Key className="h-4 w-4 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium truncate">
{apiKey.name}
</span>
<Badge
variant={apiKey.isActive ? "default" : "secondary"}
className="text-[10px] px-1.5 py-0"
>
{apiKey.isActive ? "active" : "revoked"}
</Badge>
</div>
<div className="flex items-center gap-2 mt-0.5">
<code className="text-[10px] text-muted-foreground font-mono">
{apiKey.keyPrefix}...
</code>
<span className="text-[10px] text-muted-foreground">
{scopes.join(", ")}
</span>
{apiKey.lastUsedAt && (
<span className="text-[10px] text-muted-foreground">
used{" "}
{new Date(
apiKey.lastUsedAt,
).toLocaleDateString()}
</span>
)}
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
{apiKey.isActive && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => onRevoke(apiKey.id)}
title="Revoke"
>
<Circle className="h-3.5 w-3.5" />
</Button>
)}
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive"
onClick={() => onDelete(apiKey.id)}
title="Delete"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</div>
)
}
export function ClaudeCodeTab() {
const [keys, setKeys] = React.useState<
ReadonlyArray<ApiKeyRow>
>([])
const [loading, setLoading] = React.useState(true)
// use shared bridge state from ChatProvider
const bridge = useBridgeState()
const loadKeys = React.useCallback(async () => {
const result = await listApiKeys()
if (result.success) {
setKeys(result.data)
}
setLoading(false)
}, [])
React.useEffect(() => {
loadKeys()
}, [loadKeys])
const handleRevoke = async (id: string) => {
const prev = keys
setKeys((k) =>
k.map((key) =>
key.id === id
? { ...key, isActive: false }
: key,
),
)
const result = await revokeApiKey(id)
if (result.success) {
toast.success("Key revoked")
} else {
setKeys(prev)
toast.error(result.error)
}
}
const handleDelete = async (id: string) => {
const prev = keys
setKeys((k) => k.filter((key) => key.id !== id))
const result = await deleteApiKey(id)
if (result.success) {
toast.success("Key deleted")
} else {
setKeys(prev)
toast.error(result.error)
}
}
return (
<div className="space-y-3">
<div className="space-y-1.5">
<Label className="text-xs">Claude Code</Label>
<p className="text-muted-foreground text-xs">
Connect Claude Code to Compass via a local
bridge daemon. Uses your own Anthropic API key
for inference.
</p>
</div>
{/* connection status */}
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<div
className={`h-2 w-2 rounded-full ${
bridge.bridgeConnected
? "bg-green-500"
: "bg-red-500"
}`}
/>
<span className="text-xs">
{bridge.bridgeConnected
? "Bridge daemon detected"
: "Bridge daemon not running"}
</span>
</div>
<Switch
checked={bridge.bridgeEnabled}
onCheckedChange={bridge.setBridgeEnabled}
/>
</div>
<Separator />
{/* API keys */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs">API Keys</Label>
<CreateKeyDialog onCreated={loadKeys} />
</div>
{loading ? (
<div className="space-y-2 pt-1">
<div className="bg-muted h-12 animate-pulse rounded-md" />
<div className="bg-muted h-12 animate-pulse rounded-md" />
</div>
) : keys.length === 0 ? (
<p className="text-muted-foreground text-xs py-2">
No API keys yet. Generate one to connect
the bridge daemon.
</p>
) : (
<div className="space-y-1.5">
{keys.map((key) => (
<KeyRow
key={key.id}
apiKey={key}
onRevoke={handleRevoke}
onDelete={handleDelete}
/>
))}
</div>
)}
</div>
<Separator />
<SetupInstructions />
</div>
)
}

View File

@ -7,6 +7,7 @@ import * as aiConfigSchema from "./schema-ai-config"
import * as themeSchema from "./schema-theme" import * as themeSchema from "./schema-theme"
import * as googleSchema from "./schema-google" import * as googleSchema from "./schema-google"
import * as dashboardSchema from "./schema-dashboards" import * as dashboardSchema from "./schema-dashboards"
import * as mcpSchema from "./schema-mcp"
const allSchemas = { const allSchemas = {
...schema, ...schema,
@ -17,6 +18,7 @@ const allSchemas = {
...themeSchema, ...themeSchema,
...googleSchema, ...googleSchema,
...dashboardSchema, ...dashboardSchema,
...mcpSchema,
} }
export function getDb(d1: D1Database) { export function getDb(d1: D1Database) {

43
src/db/schema-mcp.ts Normal file
View File

@ -0,0 +1,43 @@
import {
sqliteTable,
text,
integer,
} from "drizzle-orm/sqlite-core"
import { users } from "./schema"
export const mcpApiKeys = sqliteTable("mcp_api_keys", {
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
name: text("name").notNull(),
keyPrefix: text("key_prefix").notNull(),
keyHash: text("key_hash").notNull(),
scopes: text("scopes").notNull(), // JSON array: ["read","write","admin"]
lastUsedAt: text("last_used_at"),
createdAt: text("created_at").notNull(),
expiresAt: text("expires_at"),
isActive: integer("is_active", { mode: "boolean" })
.notNull()
.default(true),
})
export const mcpUsage = sqliteTable("mcp_usage", {
id: text("id").primaryKey(),
apiKeyId: text("api_key_id")
.notNull()
.references(() => mcpApiKeys.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
toolName: text("tool_name").notNull(),
success: integer("success", { mode: "boolean" }).notNull(),
errorMessage: text("error_message"),
durationMs: integer("duration_ms").notNull(),
createdAt: text("created_at").notNull(),
})
export type McpApiKey = typeof mcpApiKeys.$inferSelect
export type NewMcpApiKey = typeof mcpApiKeys.$inferInsert
export type McpUsage = typeof mcpUsage.$inferSelect
export type NewMcpUsage = typeof mcpUsage.$inferInsert

View File

@ -1,9 +1,13 @@
"use client" "use client"
import { useEffect, useRef } from "react" import { useEffect, useMemo, useRef } from "react"
import { usePathname, useRouter } from "next/navigation" import { usePathname, useRouter } from "next/navigation"
import { useChat } from "@ai-sdk/react" import { useChat } from "@ai-sdk/react"
import { DefaultChatTransport, type UIMessage } from "ai" import {
DefaultChatTransport,
type ChatTransport,
type UIMessage,
} from "ai"
import { toast } from "sonner" import { toast } from "sonner"
import { import {
initializeActionHandlers, initializeActionHandlers,
@ -18,6 +22,33 @@ interface UseCompassChatOptions {
messages: ReadonlyArray<UIMessage> messages: ReadonlyArray<UIMessage>
}) => void | Promise<void> }) => void | Promise<void>
readonly openPanel?: () => void readonly openPanel?: () => void
readonly bridgeTransport?:
| ChatTransport<UIMessage>
| null
}
// useChat captures transport at init -- this wrapper
// delegates at send-time so bridge/default swaps work
class DynamicTransport
implements ChatTransport<UIMessage>
{
private resolve: () => ChatTransport<UIMessage>
constructor(
resolve: () => ChatTransport<UIMessage>
) {
this.resolve = resolve
}
sendMessages: ChatTransport<UIMessage>["sendMessages"] =
(options) => {
return this.resolve().sendMessages(options)
}
reconnectToStream: ChatTransport<UIMessage>["reconnectToStream"] =
async (options) => {
return this.resolve().reconnectToStream(options)
}
} }
export function useCompassChat(options?: UseCompassChatOptions) { export function useCompassChat(options?: UseCompassChatOptions) {
@ -31,17 +62,48 @@ export function useCompassChat(options?: UseCompassChatOptions) {
const dispatchedRef = useRef(new Set<string>()) const dispatchedRef = useRef(new Set<string>())
const bridgeRef = useRef(options?.bridgeTransport)
bridgeRef.current = options?.bridgeTransport
const defaultTransport = useMemo(
() =>
new DefaultChatTransport({
api: "/api/agent",
headers: {
"x-current-page": pathname,
"x-timezone":
Intl.DateTimeFormat().resolvedOptions()
.timeZone,
"x-conversation-id":
options?.conversationId ?? "",
},
}),
[pathname, options?.conversationId]
)
const defaultRef = useRef(defaultTransport)
defaultRef.current = defaultTransport
// stable transport -- delegates at send-time
const transport = useMemo(
() =>
new DynamicTransport(() => {
if (bridgeRef.current) {
console.log(
"[chat] routing → bridge transport"
)
return bridgeRef.current
}
console.log(
"[chat] routing → default transport"
)
return defaultRef.current
}),
[]
)
const chatState = useChat({ const chatState = useChat({
transport: new DefaultChatTransport({ transport,
api: "/api/agent",
headers: {
"x-current-page": pathname,
"x-timezone":
Intl.DateTimeFormat().resolvedOptions().timeZone,
"x-conversation-id":
options?.conversationId ?? "",
},
}),
onFinish: options?.onFinish, onFinish: options?.onFinish,
onError: (err) => { onError: (err) => {
toast.error(err.message) toast.error(err.message)

View File

@ -0,0 +1,175 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
// the ws-transport module is "use client" and relies on
// browser globals (WebSocket, localStorage, window).
// we test what we can: the detectBridge timeout logic
// and the constructor / getApiKey behavior via mocks.
describe("WebSocketChatTransport", () => {
beforeEach(() => {
vi.restoreAllMocks()
})
it("can be imported without throwing", async () => {
// mock WebSocket globally so the module loads
vi.stubGlobal(
"WebSocket",
class {
static OPEN = 1
readyState = 0
close = vi.fn()
send = vi.fn()
onopen: (() => void) | null = null
onmessage: ((e: unknown) => void) | null = null
onerror: (() => void) | null = null
onclose: (() => void) | null = null
addEventListener = vi.fn()
removeEventListener = vi.fn()
},
)
const mod = await import("../ws-transport")
expect(mod.WebSocketChatTransport).toBeDefined()
expect(mod.BRIDGE_PORT).toBe(18789)
})
it("getApiKey returns null when window is undefined", async () => {
// simulate server-side: no window
const originalWindow = globalThis.window
// @ts-expect-error intentionally removing window
delete globalThis.window
vi.stubGlobal(
"WebSocket",
class {
static OPEN = 1
readyState = 0
close = vi.fn()
send = vi.fn()
onopen: (() => void) | null = null
onmessage: ((e: unknown) => void) | null = null
onerror: (() => void) | null = null
onclose: (() => void) | null = null
addEventListener = vi.fn()
removeEventListener = vi.fn()
},
)
// re-import fresh
vi.resetModules()
const { WebSocketChatTransport } = await import(
"../ws-transport"
)
const transport = new WebSocketChatTransport()
// ensureConnected should reject because getApiKey
// returns null
await expect(
(transport as unknown as {
ensureConnected: () => Promise<void>
}).ensureConnected(),
).rejects.toThrow("no bridge API key configured")
// restore window
globalThis.window = originalWindow
})
})
describe("detectBridge", () => {
beforeEach(() => {
vi.restoreAllMocks()
vi.useFakeTimers()
})
it("resolves false when WebSocket errors", async () => {
vi.stubGlobal(
"WebSocket",
class {
close = vi.fn()
onerror: (() => void) | null = null
onopen: (() => void) | null = null
constructor() {
// fire error on next tick
setTimeout(() => {
if (this.onerror) this.onerror()
}, 0)
}
},
)
vi.resetModules()
const { detectBridge } = await import("../ws-transport")
const promise = detectBridge("ws://localhost:9999")
await vi.advanceTimersByTimeAsync(100)
const result = await promise
expect(result).toBe(false)
vi.useRealTimers()
})
it("resolves true when WebSocket connects", async () => {
vi.stubGlobal(
"WebSocket",
class {
close = vi.fn()
onerror: (() => void) | null = null
onopen: (() => void) | null = null
constructor() {
setTimeout(() => {
if (this.onopen) this.onopen()
}, 0)
}
},
)
vi.resetModules()
const { detectBridge } = await import("../ws-transport")
const promise = detectBridge("ws://localhost:18789")
await vi.advanceTimersByTimeAsync(100)
const result = await promise
expect(result).toBe(true)
vi.useRealTimers()
})
it("resolves false on connect timeout", async () => {
vi.stubGlobal(
"WebSocket",
class {
close = vi.fn()
onerror: (() => void) | null = null
onopen: (() => void) | null = null
// never fires onopen or onerror
},
)
vi.resetModules()
const { detectBridge } = await import("../ws-transport")
const promise = detectBridge("ws://localhost:18789")
// advance past the 3000ms CONNECT_TIMEOUT
await vi.advanceTimersByTimeAsync(3500)
const result = await promise
expect(result).toBe(false)
vi.useRealTimers()
})
it("resolves false if WebSocket constructor throws", async () => {
vi.stubGlobal("WebSocket", class {
constructor() {
throw new Error("WebSocket not supported")
}
})
vi.resetModules()
const { detectBridge } = await import("../ws-transport")
const result = await detectBridge("ws://localhost:18789")
expect(result).toBe(false)
vi.useRealTimers()
})
})

View File

@ -0,0 +1,64 @@
// module-level bridge state store
// works outside React context (like model-dropdown's shared state)
// both ChatProvider and settings tab read/write from here
interface BridgeState {
readonly connected: boolean
readonly enabled: boolean
}
let state: BridgeState = {
connected: false,
enabled: false,
}
const listeners = new Set<() => void>()
export function getBridgeSnapshot(): BridgeState {
return state
}
export function subscribeBridge(
listener: () => void
): () => void {
listeners.add(listener)
return () => {
listeners.delete(listener)
}
}
function notify(): void {
for (const l of listeners) l()
}
export function setBridgeConnected(
connected: boolean
): void {
if (state.connected === connected) return
state = { ...state, connected }
notify()
}
export function setBridgeEnabled(
enabled: boolean
): void {
if (state.enabled === enabled) return
state = { ...state, enabled }
if (typeof window !== "undefined") {
localStorage.setItem(
"compass-bridge-enabled",
String(enabled)
)
}
notify()
}
// initialize from localStorage on module load
if (typeof window !== "undefined") {
const stored = localStorage.getItem(
"compass-bridge-enabled"
)
if (stored === "true") {
state = { ...state, enabled: true }
}
}

View File

@ -0,0 +1,300 @@
"use client"
import type {
ChatTransport,
UIMessage,
UIMessageChunk,
} from "ai"
// bridge protocol message types
type BridgeServerMessage =
| {
readonly type: "auth_ok"
readonly user: {
readonly id: string
readonly name: string
readonly role: string
}
}
| { readonly type: "auth_error"; readonly message: string }
| {
readonly type: "chat.ack"
readonly id: string
readonly runId: string
}
| {
readonly type: "chunk"
readonly runId: string
readonly chunk: UIMessageChunk
}
| { readonly type: "chat.done"; readonly runId: string }
| {
readonly type: "chat.error"
readonly runId: string
readonly error: string
}
| { readonly type: "pong" }
const BRIDGE_PORT = 18789
const DEFAULT_URL = `ws://localhost:${BRIDGE_PORT}`
const AUTH_TIMEOUT = 5000
const CONNECT_TIMEOUT = 3000
function isBridgeServerMessage(
raw: unknown,
): raw is BridgeServerMessage {
return (
typeof raw === "object" &&
raw !== null &&
"type" in raw
)
}
export class WebSocketChatTransport
implements ChatTransport<UIMessage>
{
private ws: WebSocket | null = null
private authenticated = false
private readonly url: string
private connectPromise: Promise<void> | null = null
constructor(url = DEFAULT_URL) {
this.url = url
}
private async ensureConnected(): Promise<void> {
if (
this.ws?.readyState === WebSocket.OPEN &&
this.authenticated
) {
return
}
if (this.connectPromise) return this.connectPromise
this.connectPromise = new Promise<void>(
(resolve, reject) => {
const ws = new WebSocket(this.url)
let authResolved = false
const timeout = setTimeout(() => {
if (!authResolved) {
authResolved = true
ws.close()
this.connectPromise = null
reject(new Error("bridge auth timeout"))
}
}, AUTH_TIMEOUT)
// daemon auto-authenticates on connect --
// just wait for auth_ok message
ws.onmessage = (event) => {
if (authResolved) return
const raw: unknown = JSON.parse(
String(event.data),
)
if (!isBridgeServerMessage(raw)) return
const msg = raw
if (msg.type === "auth_ok") {
authResolved = true
clearTimeout(timeout)
this.ws = ws
this.authenticated = true
this.connectPromise = null
resolve()
} else if (msg.type === "auth_error") {
authResolved = true
clearTimeout(timeout)
ws.close()
this.connectPromise = null
reject(new Error(msg.message))
}
}
ws.onerror = () => {
if (!authResolved) {
authResolved = true
clearTimeout(timeout)
this.connectPromise = null
reject(
new Error("bridge connection failed")
)
}
}
ws.onclose = () => {
this.ws = null
this.authenticated = false
if (!authResolved) {
authResolved = true
clearTimeout(timeout)
this.connectPromise = null
reject(
new Error("bridge connection closed")
)
}
}
}
)
return this.connectPromise
}
sendMessages: ChatTransport<UIMessage>["sendMessages"] =
async (options) => {
await this.ensureConnected()
const ws = this.ws
if (!ws) throw new Error("bridge not connected")
const messageId = crypto.randomUUID()
// read bridge model preference from localStorage
const bridgeModel =
typeof window !== "undefined"
? localStorage.getItem("compass-bridge-model")
: null
console.log(
"[bridge] sending message via WebSocket transport",
{ model: bridgeModel }
)
ws.send(
JSON.stringify({
type: "chat.send",
id: messageId,
trigger: options.trigger,
messages: options.messages,
model: bridgeModel ?? undefined,
context: {
currentPage:
options.headers instanceof Headers
? options.headers.get("x-current-page")
: options.headers?.["x-current-page"] ??
"/dashboard",
timezone:
options.headers instanceof Headers
? options.headers.get("x-timezone")
: options.headers?.["x-timezone"] ??
"UTC",
conversationId: options.chatId,
},
})
)
let currentRunId: string | null = null
return new ReadableStream<UIMessageChunk>({
start: (controller) => {
const onMessage = (event: MessageEvent) => {
const raw: unknown = JSON.parse(
String(event.data),
)
if (!isBridgeServerMessage(raw)) return
const msg = raw
switch (msg.type) {
case "chat.ack":
currentRunId = msg.runId
break
case "chunk":
if (msg.runId === currentRunId) {
controller.enqueue(msg.chunk)
}
break
case "chat.done":
if (msg.runId === currentRunId) {
ws.removeEventListener(
"message",
onMessage
)
controller.close()
}
break
case "chat.error":
if (msg.runId === currentRunId) {
ws.removeEventListener(
"message",
onMessage
)
controller.error(
new Error(msg.error)
)
}
break
default:
break
}
}
ws.addEventListener("message", onMessage)
if (options.abortSignal) {
options.abortSignal.addEventListener(
"abort",
() => {
if (currentRunId) {
ws.send(
JSON.stringify({
type: "chat.abort",
runId: currentRunId,
})
)
}
ws.removeEventListener(
"message",
onMessage
)
controller.close()
},
{ once: true }
)
}
},
})
}
reconnectToStream: ChatTransport<UIMessage>["reconnectToStream"] =
async () => {
return null
}
disconnect(): void {
if (this.ws) {
this.ws.close()
this.ws = null
this.authenticated = false
}
}
}
// detect if bridge daemon is running
export async function detectBridge(
url = DEFAULT_URL
): Promise<boolean> {
return new Promise((resolve) => {
try {
const ws = new WebSocket(url)
const timer = setTimeout(() => {
ws.close()
resolve(false)
}, CONNECT_TIMEOUT)
ws.onopen = () => {
clearTimeout(timer)
ws.close()
resolve(true)
}
ws.onerror = () => {
clearTimeout(timer)
resolve(false)
}
} catch {
resolve(false)
}
})
}
export { BRIDGE_PORT }

View File

@ -0,0 +1,160 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { generateApiKey, hashApiKey, validateApiKey } from "../auth"
describe("generateApiKey", () => {
it("produces a key with ck_ prefix", () => {
const { key } = generateApiKey()
expect(key.startsWith("ck_")).toBe(true)
})
it("produces a key of 43 characters (3 prefix + 40 hex)", () => {
const { key } = generateApiKey()
expect(key.length).toBe(43)
})
it("returns a keyPrefix of first 8 chars", () => {
const { key, keyPrefix } = generateApiKey()
expect(keyPrefix).toBe(key.slice(0, 8))
expect(keyPrefix.length).toBe(8)
})
it("generates unique keys each call", () => {
const a = generateApiKey()
const b = generateApiKey()
expect(a.key).not.toBe(b.key)
})
it("key body is valid hex", () => {
const { key } = generateApiKey()
const hex = key.slice(3) // strip ck_
expect(hex).toMatch(/^[0-9a-f]{40}$/)
})
})
describe("hashApiKey", () => {
it("returns a 64-char hex string (SHA-256)", async () => {
const hash = await hashApiKey("ck_test1234")
expect(hash.length).toBe(64)
expect(hash).toMatch(/^[0-9a-f]{64}$/)
})
it("returns consistent output for the same input", async () => {
const a = await hashApiKey("ck_abc123")
const b = await hashApiKey("ck_abc123")
expect(a).toBe(b)
})
it("returns different output for different inputs", async () => {
const a = await hashApiKey("ck_key_one")
const b = await hashApiKey("ck_key_two")
expect(a).not.toBe(b)
})
})
describe("validateApiKey", () => {
function createMockDb(row: Record<string, unknown> | undefined) {
const chainMock = {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
get: vi.fn().mockResolvedValue(row),
}
const updateChain = {
set: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
run: vi.fn().mockResolvedValue(undefined),
}
return {
select: vi.fn().mockReturnValue(chainMock),
update: vi.fn().mockReturnValue(updateChain),
_chainMock: chainMock,
_updateChain: updateChain,
} as unknown as Parameters<typeof validateApiKey>[0]
}
it("returns invalid for a key not found in DB", async () => {
const db = createMockDb(undefined)
const result = await validateApiKey(db, "ck_nonexistent")
expect(result.valid).toBe(false)
if (!result.valid) {
expect(result.error).toBe("Invalid API key")
}
})
it("returns valid for an active, non-expired key", async () => {
const db = createMockDb({
id: "key-1",
userId: "user-1",
scopes: '["read","write"]',
isActive: true,
expiresAt: null,
})
const result = await validateApiKey(db, "ck_validkey")
expect(result.valid).toBe(true)
if (result.valid) {
expect(result.userId).toBe("user-1")
expect(result.scopes).toEqual(["read", "write"])
expect(result.keyId).toBe("key-1")
}
})
it("returns expired for an expired key", async () => {
const pastDate = new Date(
Date.now() - 86400000
).toISOString()
const db = createMockDb({
id: "key-2",
userId: "user-2",
scopes: '["read"]',
isActive: true,
expiresAt: pastDate,
})
const result = await validateApiKey(db, "ck_expiredkey")
expect(result.valid).toBe(false)
if (!result.valid) {
expect(result.error).toBe("API key expired")
}
})
it("returns valid for a key with future expiry", async () => {
const futureDate = new Date(
Date.now() + 86400000
).toISOString()
const db = createMockDb({
id: "key-3",
userId: "user-3",
scopes: '["admin"]',
isActive: true,
expiresAt: futureDate,
})
const result = await validateApiKey(db, "ck_futurekey")
expect(result.valid).toBe(true)
if (result.valid) {
expect(result.userId).toBe("user-3")
expect(result.scopes).toEqual(["admin"])
}
})
it("fires a last-used-at update on valid key", async () => {
const db = createMockDb({
id: "key-4",
userId: "user-4",
scopes: '["read"]',
isActive: true,
expiresAt: null,
})
await validateApiKey(db, "ck_trackusage")
const mockDb = db as unknown as {
update: ReturnType<typeof vi.fn>
}
expect(mockDb.update).toHaveBeenCalled()
})
})

View File

@ -0,0 +1,219 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
// mock all heavy external deps before importing the module
vi.mock("@opennextjs/cloudflare", () => ({
getCloudflareContext: vi
.fn()
.mockResolvedValue({ env: { DB: {} } }),
}))
vi.mock("@/db", () => ({
getDb: vi.fn().mockReturnValue({
query: {},
}),
}))
vi.mock("@/lib/agent/memory", () => ({
saveMemory: vi.fn(),
searchMemories: vi.fn().mockResolvedValue([]),
}))
vi.mock("@/app/actions/plugins", () => ({
installSkill: vi.fn(),
uninstallSkill: vi.fn(),
toggleSkill: vi.fn(),
getInstalledSkills: vi.fn().mockResolvedValue([]),
}))
vi.mock("@/app/actions/themes", () => ({
getCustomThemes: vi
.fn()
.mockResolvedValue({ success: true, data: [] }),
setUserThemePreference: vi
.fn()
.mockResolvedValue({ success: true }),
}))
vi.mock("@/app/actions/dashboards", () => ({
getCustomDashboards: vi
.fn()
.mockResolvedValue({ success: true, data: [] }),
}))
vi.mock("@/lib/theme/presets", () => ({
THEME_PRESETS: [
{
id: "default",
name: "Default",
description: "Default theme",
},
],
}))
import {
executeBridgeTool,
getAvailableTools,
} from "../tool-adapter"
describe("executeBridgeTool", () => {
it("returns error for unknown tool", async () => {
const result = await executeBridgeTool(
"nonexistentTool",
"user-1",
"member",
{},
["read"],
)
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error).toContain("unknown tool")
}
})
it("read scope cannot access write tools", async () => {
const result = await executeBridgeTool(
"rememberContext",
"user-1",
"member",
{ content: "test", memoryType: "note" },
["read"],
)
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error).toContain("insufficient scope")
expect(result.error).toContain('"write"')
}
})
it("read scope cannot access admin tools", async () => {
const result = await executeBridgeTool(
"installSkill",
"user-1",
"admin",
{ source: "github:foo/bar" },
["read"],
)
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error).toContain("insufficient scope")
}
})
it("write scope cannot access admin tools", async () => {
const result = await executeBridgeTool(
"installSkill",
"user-1",
"admin",
{ source: "github:foo/bar" },
["write"],
)
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error).toContain("insufficient scope")
}
})
it("write scope can access read tools", async () => {
const result = await executeBridgeTool(
"listThemes",
"user-1",
"member",
{},
["write"],
)
expect(result.success).toBe(true)
})
it("admin scope can access read tools", async () => {
const result = await executeBridgeTool(
"listThemes",
"user-1",
"member",
{},
["admin"],
)
expect(result.success).toBe(true)
})
it("admin scope can access write tools", async () => {
const result = await executeBridgeTool(
"setTheme",
"user-1",
"member",
{ themeId: "default" },
["admin"],
)
// setTheme calls a mocked function, should succeed
expect(result.success).toBe(true)
})
it("catches handler errors and returns failure", async () => {
// recallMemory calls searchMemories which we can make throw
const { searchMemories } = await import(
"@/lib/agent/memory"
)
vi.mocked(searchMemories).mockRejectedValueOnce(
new Error("db connection lost"),
)
const result = await executeBridgeTool(
"recallMemory",
"user-1",
"member",
{ query: "test" },
["read"],
)
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error).toBe("db connection lost")
}
})
})
describe("getAvailableTools", () => {
it("returns only read tools for read scope", () => {
const tools = getAvailableTools(["read"])
const scopes = tools.map((t) => t.scope)
expect(scopes.every((s) => s === "read")).toBe(true)
expect(tools.length).toBeGreaterThan(0)
})
it("returns read + write tools for write scope", () => {
const tools = getAvailableTools(["write"])
const scopes = new Set(tools.map((t) => t.scope))
expect(scopes.has("read")).toBe(true)
expect(scopes.has("write")).toBe(true)
expect(scopes.has("admin")).toBe(false)
})
it("returns all tools for admin scope", () => {
const tools = getAvailableTools(["admin"])
const scopes = new Set(tools.map((t) => t.scope))
expect(scopes.has("read")).toBe(true)
expect(scopes.has("write")).toBe(true)
expect(scopes.has("admin")).toBe(true)
})
it("admin scope returns more tools than read scope", () => {
const readTools = getAvailableTools(["read"])
const adminTools = getAvailableTools(["admin"])
expect(adminTools.length).toBeGreaterThan(
readTools.length,
)
})
it("every tool has name, description, scope", () => {
const tools = getAvailableTools(["admin"])
for (const tool of tools) {
expect(tool.name).toBeTruthy()
expect(tool.description).toBeTruthy()
expect(
["read", "write", "admin"].includes(tool.scope),
).toBe(true)
}
})
it("returns empty array for empty scopes", () => {
const tools = getAvailableTools([])
expect(tools.length).toBe(0)
})
it("returns empty array for invalid scope", () => {
const tools = getAvailableTools(["bogus"])
expect(tools.length).toBe(0)
})
})

134
src/lib/mcp/auth.ts Normal file
View File

@ -0,0 +1,134 @@
import { eq, and, gte, sql } from "drizzle-orm"
import type { DrizzleD1Database } from "drizzle-orm/d1"
import { mcpApiKeys, mcpUsage } from "@/db/schema-mcp"
const KEY_PREFIX = "ck_"
const KEY_RANDOM_BYTES = 20 // 20 bytes = 40 hex chars
export type ValidKeyResult =
| Readonly<{
valid: true
userId: string
scopes: ReadonlyArray<string>
keyId: string
}>
| Readonly<{
valid: false
error: string
}>
function toHex(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer)
let hex = ""
for (const b of bytes) {
hex += b.toString(16).padStart(2, "0")
}
return hex
}
export async function hashApiKey(
key: string
): Promise<string> {
const encoded = new TextEncoder().encode(key)
const digest = await crypto.subtle.digest(
"SHA-256",
encoded
)
return toHex(digest)
}
export function generateApiKey(): Readonly<{
key: string
keyPrefix: string
}> {
const randomBytes = new Uint8Array(KEY_RANDOM_BYTES)
crypto.getRandomValues(randomBytes)
const hex = toHex(randomBytes.buffer)
const key = `${KEY_PREFIX}${hex}`
// prefix = first 8 chars for identification
const keyPrefix = key.slice(0, 8)
return { key, keyPrefix }
}
export async function validateApiKey(
db: DrizzleD1Database<Record<string, unknown>>,
rawKey: string
): Promise<ValidKeyResult> {
const hash = await hashApiKey(rawKey)
const row = await db
.select()
.from(mcpApiKeys)
.where(
and(
eq(mcpApiKeys.keyHash, hash),
eq(mcpApiKeys.isActive, true)
)
)
.get()
if (!row) {
return { valid: false, error: "Invalid API key" }
}
// check expiry
if (row.expiresAt) {
const now = new Date()
const expiry = new Date(row.expiresAt)
if (now > expiry) {
return { valid: false, error: "API key expired" }
}
}
// update last used timestamp (fire-and-forget)
const now = new Date().toISOString()
db.update(mcpApiKeys)
.set({ lastUsedAt: now })
.where(eq(mcpApiKeys.id, row.id))
.run()
.catch(() => {
// non-critical: best-effort timestamp update
})
let scopes: ReadonlyArray<string>
try {
const parsed: unknown = JSON.parse(row.scopes)
if (!Array.isArray(parsed)) {
return { valid: false, error: "invalid key data" }
}
scopes = parsed.filter(
(s): s is string => typeof s === "string",
)
} catch {
return { valid: false, error: "invalid key data" }
}
return {
valid: true,
userId: row.userId,
scopes,
keyId: row.id,
}
}
export async function checkRateLimit(
db: DrizzleD1Database<Record<string, unknown>>,
keyId: string,
windowMs = 60_000,
maxRequests = 100,
): Promise<boolean> {
const since = new Date(
Date.now() - windowMs,
).toISOString()
const rows = await db
.select({ count: sql<number>`count(*)` })
.from(mcpUsage)
.where(
and(
eq(mcpUsage.apiKeyId, keyId),
gte(mcpUsage.createdAt, since),
),
)
.get()
return (rows?.count ?? 0) < maxRequests
}

414
src/lib/mcp/tool-adapter.ts Normal file
View File

@ -0,0 +1,414 @@
import { getCloudflareContext } from "@opennextjs/cloudflare"
import { getDb } from "@/db"
import { saveMemory, searchMemories } from "@/lib/agent/memory"
import {
installSkill as installSkillAction,
uninstallSkill as uninstallSkillAction,
toggleSkill as toggleSkillAction,
getInstalledSkills as getInstalledSkillsAction,
} from "@/app/actions/plugins"
import {
getCustomThemes,
setUserThemePreference,
} from "@/app/actions/themes"
import {
getCustomDashboards,
} from "@/app/actions/dashboards"
import { THEME_PRESETS } from "@/lib/theme/presets"
import type { BridgeToolScope, BridgeToolMeta } from "@/lib/mcp/types"
function getString(
args: Record<string, unknown>,
key: string,
): string | undefined {
const v = args[key]
return typeof v === "string" ? v : undefined
}
function getNumber(
args: Record<string, unknown>,
key: string,
): number | undefined {
const v = args[key]
return typeof v === "number" ? v : undefined
}
function getBoolean(
args: Record<string, unknown>,
key: string,
): boolean | undefined {
const v = args[key]
return typeof v === "boolean" ? v : undefined
}
type BridgeToolHandler = (
userId: string,
userRole: string,
args: Record<string, unknown>,
) => Promise<unknown>
type BridgeToolEntry = Readonly<{
handler: BridgeToolHandler
scope: BridgeToolScope
description: string
}>
const bridgeToolRegistry: Readonly<
Record<string, BridgeToolEntry>
> = {
queryData: {
scope: "read",
description:
"Query the application database by type " +
"(customers, vendors, projects, etc.)",
handler: async (_userId, _userRole, args) => {
const { env } = await getCloudflareContext()
const db = getDb(env.DB)
const queryType = getString(args, "queryType")
if (!queryType) return { error: "queryType required" }
const id = getString(args, "id")
const search = getString(args, "search")
const cap = getNumber(args, "limit") ?? 20
switch (queryType) {
case "customers": {
const rows = await db.query.customers.findMany({
limit: cap,
...(search
? {
where: (c, { like }) =>
like(c.name, `%${search}%`),
}
: {}),
})
return { data: rows, count: rows.length }
}
case "vendors": {
const rows = await db.query.vendors.findMany({
limit: cap,
...(search
? {
where: (v, { like }) =>
like(v.name, `%${search}%`),
}
: {}),
})
return { data: rows, count: rows.length }
}
case "projects": {
const rows = await db.query.projects.findMany({
limit: cap,
...(search
? {
where: (p, { like }) =>
like(p.name, `%${search}%`),
}
: {}),
})
return { data: rows, count: rows.length }
}
case "invoices": {
const rows = await db.query.invoices.findMany({
limit: cap,
})
return { data: rows, count: rows.length }
}
case "vendor_bills": {
const rows = await db.query.vendorBills.findMany({
limit: cap,
})
return { data: rows, count: rows.length }
}
case "schedule_tasks": {
const rows =
await db.query.scheduleTasks.findMany({
limit: cap,
...(search
? {
where: (t, { like }) =>
like(t.title, `%${search}%`),
}
: {}),
})
return { data: rows, count: rows.length }
}
case "project_detail": {
if (!id) {
return { error: "id required for detail query" }
}
const row =
await db.query.projects.findFirst({
where: (p, { eq }) => eq(p.id, id),
})
return row ? { data: row } : { error: "not found" }
}
case "customer_detail": {
if (!id) {
return { error: "id required for detail query" }
}
const row =
await db.query.customers.findFirst({
where: (c, { eq }) => eq(c.id, id),
})
return row ? { data: row } : { error: "not found" }
}
case "vendor_detail": {
if (!id) {
return { error: "id required for detail query" }
}
const row = await db.query.vendors.findFirst({
where: (v, { eq }) => eq(v.id, id),
})
return row ? { data: row } : { error: "not found" }
}
default:
return { error: "unknown query type" }
}
},
},
recallMemory: {
scope: "read",
description:
"Search persistent memories for the user",
handler: async (userId, _userRole, args) => {
const { env } = await getCloudflareContext()
const db = getDb(env.DB)
const query = getString(args, "query")
if (!query) return { error: "query required" }
const limit = getNumber(args, "limit") ?? 5
const results = await searchMemories(
db,
userId,
query,
limit,
)
return {
action: "memory_recall",
results,
count: results.length,
}
},
},
listThemes: {
scope: "read",
description:
"List available visual themes (presets + custom)",
handler: async () => {
const presets = THEME_PRESETS.map((p) => ({
id: p.id,
name: p.name,
description: p.description,
isPreset: true,
}))
const customResult = await getCustomThemes()
const customs = customResult.success
? customResult.data.map((c) => ({
id: c.id,
name: c.name,
description: c.description,
isPreset: false,
}))
: []
return { themes: [...presets, ...customs] }
},
},
listDashboards: {
scope: "read",
description: "List user's saved custom dashboards",
handler: async () => {
const result = await getCustomDashboards()
if (!result.success) return { error: result.error }
return {
dashboards: result.data,
count: result.data.length,
}
},
},
listInstalledSkills: {
scope: "read",
description:
"List all installed agent skills with status",
handler: async () => getInstalledSkillsAction(),
},
rememberContext: {
scope: "write",
description:
"Save something to persistent memory",
handler: async (userId, _userRole, args) => {
const { env } = await getCloudflareContext()
const db = getDb(env.DB)
const content = getString(args, "content")
if (!content) return { error: "content required" }
const memoryType = getString(args, "memoryType")
if (!memoryType) return { error: "memoryType required" }
const tags = getString(args, "tags")
const importance = getNumber(args, "importance")
const id = await saveMemory(
db,
userId,
content,
memoryType,
tags,
importance,
)
return {
action: "memory_saved",
id,
content,
memoryType,
}
},
},
setTheme: {
scope: "write",
description: "Switch the user's visual theme",
handler: async (_userId, _userRole, args) => {
const themeId = getString(args, "themeId")
if (!themeId) return { error: "themeId required" }
const result = await setUserThemePreference(themeId)
if (!result.success) return { error: result.error }
return {
action: "apply_theme",
themeId,
}
},
},
installSkill: {
scope: "admin",
description:
"Install a skill from GitHub (admin only)",
handler: async (_userId, userRole, args) => {
if (userRole !== "admin") {
return {
error: "admin role required to install skills",
}
}
const source = getString(args, "source")
if (!source) return { error: "source required" }
return installSkillAction(source)
},
},
uninstallSkill: {
scope: "admin",
description:
"Remove an installed skill (admin only)",
handler: async (_userId, userRole, args) => {
if (userRole !== "admin") {
return { error: "admin role required" }
}
const pluginId = getString(args, "pluginId")
if (!pluginId) return { error: "pluginId required" }
return uninstallSkillAction(pluginId)
},
},
toggleInstalledSkill: {
scope: "admin",
description:
"Enable or disable an installed skill (admin only)",
handler: async (_userId, userRole, args) => {
if (userRole !== "admin") {
return { error: "admin role required" }
}
const pluginId = getString(args, "pluginId")
if (!pluginId) return { error: "pluginId required" }
const enabled = getBoolean(args, "enabled")
if (enabled === undefined) {
return { error: "enabled required" }
}
return toggleSkillAction(pluginId, enabled)
},
},
}
const SCOPE_HIERARCHY: Readonly<
Record<BridgeToolScope, ReadonlyArray<BridgeToolScope>>
> = {
read: ["read"],
write: ["read", "write"],
admin: ["read", "write", "admin"],
}
function hasScope(
userScopes: ReadonlyArray<string>,
requiredScope: BridgeToolScope,
): boolean {
return userScopes.some((s) => {
const allowed =
SCOPE_HIERARCHY[s as BridgeToolScope]
return allowed !== undefined &&
allowed.includes(requiredScope)
})
}
export async function executeBridgeTool(
toolName: string,
userId: string,
userRole: string,
args: Record<string, unknown>,
scopes: ReadonlyArray<string>,
): Promise<
| { readonly success: true; readonly result: unknown }
| { readonly success: false; readonly error: string }
> {
const entry = bridgeToolRegistry[toolName]
if (!entry) {
return {
success: false,
error: `unknown tool: ${toolName}`,
}
}
if (!hasScope(scopes, entry.scope)) {
return {
success: false,
error:
`insufficient scope: "${entry.scope}" ` +
`required for tool "${toolName}"`,
}
}
try {
const result = await entry.handler(
userId,
userRole,
args,
)
return { success: true, result }
} catch (err) {
const message =
err instanceof Error ? err.message : "tool failed"
return { success: false, error: message }
}
}
export function getAvailableTools(
scopes: ReadonlyArray<string>,
): ReadonlyArray<BridgeToolMeta> {
const tools: Array<BridgeToolMeta> = []
for (const [name, entry] of Object.entries(
bridgeToolRegistry,
)) {
if (hasScope(scopes, entry.scope)) {
tools.push({
name,
description: entry.description,
scope: entry.scope,
})
}
}
return tools
}

142
src/lib/mcp/types.ts Normal file
View File

@ -0,0 +1,142 @@
// Wire protocol types for the MCP bridge WebSocket connection
// --- Client → Server messages ---
export type BridgeAuthMessage = Readonly<{
type: "auth"
compassUrl: string
apiKey: string
}>
export type BridgeChatSend = Readonly<{
type: "chat.send"
id: string
messages: ReadonlyArray<unknown>
context: Readonly<{
currentPage: string
timezone: string
conversationId: string
}>
}>
export type BridgeChatAbort = Readonly<{
type: "chat.abort"
runId: string
}>
export type BridgePing = Readonly<{
type: "ping"
}>
// --- Server → Client messages ---
export type BridgeAuthOk = Readonly<{
type: "auth_ok"
user: Readonly<{
id: string
name: string
role: string
}>
}>
export type BridgeAuthError = Readonly<{
type: "auth_error"
message: string
}>
export type BridgeChatAck = Readonly<{
type: "chat.ack"
id: string
runId: string
}>
export type BridgeChunk = Readonly<{
type: "chunk"
runId: string
chunk: unknown
}>
export type BridgeChatDone = Readonly<{
type: "chat.done"
runId: string
}>
export type BridgeChatError = Readonly<{
type: "chat.error"
runId: string
error: string
}>
export type BridgePong = Readonly<{
type: "pong"
}>
// --- Unions ---
export type BridgeClientMessage =
| BridgeAuthMessage
| BridgeChatSend
| BridgeChatAbort
| BridgePing
export type BridgeServerMessage =
| BridgeAuthOk
| BridgeAuthError
| BridgeChatAck
| BridgeChunk
| BridgeChatDone
| BridgeChatError
| BridgePong
export type BridgeMessage =
| BridgeClientMessage
| BridgeServerMessage
// --- Tool types ---
export const BRIDGE_TOOL_SCOPES = [
"read",
"write",
"admin",
] as const
export type BridgeToolScope =
(typeof BRIDGE_TOOL_SCOPES)[number]
export type BridgeToolRequest = Readonly<{
tool: string
args: Record<string, unknown>
}>
export type BridgeToolResponse = Readonly<{
success: boolean
result?: unknown
error?: string
}>
export type BridgeToolMeta = Readonly<{
name: string
description: string
scope: BridgeToolScope
}>
// --- Registration / context responses ---
export type BridgeRegisterResponse = Readonly<{
user: Readonly<{
id: string
name: string
email: string
role: string
}>
tools: ReadonlyArray<BridgeToolMeta>
memories: ReadonlyArray<unknown>
dashboards: ReadonlyArray<unknown>
skills: ReadonlyArray<unknown>
}>
export type BridgeContextResponse = Readonly<{
memories: ReadonlyArray<unknown>
dashboards: ReadonlyArray<unknown>
skills: ReadonlyArray<unknown>
}>

View File

@ -12,10 +12,17 @@ const publicPaths = [
"/callback", "/callback",
] ]
// bridge routes use their own API key auth
const bridgePaths = [
"/api/bridge/register",
"/api/bridge/tools",
"/api/bridge/context",
]
function isPublicPath(pathname: string): boolean { function isPublicPath(pathname: string): boolean {
// exact matches or starts with /api/auth/
return ( return (
publicPaths.includes(pathname) || publicPaths.includes(pathname) ||
bridgePaths.includes(pathname) ||
pathname.startsWith("/api/auth/") || pathname.startsWith("/api/auth/") ||
pathname.startsWith("/api/netsuite/") || pathname.startsWith("/api/netsuite/") ||
pathname.startsWith("/api/google/") pathname.startsWith("/api/google/")

View File

@ -27,5 +27,5 @@
] ]
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", "references"] "exclude": ["node_modules", "references", "packages"]
} }

16
vitest.config.ts Normal file
View File

@ -0,0 +1,16 @@
import { defineConfig } from "vitest/config"
import path from "path"
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
test: {
globals: true,
environment: "node",
include: ["src/**/__tests__/**/*.test.ts"],
exclude: ["node_modules", "references", "packages"],
},
})