Add Cloudflare Workers sandbox runtime for isolated code execution
Unified · Split
| @@ -39,6 +39,15 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json · 38 unmodified lines | |||
| 39 | convex_local_backend.sqlite3 | 39 | convex_local_backend.sqlite3 |
| 40 | convex_local_storage/ | 40 | convex_local_storage/ |
| 41 | 41 | ||
| 42 | # Local OpenAPI sources service state | ||
| 43 | sources/data/*.sqlite | ||
| 44 | sources/data/*.sqlite-shm | ||
| 45 | sources/data/*.sqlite-wal | ||
| 46 | |||
| 47 | # Cloudflare Workers local state | ||
| 48 | .wrangler/ | ||
| 49 | .dev.vars | ||
| 50 | |||
| 42 | # Generated push env file | 51 | # Generated push env file |
| 43 | executor/.env.executor-push | 52 | executor/.env.executor-push |
| 44 | 53 | ||
| @@ -130,6 +130,25 @@ · 129 unmodified lines | |||
| 130 | "name": "@executor/convex", | 130 | "name": "@executor/convex", |
| 131 | "version": "0.0.1", | 131 | "version": "0.0.1", |
| 132 | }, | 132 | }, |
| 133 | "executor/packages/sandbox-host": { | ||
| 134 | "name": "@executor/sandbox-host", | ||
| 135 | "version": "0.1.0", | ||
| 136 | "dependencies": { | ||
| 137 | "better-result": "^2.7.0", | ||
| 138 | }, | ||
| 139 | "devDependencies": { | ||
| 140 | "@cloudflare/workers-types": "^4.20250214.0", | ||
| 141 | "typescript": "^5.9.3", | ||
| 142 | "wrangler": "^4.0.0", | ||
| 143 | }, | ||
| 144 | }, | ||
| 145 | "sources": { | ||
| 146 | "name": "@proto/sources", | ||
| 147 | "devDependencies": { | ||
| 148 | "@types/bun": "latest", | ||
| 149 | "typescript": "^5.9.3", | ||
| 150 | }, | ||
| 151 | }, | ||
| 133 | }, | 152 | }, |
| 134 | "trustedDependencies": [ | 153 | "trustedDependencies": [ |
| 135 | "sharp", | 154 | "sharp", |
| @@ -284,6 +303,26 @@ · 148 unmodified lines | |||
| 284 | 303 | ||
| 285 | "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], | 304 | "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], |
| 286 | 305 | ||
| 306 | "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="], | ||
| 307 | |||
| 308 | "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], | ||
| 309 | |||
| 310 | "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], | ||
| 311 | |||
| 312 | "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.12.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20260115.0" }, "optionalPeers": ["workerd"] }, "sha512-tP/Wi+40aBJovonSNJSsS7aFJY0xjuckKplmzDs2Xat06BJ68B6iG7YDUWXJL8gNn0gqW7YC5WhlYhO3QbugQA=="], | ||
| 313 | |||
| 314 | "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260210.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-e3vMgzr8ZM6VjpJVFrnMBhjvFhlMIkhT+BLpBk3pKaWsrXao+azDlmzzxB3Zf4CZ8LmCEtaP7n5d2mNGL6Dqww=="], | ||
| 315 | |||
| 316 | "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260210.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ng2uLJVMrI5VrcAS26gDGM+qxCuWD4ZA8VR4i88RdyM8TLn+AqPFisrvn7AMA+QSv0+ck+ZdFtXek7qNp2gNuA=="], | ||
| 317 | |||
| 318 | "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260210.0", "", { "os": "linux", "cpu": "x64" }, "sha512-frn2/+6DV59h13JbGSk9ATvJw3uORWssFIKZ/G/to+WRrIDQgCpSrjLtGbFSSn5eBEhYOvwxPKc7IrppkmIj/w=="], | ||
| 319 | |||
| 320 | "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260210.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0fmxEHaDcAF+7gcqnBcQdBCOzNvGz3mTMwqxEYJc5xZgFwQf65/dYK5fnV8z56GVNqu88NEnLMG3DD2G7Ey1vw=="], | ||
| 321 | |||
| 322 | "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260210.0", "", { "os": "win32", "cpu": "x64" }, "sha512-G/Apjk/QLNnwbu8B0JO9FuAJKHNr+gl8X3G/7qaUrpwIkPx5JFQElVE6LKk4teSrycvAy5AzLFAL0lOB1xsUIQ=="], | ||
| 323 | |||
| 324 | "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260210.0", "", {}, "sha512-zHaF0RZVYUQwNCJCECnNAJdMur72Lk3FMiD6wU78Dx3Bv7DQRcuXNmPNuJmsGnosVZCcWintHlPTQ/4BEiDG5w=="], | ||
| 325 | |||
| 287 | "@convex-dev/migrations": ["@convex-dev/migrations@0.3.1", "", { "peerDependencies": { "convex": "^1.24.8" } }, "sha512-dGiN82vWMN415/AGMcVooDOJk8iTousZahBBs27uA03pL74Uo3ksucUW3CiCfPx75rv+x9UnenEakimQYYNIrA=="], | 326 | "@convex-dev/migrations": ["@convex-dev/migrations@0.3.1", "", { "peerDependencies": { "convex": "^1.24.8" } }, "sha512-dGiN82vWMN415/AGMcVooDOJk8iTousZahBBs27uA03pL74Uo3ksucUW3CiCfPx75rv+x9UnenEakimQYYNIrA=="], |
| 288 | 327 | ||
| 289 | "@convex-dev/stripe": ["@convex-dev/stripe@0.1.3", "", { "dependencies": { "stripe": "^20.0.0" }, "peerDependencies": { "convex": "^1.29.3", "react": "^18.3.1 || ^19.0.0" } }, "sha512-yL0ygrWrjto9f7rprCb4KYp+p3Oka7KG5e3HBoJ9qSzbncpavZI+JTkGZnLCYZjPOo2ByCrBJvtGAxfP169Nzw=="], | 328 | "@convex-dev/stripe": ["@convex-dev/stripe@0.1.3", "", { "dependencies": { "stripe": "^20.0.0" }, "peerDependencies": { "convex": "^1.29.3", "react": "^18.3.1 || ^19.0.0" } }, "sha512-yL0ygrWrjto9f7rprCb4KYp+p3Oka7KG5e3HBoJ9qSzbncpavZI+JTkGZnLCYZjPOo2ByCrBJvtGAxfP169Nzw=="], |
| @@ -294,6 +333,8 @@ · 4 unmodified lines | |||
| 294 | 333 | ||
| 295 | "@convex-dev/workpool": ["@convex-dev/workpool@0.2.19", "", { "peerDependencies": { "convex": ">=1.25.0 <1.35.0", "convex-helpers": "^0.1.94" } }, "sha512-U2KwYnsKILyxW1baWEhDv+ZtnL5FZbYFxBT5owQ0Lw/kseiudMZraA4clH+/6gowHSahWpkq4wndhcOfpfhuOA=="], | 334 | "@convex-dev/workpool": ["@convex-dev/workpool@0.2.19", "", { "peerDependencies": { "convex": ">=1.25.0 <1.35.0", "convex-helpers": "^0.1.94" } }, "sha512-U2KwYnsKILyxW1baWEhDv+ZtnL5FZbYFxBT5owQ0Lw/kseiudMZraA4clH+/6gowHSahWpkq4wndhcOfpfhuOA=="], |
| 296 | 335 | ||
| 336 | "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], | ||
| 337 | |||
| 297 | "@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="], | 338 | "@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="], |
| 298 | 339 | ||
| 299 | "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], | 340 | "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], |
| @@ -400,6 +441,8 @@ · 100 unmodified lines | |||
| 400 | 441 | ||
| 401 | "@executor/convex": ["@executor/convex@workspace:executor/convex"], | 442 | "@executor/convex": ["@executor/convex@workspace:executor/convex"], |
| 402 | 443 | ||
| 444 | "@executor/sandbox-host": ["@executor/sandbox-host@workspace:executor/packages/sandbox-host"], | ||
| 445 | |||
| 403 | "@executor/web": ["@executor/web@workspace:executor/apps/web"], | 446 | "@executor/web": ["@executor/web@workspace:executor/apps/web"], |
| 404 | 447 | ||
| 405 | "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], | 448 | "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], |
| @@ -496,7 +539,7 @@ · 90 unmodified lines | |||
| 496 | 539 | ||
| 497 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], | 540 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], |
| 498 | 541 | ||
| 499 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], | 542 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], |
| 500 | 543 | ||
| 501 | "@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=="], | 544 | "@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=="], |
| 502 | 545 | ||
| @@ -576,6 +619,14 @@ · 73 unmodified lines | |||
| 576 | 619 | ||
| 577 | "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], | 620 | "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], |
| 578 | 621 | ||
| 622 | "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], | ||
| 623 | |||
| 624 | "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], | ||
| 625 | |||
| 626 | "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], | ||
| 627 | |||
| 628 | "@proto/sources": ["@proto/sources@workspace:sources"], | ||
| 629 | |||
| 579 | "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], | 630 | "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], |
| 580 | 631 | ||
| 581 | "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], | 632 | "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], |
| @@ -828,6 +879,8 @@ · 246 unmodified lines | |||
| 828 | 879 | ||
| 829 | "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], | 880 | "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], |
| 830 | 881 | ||
| 882 | "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], | ||
| 883 | |||
| 831 | "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], | 884 | "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], |
| 832 | 885 | ||
| 833 | "@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=="], | 886 | "@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=="], |
| @@ -920,6 +973,8 @@ · 86 unmodified lines | |||
| 920 | 973 | ||
| 921 | "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], | 974 | "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], |
| 922 | 975 | ||
| 976 | "@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="], | ||
| 977 | |||
| 923 | "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], | 978 | "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], |
| 924 | 979 | ||
| 925 | "@streamdown/code": ["@streamdown/code@1.0.1", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-U9LITfQ28tZYAoY922jdtw1ryg4kgRBdURopqK9hph7G2fBUwPeHthjH7SvaV0fvFv7EqjqCzARJuWUljLe9Ag=="], | 980 | "@streamdown/code": ["@streamdown/code@1.0.1", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-U9LITfQ28tZYAoY922jdtw1ryg4kgRBdURopqK9hph7G2fBUwPeHthjH7SvaV0fvFv7EqjqCzARJuWUljLe9Ag=="], |
| @@ -1208,10 +1263,14 @@ · 282 unmodified lines | |||
| 1208 | 1263 | ||
| 1209 | "basic-ftp": ["basic-ftp@5.1.0", "", {}, "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw=="], | 1264 | "basic-ftp": ["basic-ftp@5.1.0", "", {}, "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw=="], |
| 1210 | 1265 | ||
| 1266 | "better-result": ["better-result@2.7.0", "", { "dependencies": { "@clack/prompts": "^0.11.0" }, "bin": { "better-result": "bin/cli.mjs" } }, "sha512-7zrmXjAK8u8Z6SOe4R65XObOR5X+Y2I/VVku3t5cPOGQ8/WsBcfFmfnIPiEl5EBMDOzPHRwbiPbMtQBKYdw7RA=="], | ||
| 1267 | |||
| 1211 | "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], | 1268 | "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], |
| 1212 | 1269 | ||
| 1213 | "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], | 1270 | "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], |
| 1214 | 1271 | ||
| 1272 | "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], | ||
| 1273 | |||
| 1215 | "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], | 1274 | "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], |
| 1216 | 1275 | ||
| 1217 | "bowser": ["bowser@2.13.1", "", {}, "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw=="], | 1276 | "bowser": ["bowser@2.13.1", "", {}, "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw=="], |
| @@ -1394,7 +1453,7 @@ · 176 unmodified lines | |||
| 1394 | 1453 | ||
| 1395 | "electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="], | 1454 | "electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="], |
| 1396 | 1455 | ||
| 1397 | "elysia": ["elysia@1.4.23", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-mFIT/hEnNfrfbjGRUqunLNcURJfSXpKY4j+EWr4vP6Eoulf7feqs0WQLZwlgFZCxhdyfu0mrypIZ4mNJcEVVlQ=="], | 1456 | "elysia": ["elysia@1.4.24", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-cgMUPXzZYZGafj2VePFnckVPZ8xJKbZNOgv4hlM2nW96cPSG1BL3y2KNRrKmmA2hKEuUw0tvHVwG//pMqSZ5Jw=="], |
| 1398 | 1457 | ||
| 1399 | "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], | 1458 | "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], |
| 1400 | 1459 | ||
| @@ -1410,6 +1469,8 @@ · 9 unmodified lines | |||
| 1410 | 1469 | ||
| 1411 | "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], | 1470 | "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], |
| 1412 | 1471 | ||
| 1472 | "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], | ||
| 1473 | |||
| 1413 | "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.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", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], | 1474 | "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.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", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], |
| 1414 | 1475 | ||
| 1415 | "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], | 1476 | "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], |
| @@ -1994,6 +2055,8 @@ · 578 unmodified lines | |||
| 1994 | 2055 | ||
| 1995 | "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], | 2056 | "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], |
| 1996 | 2057 | ||
| 2058 | "miniflare": ["miniflare@4.20260210.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260210.0", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-HXR6m53IOqEzq52DuGF1x7I1K6lSIqzhbCbQXv/cTmPnPJmNkr7EBtLDm4nfSkOvlDtnwDCLUjWII5fyGJI5Tw=="], | ||
| 2059 | |||
| 1997 | "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], | 2060 | "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], |
| 1998 | 2061 | ||
| 1999 | "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], | 2062 | "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], |
| @@ -2474,6 +2537,8 @@ · 474 unmodified lines | |||
| 2474 | 2537 | ||
| 2475 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], | 2538 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], |
| 2476 | 2539 | ||
| 2540 | "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], | ||
| 2541 | |||
| 2477 | "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], | 2542 | "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], |
| 2478 | 2543 | ||
| 2479 | "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], | 2544 | "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], |
| @@ -2546,6 +2611,10 @@ · 66 unmodified lines | |||
| 2546 | 2611 | ||
| 2547 | "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], | 2612 | "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], |
| 2548 | 2613 | ||
| 2614 | "workerd": ["workerd@1.20260210.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260210.0", "@cloudflare/workerd-darwin-arm64": "1.20260210.0", "@cloudflare/workerd-linux-64": "1.20260210.0", "@cloudflare/workerd-linux-arm64": "1.20260210.0", "@cloudflare/workerd-windows-64": "1.20260210.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Sb0WXhrvf+XHQigP2trAxQnXo7wxZFC4PWnn6I7LhFxiTvzxvOAqMEiLkIz58wggRCb54T/KAA8hdjkTniR5FA=="], | ||
| 2615 | |||
| 2616 | "wrangler": ["wrangler@4.64.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.12.1", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260210.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260210.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260210.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-0PBiVEbshQT4Av/KLHbOAks4ioIKp/eAO7Xr2BgAX5v7cFYYgeOvudBrbtZa/hDDIA6858QuJnTQ8mI+cm8Vqw=="], | ||
| 2617 | |||
| 2549 | "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], | 2618 | "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], |
| 2550 | 2619 | ||
| 2551 | "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=="], | 2620 | "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=="], |
| @@ -2572,6 +2641,10 @@ · 20 unmodified lines | |||
| 2572 | 2641 | ||
| 2573 | "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], | 2642 | "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], |
| 2574 | 2643 | ||
| 2644 | "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], | ||
| 2645 | |||
| 2646 | "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], | ||
| 2647 | |||
| 2575 | "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], | 2648 | "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], |
| 2576 | 2649 | ||
| 2577 | "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], | 2650 | "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], |
| @@ -2580,10 +2653,16 @@ · 2 unmodified lines | |||
| 2580 | 2653 | ||
| 2581 | "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], | 2654 | "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], |
| 2582 | 2655 | ||
| 2656 | "@assistant/bot/@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], | ||
| 2657 | |||
| 2658 | "@assistant/server/@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], | ||
| 2659 | |||
| 2583 | "@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=="], | 2660 | "@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=="], |
| 2584 | 2661 | ||
| 2585 | "@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=="], | 2662 | "@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=="], |
| 2586 | 2663 | ||
| 2664 | "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], | ||
| 2665 | |||
| 2587 | "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], | 2666 | "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], |
| 2588 | 2667 | ||
| 2589 | "@convex-dev/workos/@workos-inc/authkit-react": ["@workos-inc/authkit-react@0.11.0", "", { "dependencies": { "@workos-inc/authkit-js": "0.13.0" }, "peerDependencies": { "react": ">=17" } }, "sha512-67HFSxP4wXC8ECGyvc1yGMwuD5NGkwT2OPt8DavHoKAlO+hRaAlu9wwzqUx1EJrHht0Dcx+l20Byq8Ab0bEhlg=="], | 2668 | "@convex-dev/workos/@workos-inc/authkit-react": ["@workos-inc/authkit-react@0.11.0", "", { "dependencies": { "@workos-inc/authkit-js": "0.13.0" }, "peerDependencies": { "react": ">=17" } }, "sha512-67HFSxP4wXC8ECGyvc1yGMwuD5NGkwT2OPt8DavHoKAlO+hRaAlu9wwzqUx1EJrHht0Dcx+l20Byq8Ab0bEhlg=="], |
| @@ -2612,12 +2691,18 @@ · 22 unmodified lines | |||
| 2612 | 2691 | ||
| 2613 | "@isaacs/cliui/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=="], | 2692 | "@isaacs/cliui/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=="], |
| 2614 | 2693 | ||
| 2694 | "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], | ||
| 2695 | |||
| 2696 | "@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], | ||
| 2697 | |||
| 2615 | "@mariozechner/pi-ai/undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="], | 2698 | "@mariozechner/pi-ai/undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="], |
| 2616 | 2699 | ||
| 2617 | "@mistralai/mistralai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], | 2700 | "@mistralai/mistralai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], |
| 2618 | 2701 | ||
| 2619 | "@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], | 2702 | "@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], |
| 2620 | 2703 | ||
| 2704 | "@proto/sources/@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], | ||
| 2705 | |||
| 2621 | "@redocly/openapi-core/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], | 2706 | "@redocly/openapi-core/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], |
| 2622 | 2707 | ||
| 2623 | "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], | 2708 | "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], |
| @@ -2690,6 +2775,10 @@ · 66 unmodified lines | |||
| 2690 | 2775 | ||
| 2691 | "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], | 2776 | "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |
| 2692 | 2777 | ||
| 2778 | "miniflare/undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], | ||
| 2779 | |||
| 2780 | "miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], | ||
| 2781 | |||
| 2693 | "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], | 2782 | "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], |
| 2694 | 2783 | ||
| 2695 | "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], | 2784 | "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], |
| @@ -2728,6 +2817,8 @@ · 32 unmodified lines | |||
| 2728 | 2817 | ||
| 2729 | "vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], | 2818 | "vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], |
| 2730 | 2819 | ||
| 2820 | "wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], | ||
| 2821 | |||
| 2731 | "wrap-ansi/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=="], | 2822 | "wrap-ansi/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=="], |
| 2732 | 2823 | ||
| 2733 | "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], | 2824 | "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], |
| @@ -2738,6 +2829,10 @@ · 4 unmodified lines | |||
| 2738 | 2829 | ||
| 2739 | "yargs/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=="], | 2830 | "yargs/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=="], |
| 2740 | 2831 | ||
| 2832 | "@assistant/bot/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], | ||
| 2833 | |||
| 2834 | "@assistant/server/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], | ||
| 2835 | |||
| 2741 | "@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=="], | 2836 | "@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=="], |
| 2742 | 2837 | ||
| 2743 | "@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=="], | 2838 | "@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=="], |
| @@ -2768,6 +2863,8 @@ · 24 unmodified lines | |||
| 2768 | 2863 | ||
| 2769 | "@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], | 2864 | "@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], |
| 2770 | 2865 | ||
| 2866 | "@proto/sources/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], | ||
| 2867 | |||
| 2771 | "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], | 2868 | "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], |
| 2772 | 2869 | ||
| 2773 | "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], | 2870 | "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], |
| @@ -2790,6 +2887,58 @@ · 16 unmodified lines | |||
| 2790 | 2887 | ||
| 2791 | "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], | 2888 | "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], |
| 2792 | 2889 | ||
| 2890 | "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], | ||
| 2891 | |||
| 2892 | "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], | ||
| 2893 | |||
| 2894 | "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], | ||
| 2895 | |||
| 2896 | "wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], | ||
| 2897 | |||
| 2898 | "wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], | ||
| 2899 | |||
| 2900 | "wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], | ||
| 2901 | |||
| 2902 | "wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], | ||
| 2903 | |||
| 2904 | "wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], | ||
| 2905 | |||
| 2906 | "wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], | ||
| 2907 | |||
| 2908 | "wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], | ||
| 2909 | |||
| 2910 | "wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], | ||
| 2911 | |||
| 2912 | "wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], | ||
| 2913 | |||
| 2914 | "wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], | ||
| 2915 | |||
| 2916 | "wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], | ||
| 2917 | |||
| 2918 | "wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], | ||
| 2919 | |||
| 2920 | "wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], | ||
| 2921 | |||
| 2922 | "wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], | ||
| 2923 | |||
| 2924 | "wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], | ||
| 2925 | |||
| 2926 | "wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], | ||
| 2927 | |||
| 2928 | "wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], | ||
| 2929 | |||
| 2930 | "wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], | ||
| 2931 | |||
| 2932 | "wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], | ||
| 2933 | |||
| 2934 | "wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], | ||
| 2935 | |||
| 2936 | "wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], | ||
| 2937 | |||
| 2938 | "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], | ||
| 2939 | |||
| 2940 | "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], | ||
| 2941 | |||
| 2793 | "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], | 2942 | "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], |
| 2794 | 2943 | ||
| 2795 | "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], | 2944 | "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], |
| @@ -6,10 +6,12 @@ · 5 unmodified lines | |||
| 6 | * Reads all configuration from the root .env file (auto-loaded by Bun). | 6 | * Reads all configuration from the root .env file (auto-loaded by Bun). |
| 7 | * | 7 | * |
| 8 | * Starts: | 8 | * Starts: |
| 9 | * 1. Convex cloud dev function watcher ─┐ | 9 | * 1. Sources catalog API (port 4343) ─┐ |
| 10 | * 2. Executor web UI (port 4312) ├─ all started concurrently | 10 | * 2. Convex cloud dev function watcher │ |
| 11 | * 3. Assistant server (port 3000) │ | 11 | * 3. CF sandbox host worker (port 8787) ├─ all started concurrently |
| 12 | * 4. Discord bot ─┘ | 12 | * 4. Executor web UI (port 4312) │ |
| 13 | * 5. Assistant server (port 3000) │ | ||
| 14 | * 6. Discord bot ─┘ | ||
| 13 | * | 15 | * |
| 14 | * All processes are killed when this script exits (Ctrl+C). | 16 | * All processes are killed when this script exits (Ctrl+C). |
| 15 | * PIDs are written to .dev.pids for use with `bun run kill:all`. | 17 | * PIDs are written to .dev.pids for use with `bun run kill:all`. |
| @@ -21,8 +23,10 @@ import { unlinkSync } from "node:fs"; · 5 unmodified lines | |||
| 21 | const PID_FILE = join(import.meta.dir, ".dev.pids"); | 23 | const PID_FILE = join(import.meta.dir, ".dev.pids"); |
| 22 | 24 | ||
| 23 | const colors = { | 25 | const colors = { |
| 26 | sources: "\x1b[33m", // yellow | ||
| 24 | convex: "\x1b[36m", // cyan | 27 | convex: "\x1b[36m", // cyan |
| 25 | web: "\x1b[34m", // blue | 28 | web: "\x1b[34m", // blue |
| 29 | sandbox: "\x1b[91m", // bright red | ||
| 26 | assistant: "\x1b[32m", // green | 30 | assistant: "\x1b[32m", // green |
| 27 | bot: "\x1b[35m", // magenta | 31 | bot: "\x1b[35m", // magenta |
| 28 | reset: "\x1b[0m", | 32 | reset: "\x1b[0m", |
| @@ -117,7 +121,8 @@ process.on("SIGTERM", shutdown); · 88 unmodified lines | |||
| 117 | // ── Kill stale processes from a previous run ── | 121 | // ── Kill stale processes from a previous run ── |
| 118 | 122 | ||
| 119 | const EXECUTOR_WEB_PORT = Number(Bun.env.EXECUTOR_WEB_PORT ?? 4312); | 123 | const EXECUTOR_WEB_PORT = Number(Bun.env.EXECUTOR_WEB_PORT ?? 4312); |
| 120 | const DEV_PORTS = [3000, EXECUTOR_WEB_PORT]; | 124 | const SANDBOX_PORT = Number(Bun.env.SANDBOX_PORT ?? 8787); |
| 125 | const DEV_PORTS = [3000, EXECUTOR_WEB_PORT, SANDBOX_PORT]; | ||
| 121 | 126 | ||
| 122 | async function killStaleProcesses() { | 127 | async function killStaleProcesses() { |
| 123 | let killed = 0; | 128 | let killed = 0; |
| @@ -164,6 +169,11 @@ const urls = resolveExecutorUrls(); · 40 unmodified lines | |||
| 164 | console.log(prefix("convex", `Using Convex URL: ${urls.convexUrl}`)); | 169 | console.log(prefix("convex", `Using Convex URL: ${urls.convexUrl}`)); |
| 165 | console.log(prefix("convex", `Using executor HTTP URL: ${urls.executorUrl}`)); | 170 | console.log(prefix("convex", `Using executor HTTP URL: ${urls.executorUrl}`)); |
| 166 | 171 | ||
| 172 | // 1. Sources catalog API | ||
| 173 | spawnService("sources", ["bun", "--hot", "server.ts"], { | ||
| 174 | cwd: "./sources", | ||
| 175 | }); | ||
| 176 | |||
| 167 | // 2. Start Convex file watcher (non-blocking — repushes on changes) | 177 | // 2. Start Convex file watcher (non-blocking — repushes on changes) |
| 168 | spawnService("convex", [ | 178 | spawnService("convex", [ |
| 169 | "bunx", "convex", "dev", | 179 | "bunx", "convex", "dev", |
| @@ -172,7 +182,15 @@ spawnService("convex", [ · 2 unmodified lines | |||
| 172 | cwd: "./executor", | 182 | cwd: "./executor", |
| 173 | }); | 183 | }); |
| 174 | 184 | ||
| 175 | // 3. Everything else in parallel | 185 | // 3. CF sandbox host worker (local wrangler dev for iterating on the worker) |
| 186 | spawnService("sandbox", [ | ||
| 187 | "bunx", "wrangler", "dev", | ||
| 188 | "--port", String(SANDBOX_PORT), | ||
| 189 | ], { | ||
| 190 | cwd: "./executor/packages/sandbox-host", | ||
| 191 | }); | ||
| 192 | |||
| 193 | // 4. Everything else in parallel | ||
| 176 | spawnService("web", ["bun", "run", "dev"], { | 194 | spawnService("web", ["bun", "run", "dev"], { |
| 177 | cwd: "./executor/apps/web", | 195 | cwd: "./executor/apps/web", |
| 178 | }); | 196 | }); |
| @@ -116,20 +116,62 @@ function countAllTools(node: NamespaceNode): number { · 115 unmodified lines | |||
| 116 | 116 | ||
| 117 | /** TS helper types for OpenAPI indexed access (same as typechecker.ts) */ | 117 | /** TS helper types for OpenAPI indexed access (same as typechecker.ts) */ |
| 118 | const OPENAPI_HELPER_TYPES = ` | 118 | const OPENAPI_HELPER_TYPES = ` |
| 119 | type _OrEmpty<T> = [T] extends [never] ? {} : T; | 119 | type _Normalize<T> = Exclude<T, undefined | null>; |
| 120 | type _OrEmpty<T> = [_Normalize<T>] extends [never] ? {} : _Normalize<T>; | ||
| 120 | type _Simplify<T> = { [K in keyof T]: T[K] } & {}; | 121 | type _Simplify<T> = { [K in keyof T]: T[K] } & {}; |
| 122 | type _ParamsOf<Op> = | ||
| 123 | Op extends { parameters: infer P } ? P : | ||
| 124 | Op extends { parameters?: infer P } ? P : | ||
| 125 | never; | ||
| 126 | type _ParamAt<Op, K extends "query" | "path" | "header" | "cookie"> = | ||
| 127 | _ParamsOf<Op> extends { [P in K]?: infer V } ? V : never; | ||
| 128 | type _BodyOf<Op> = | ||
| 129 | Op extends { requestBody?: infer B } ? B : | ||
| 130 | Op extends { requestBody: infer B } ? B : | ||
| 131 | never; | ||
| 132 | type _BodyContent<B> = | ||
| 133 | B extends { content: infer C } | ||
| 134 | ? C extends Record<string, infer V> ? V : never | ||
| 135 | : never; | ||
| 121 | type ToolInput<Op> = _Simplify< | 136 | type ToolInput<Op> = _Simplify< |
| 122 | _OrEmpty<Op extends { parameters: { query: infer Q } } ? { [K in keyof Q]: Q[K] } : never> & | 137 | _OrEmpty<_ParamAt<Op, "query">> & |
| 123 | _OrEmpty<Op extends { parameters: { path: infer P } } ? { [K in keyof P]: P[K] } : never> & | 138 | _OrEmpty<_ParamAt<Op, "path">> & |
| 124 | _OrEmpty<Op extends { requestBody: { content: { "application/json": infer B } } } ? B : never> | 139 | _OrEmpty<_ParamAt<Op, "header">> & |
| 140 | _OrEmpty<_ParamAt<Op, "cookie">> & | ||
| 141 | _OrEmpty<_BodyContent<_BodyOf<Op>>> | ||
| 125 | >; | 142 | >; |
| 126 | type ToolOutput<Op> = | 143 | type _ResponsesOf<Op> = Op extends { responses: infer R } ? R : never; |
| 127 | Op extends { responses: { 200: { content: { "application/json": infer R } } } } ? R : | 144 | type _RespAt<Op, Code extends PropertyKey> = |
| 128 | Op extends { responses: { 201: { content: { "application/json": infer R } } } } ? R : | 145 | _ResponsesOf<Op> extends { [K in Code]?: infer R } ? R : never; |
| 129 | Op extends { responses: { 202: { content: { "application/json": infer R } } } } ? R : | 146 | type _ResponsePayload<R> = |
| 130 | Op extends { responses: { 204: unknown } } ? void : | 147 | [R] extends [never] ? never : |
| 131 | Op extends { responses: { 205: unknown } } ? void : | 148 | R extends { content: infer C } |
| 132 | unknown; | 149 | ? C extends Record<string, infer V> ? V : unknown |
| 150 | : R extends { schema: infer S } ? S : unknown; | ||
| 151 | type _HasStatus<Op, Code extends PropertyKey> = | ||
| 152 | [_RespAt<Op, Code>] extends [never] ? false : true; | ||
| 153 | type _PayloadAt<Op, Code extends PropertyKey> = | ||
| 154 | Code extends 204 | 205 | ||
| 155 | ? (_HasStatus<Op, Code> extends true ? void : never) | ||
| 156 | : _ResponsePayload<_RespAt<Op, Code>>; | ||
| 157 | type _FirstKnown<T extends readonly unknown[]> = | ||
| 158 | T extends readonly [infer H, ...infer Rest] | ||
| 159 | ? [H] extends [never] ? _FirstKnown<Rest> : H | ||
| 160 | : unknown; | ||
| 161 | type ToolOutput<Op> = _FirstKnown<[ | ||
| 162 | _PayloadAt<Op, 200>, | ||
| 163 | _PayloadAt<Op, 201>, | ||
| 164 | _PayloadAt<Op, 202>, | ||
| 165 | _PayloadAt<Op, 203>, | ||
| 166 | _PayloadAt<Op, 204>, | ||
| 167 | _PayloadAt<Op, 205>, | ||
| 168 | _PayloadAt<Op, 206>, | ||
| 169 | _PayloadAt<Op, 207>, | ||
| 170 | _PayloadAt<Op, 208>, | ||
| 171 | _PayloadAt<Op, 226>, | ||
| 172 | _PayloadAt<Op, "default">, | ||
| 173 | unknown | ||
| 174 | ]>; | ||
| 133 | `; | 175 | `; |
| 134 | 176 | ||
| 135 | function generateToolsDts(tools: ToolDescriptor[], dtsSources: Set<string>): string { | 177 | function generateToolsDts(tools: ToolDescriptor[], dtsSources: Set<string>): string { |
| @@ -1,6 +1,6 @@ | |||
| 1 | "use client"; | 1 | "use client"; |
| 2 | 2 | ||
| 3 | import { useState } from "react"; | 3 | import { useRef, useState } from "react"; |
| 4 | import { | 4 | import { |
| 5 | Wrench, | 5 | Wrench, |
| 6 | Plus, | 6 | Plus, |
| @@ -11,6 +11,7 @@ import { · 4 unmodified lines | |||
| 11 | AlertTriangle, | 11 | AlertTriangle, |
| 12 | KeyRound, | 12 | KeyRound, |
| 13 | Pencil, | 13 | Pencil, |
| 14 | Loader2, | ||
| 14 | } from "lucide-react"; | 15 | } from "lucide-react"; |
| 15 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | 16 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; |
| 16 | import { Button } from "@/components/ui/button"; | 17 | import { Button } from "@/components/ui/button"; |
| @@ -56,158 +57,27 @@ import type { · 39 unmodified lines | |||
| 56 | import { parse as parseDomain } from "tldts"; | 57 | import { parse as parseDomain } from "tldts"; |
| 57 | import { toast } from "sonner"; | 58 | import { toast } from "sonner"; |
| 58 | import { cn } from "@/lib/utils"; | 59 | import { cn } from "@/lib/utils"; |
| 60 | import { Streamdown } from "streamdown"; | ||
| 59 | 61 | ||
| 60 | // ── API Presets ── | 62 | interface CatalogCollectionItem { |
| 61 | 63 | id: string; | |
| 62 | interface ApiPreset { | ||
| 63 | name: string; | 64 | name: string; |
| 64 | label: string; | 65 | summary: string; |
| 65 | description: string; | 66 | specUrl: string; |
| 66 | type: "openapi" | "mcp" | "graphql"; | 67 | originUrl?: string; |
| 67 | spec?: string; | 68 | providerName: string; |
| 68 | url?: string; | 69 | logoUrl?: string; |
| 69 | endpoint?: string; | 70 | categories?: string; |
| 70 | baseUrl?: string; | 71 | version?: string; |
| 71 | authNote?: string; | ||
| 72 | } | 72 | } |
| 73 | 73 | ||
| 74 | const API_PRESETS: ApiPreset[] = [ | 74 | interface CatalogCollectionsResponse { |
| 75 | { | 75 | items?: CatalogCollectionItem[]; |
| 76 | name: "github", | 76 | totalCount?: number; |
| 77 | label: "GitHub", | 77 | hasMore?: boolean; |
| 78 | description: "Repos, issues, PRs, actions, users, orgs", | 78 | error?: string; |
| 79 | type: "openapi", | 79 | detail?: string; |
| 80 | spec: "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml", | 80 | } |
| 81 | baseUrl: "https://api.github.com", | ||
| 82 | authNote: "Add a bearer credential with a PAT for authenticated access", | ||
| 83 | }, | ||
| 84 | { | ||
| 85 | name: "vercel", | ||
| 86 | label: "Vercel", | ||
| 87 | description: "Deployments, projects, domains, env vars, teams", | ||
| 88 | type: "openapi", | ||
| 89 | spec: "https://openapi.vercel.sh", | ||
| 90 | baseUrl: "https://api.vercel.com", | ||
| 91 | authNote: "Requires API token as bearer credential", | ||
| 92 | }, | ||
| 93 | { | ||
| 94 | name: "slack", | ||
| 95 | label: "Slack", | ||
| 96 | description: "Messages, channels, users, reactions, files", | ||
| 97 | type: "openapi", | ||
| 98 | spec: "https://api.slack.com/specs/openapi/v2/slack_web.json", | ||
| 99 | baseUrl: "https://slack.com/api", | ||
| 100 | authNote: "Requires a bot token as bearer credential", | ||
| 101 | }, | ||
| 102 | { | ||
| 103 | name: "discord", | ||
| 104 | label: "Discord", | ||
| 105 | description: "Guilds, channels, messages, interactions, webhooks", | ||
| 106 | type: "openapi", | ||
| 107 | spec: "https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json", | ||
| 108 | baseUrl: "https://discord.com/api/v10", | ||
| 109 | authNote: "Requires a bot token as bearer credential", | ||
| 110 | }, | ||
| 111 | { | ||
| 112 | name: "stripe", | ||
| 113 | label: "Stripe", | ||
| 114 | description: "Payments, customers, subscriptions, invoices", | ||
| 115 | type: "openapi", | ||
| 116 | spec: "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json", | ||
| 117 | baseUrl: "https://api.stripe.com", | ||
| 118 | authNote: "Requires API key as bearer credential", | ||
| 119 | }, | ||
| 120 | |||
| 121 | { | ||
| 122 | name: "openai", | ||
| 123 | label: "OpenAI", | ||
| 124 | description: "Chat completions, embeddings, images, files", | ||
| 125 | type: "openapi", | ||
| 126 | spec: "https://app.stainless.com/api/spec/documented/openai/openapi.documented.yml", | ||
| 127 | baseUrl: "https://api.openai.com", | ||
| 128 | authNote: "Requires API key as bearer credential", | ||
| 129 | }, | ||
| 130 | { | ||
| 131 | name: "cloudflare", | ||
| 132 | label: "Cloudflare", | ||
| 133 | description: "DNS, zones, workers, KV, R2, firewall", | ||
| 134 | type: "openapi", | ||
| 135 | spec: "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.yaml", | ||
| 136 | baseUrl: "https://api.cloudflare.com/client/v4", | ||
| 137 | authNote: "Requires API token as bearer credential", | ||
| 138 | }, | ||
| 139 | { | ||
| 140 | name: "sentry", | ||
| 141 | label: "Sentry", | ||
| 142 | description: "Issues, events, projects, releases, alerts", | ||
| 143 | type: "openapi", | ||
| 144 | spec: "https://raw.githubusercontent.com/getsentry/sentry-api-schema/refs/heads/main/openapi-derefed.json", | ||
| 145 | baseUrl: "https://sentry.io/api/0", | ||
| 146 | authNote: "Requires auth token as bearer credential", | ||
| 147 | }, | ||
| 148 | { | ||
| 149 | name: "jira", | ||
| 150 | label: "Jira", | ||
| 151 | description: "Issues, projects, boards, sprints, users", | ||
| 152 | type: "openapi", | ||
| 153 | spec: "https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json", | ||
| 154 | baseUrl: "https://your-domain.atlassian.net/rest/api/3", | ||
| 155 | authNote: "Requires API token with basic auth (email:token)", | ||
| 156 | }, | ||
| 157 | { | ||
| 158 | name: "pagerduty", | ||
| 159 | label: "PagerDuty", | ||
| 160 | description: "Incidents, services, schedules, escalations", | ||
| 161 | type: "openapi", | ||
| 162 | spec: "https://raw.githubusercontent.com/PagerDuty/api-schema/main/reference/REST/openapiv3.json", | ||
| 163 | baseUrl: "https://api.pagerduty.com", | ||
| 164 | authNote: "Requires API key as bearer credential", | ||
| 165 | }, | ||
| 166 | { | ||
| 167 | name: "digitalocean", | ||
| 168 | label: "DigitalOcean", | ||
| 169 | description: "Droplets, databases, domains, apps, spaces", | ||
| 170 | type: "openapi", | ||
| 171 | spec: "https://api-engineering.nyc3.cdn.digitaloceanspaces.com/spec-ci/DigitalOcean-public.v2.yaml", | ||
| 172 | baseUrl: "https://api.digitalocean.com", | ||
| 173 | authNote: "Requires API token as bearer credential", | ||
| 174 | }, | ||
| 175 | { | ||
| 176 | name: "twilio", | ||
| 177 | label: "Twilio", | ||
| 178 | description: "SMS, calls, conversations, verify, phone numbers", | ||
| 179 | type: "openapi", | ||
| 180 | spec: "https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json", | ||
| 181 | baseUrl: "https://api.twilio.com", | ||
| 182 | authNote: "Requires Account SID + Auth Token as basic auth", | ||
| 183 | }, | ||
| 184 | { | ||
| 185 | name: "notion", | ||
| 186 | label: "Notion", | ||
| 187 | description: "Pages, databases, blocks, search, users", | ||
| 188 | type: "openapi", | ||
| 189 | spec: "https://developers.notion.com/openapi.json", | ||
| 190 | baseUrl: "https://api.notion.com", | ||
| 191 | authNote: "Requires integration token as bearer credential", | ||
| 192 | }, | ||
| 193 | |||
| 194 | { | ||
| 195 | name: "resend", | ||
| 196 | label: "Resend", | ||
| 197 | description: "Send emails, manage domains, API keys", | ||
| 198 | type: "openapi", | ||
| 199 | spec: "https://raw.githubusercontent.com/resend/resend-openapi/main/resend.yaml", | ||
| 200 | baseUrl: "https://api.resend.com", | ||
| 201 | }, | ||
| 202 | { | ||
| 203 | name: "linear", | ||
| 204 | label: "Linear", | ||
| 205 | description: "Issues, projects, teams, cycles, labels", | ||
| 206 | type: "graphql", | ||
| 207 | endpoint: "https://api.linear.app/graphql", | ||
| 208 | authNote: "Requires API key as bearer credential", | ||
| 209 | }, | ||
| 210 | ]; | ||
| 211 | 81 | ||
| 212 | /** Derive a favicon URL from any URL string via Google's favicon service. */ | 82 | /** Derive a favicon URL from any URL string via Google's favicon service. */ |
| 213 | function faviconForUrl(url: string | undefined | null): string | null { | 83 | function faviconForUrl(url: string | undefined | null): string | null { |
| @@ -220,16 +90,6 @@ function faviconForUrl(url: string | undefined | null): string | null { · 6 unmodified lines | |||
| 220 | } | 90 | } |
| 221 | } | 91 | } |
| 222 | 92 | ||
| 223 | function getFaviconUrl(preset: ApiPreset): string | null { | ||
| 224 | if (preset.type === "openapi") { | ||
| 225 | return faviconForUrl(preset.baseUrl ?? null); | ||
| 226 | } | ||
| 227 | if (preset.type === "graphql") { | ||
| 228 | return faviconForUrl(preset.endpoint ?? null); | ||
| 229 | } | ||
| 230 | return faviconForUrl(preset.url ?? null); | ||
| 231 | } | ||
| 232 | |||
| 233 | function getSourceFavicon(source: ToolSourceRecord): string | null { | 93 | function getSourceFavicon(source: ToolSourceRecord): string | null { |
| 234 | if (source.type === "mcp") { | 94 | if (source.type === "mcp") { |
| 235 | return faviconForUrl((source.config.url as string) ?? null); | 95 | return faviconForUrl((source.config.url as string) ?? null); |
| @@ -237,7 +97,30 @@ function getSourceFavicon(source: ToolSourceRecord): string | null { · 1 unmodified line | |||
| 237 | if (source.type === "graphql") { | 97 | if (source.type === "graphql") { |
| 238 | return faviconForUrl((source.config.endpoint as string) ?? null); | 98 | return faviconForUrl((source.config.endpoint as string) ?? null); |
| 239 | } | 99 | } |
| 240 | return faviconForUrl((source.config.baseUrl as string) ?? null); | 100 | const spec = source.config.spec as string | undefined; |
| 101 | if (typeof spec === "string" && spec.startsWith("postman:")) { | ||
| 102 | return null; | ||
| 103 | } | ||
| 104 | const baseUrl = source.config.baseUrl as string | undefined; | ||
| 105 | const collectionUrl = source.config.collectionUrl as string | undefined; | ||
| 106 | const specUrl = typeof spec === "string" && spec.startsWith("http") ? spec : null; | ||
| 107 | return faviconForUrl(baseUrl ?? collectionUrl ?? specUrl); | ||
| 108 | } | ||
| 109 | |||
| 110 | function sourceEndpointLabel(source: ToolSourceRecord): string { | ||
| 111 | if (source.type === "mcp") return (source.config.url as string) ?? ""; | ||
| 112 | if (source.type === "graphql") return (source.config.endpoint as string) ?? ""; | ||
| 113 | |||
| 114 | const spec = source.config.spec; | ||
| 115 | if (typeof spec === "string" && spec.startsWith("postman:")) { | ||
| 116 | const uid = spec.slice("postman:".length).trim(); | ||
| 117 | if (uid.length > 0) { | ||
| 118 | return `catalog:${uid}`; | ||
| 119 | } | ||
| 120 | return "catalog:collection"; | ||
| 121 | } | ||
| 122 | |||
| 123 | return (source.config.spec as string) ?? ""; | ||
| 241 | } | 124 | } |
| 242 | 125 | ||
| 243 | function sourceKeyForSource(source: ToolSourceRecord): string | null { | 126 | function sourceKeyForSource(source: ToolSourceRecord): string | null { |
| @@ -366,6 +249,39 @@ function inferNameFromUrl(url: string): string { · 122 unmodified lines | |||
| 366 | } | 249 | } |
| 367 | } | 250 | } |
| 368 | 251 | ||
| 252 | function sanitizeSourceName(value: string): string { | ||
| 253 | const slug = value | ||
| 254 | .toLowerCase() | ||
| 255 | .replace(/[^a-z0-9]+/g, "-") | ||
| 256 | .replace(/-+/g, "-") | ||
| 257 | .replace(/^-|-$/g, "") | ||
| 258 | .slice(0, 48); | ||
| 259 | return slug || "source"; | ||
| 260 | } | ||
| 261 | |||
| 262 | function withUniqueSourceName(baseName: string, takenNames: Set<string>): string { | ||
| 263 | const loweredTaken = new Set([...takenNames].map((name) => name.toLowerCase())); | ||
| 264 | const candidate = sanitizeSourceName(baseName); | ||
| 265 | if (!loweredTaken.has(candidate.toLowerCase())) { | ||
| 266 | return candidate; | ||
| 267 | } | ||
| 268 | |||
| 269 | let suffix = 2; | ||
| 270 | while (true) { | ||
| 271 | const next = `${candidate}-${suffix}`; | ||
| 272 | if (!loweredTaken.has(next.toLowerCase())) { | ||
| 273 | return next; | ||
| 274 | } | ||
| 275 | suffix += 1; | ||
| 276 | } | ||
| 277 | } | ||
| 278 | |||
| 279 | function catalogSourceName(item: CatalogCollectionItem): string { | ||
| 280 | const owner = sanitizeSourceName(item.providerName || "catalog"); | ||
| 281 | const title = sanitizeSourceName(item.name); | ||
| 282 | return sanitizeSourceName(`${owner}-${title}`); | ||
| 283 | } | ||
| 284 | |||
| 369 | function AddSourceDialog({ | 285 | function AddSourceDialog({ |
| 370 | existingSourceNames, | 286 | existingSourceNames, |
| 371 | }: { | 287 | }: { |
| @@ -374,7 +290,7 @@ function AddSourceDialog({ · 2 unmodified lines | |||
| 374 | const { context } = useSession(); | 290 | const { context } = useSession(); |
| 375 | const upsertToolSource = useMutation(convexApi.workspace.upsertToolSource); | 291 | const upsertToolSource = useMutation(convexApi.workspace.upsertToolSource); |
| 376 | const [open, setOpen] = useState(false); | 292 | const [open, setOpen] = useState(false); |
| 377 | const [presetsOpen, setPresetsOpen] = useState(false); | 293 | const [view, setView] = useState<"catalog" | "custom">("catalog"); |
| 378 | const [type, setType] = useState<"mcp" | "openapi" | "graphql">("mcp"); | 294 | const [type, setType] = useState<"mcp" | "openapi" | "graphql">("mcp"); |
| 379 | const [name, setName] = useState(""); | 295 | const [name, setName] = useState(""); |
| 380 | const [nameManuallyEdited, setNameManuallyEdited] = useState(false); | 296 | const [nameManuallyEdited, setNameManuallyEdited] = useState(false); |
| @@ -383,7 +299,35 @@ function AddSourceDialog({ · 2 unmodified lines | |||
| 383 | const [mcpTransport, setMcpTransport] = useState<"auto" | "streamable-http" | "sse">("auto"); | 299 | const [mcpTransport, setMcpTransport] = useState<"auto" | "streamable-http" | "sse">("auto"); |
| 384 | const [mcpActorQueryParamKey, setMcpActorQueryParamKey] = useState("userId"); | 300 | const [mcpActorQueryParamKey, setMcpActorQueryParamKey] = useState("userId"); |
| 385 | const [submitting, setSubmitting] = useState(false); | 301 | const [submitting, setSubmitting] = useState(false); |
| 386 | const [addingPreset, setAddingPreset] = useState<string | null>(null); | 302 | const [locallyReservedNames, setLocallyReservedNames] = useState<string[]>([]); |
| 303 | const [catalogQuery, setCatalogQuery] = useState(""); | ||
| 304 | const [catalogSort, setCatalogSort] = useState<"popular" | "recent">("popular"); | ||
| 305 | const [catalogItems, setCatalogItems] = useState<CatalogCollectionItem[]>([]); | ||
| 306 | const [catalogOffset, setCatalogOffset] = useState(0); | ||
| 307 | const [catalogHasMore, setCatalogHasMore] = useState(true); | ||
| 308 | const [catalogTotalCount, setCatalogTotalCount] = useState<number | null>(null); | ||
| 309 | const [catalogLoading, setCatalogLoading] = useState(false); | ||
| 310 | const [catalogLoadingMore, setCatalogLoadingMore] = useState(false); | ||
| 311 | const [catalogError, setCatalogError] = useState<string | null>(null); | ||
| 312 | const [addingCatalogId, setAddingCatalogId] = useState<string | null>(null); | ||
| 313 | const catalogRequestIdRef = useRef(0); | ||
| 314 | const catalogInFlightRef = useRef(false); | ||
| 315 | |||
| 316 | const CATALOG_PAGE_SIZE = 20; | ||
| 317 | |||
| 318 | const getTakenSourceNames = () => new Set([...existingSourceNames, ...locallyReservedNames]); | ||
| 319 | |||
| 320 | const reserveSourceName = (sourceName: string) => { | ||
| 321 | setLocallyReservedNames((current) => | ||
| 322 | current.includes(sourceName) | ||
| 323 | ? current | ||
| 324 | : [...current, sourceName] | ||
| 325 | ); | ||
| 326 | }; | ||
| 327 | |||
| 328 | const getUniqueAutoSourceName = (candidate: string) => { | ||
| 329 | return withUniqueSourceName(candidate, getTakenSourceNames()); | ||
| 330 | }; | ||
| 387 | 331 | ||
| 388 | const handleEndpointChange = (value: string) => { | 332 | const handleEndpointChange = (value: string) => { |
| 389 | setEndpoint(value); | 333 | setEndpoint(value); |
| @@ -398,20 +342,131 @@ function AddSourceDialog({ · 8 unmodified lines | |||
| 398 | setNameManuallyEdited(true); | 342 | setNameManuallyEdited(true); |
| 399 | }; | 343 | }; |
| 400 | 344 | ||
| 401 | const resetForm = () => { | 345 | const resetDialogState = () => { |
| 346 | catalogRequestIdRef.current += 1; | ||
| 347 | catalogInFlightRef.current = false; | ||
| 348 | setView("catalog"); | ||
| 349 | setType("mcp"); | ||
| 402 | setName(""); | 350 | setName(""); |
| 403 | setEndpoint(""); | 351 | setEndpoint(""); |
| 404 | setBaseUrl(""); | 352 | setBaseUrl(""); |
| 405 | setMcpTransport("auto"); | 353 | setMcpTransport("auto"); |
| 406 | setMcpActorQueryParamKey("userId"); | 354 | setMcpActorQueryParamKey("userId"); |
| 407 | setNameManuallyEdited(false); | 355 | setNameManuallyEdited(false); |
| 408 | setPresetsOpen(false); | 356 | setLocallyReservedNames([]); |
| 409 | setAddingPreset(null); | 357 | setCatalogQuery(""); |
| 358 | setCatalogSort("popular"); | ||
| 359 | setCatalogItems([]); | ||
| 360 | setCatalogOffset(0); | ||
| 361 | setCatalogHasMore(true); | ||
| 362 | setCatalogTotalCount(null); | ||
| 363 | setCatalogLoading(false); | ||
| 364 | setCatalogLoadingMore(false); | ||
| 365 | setCatalogError(null); | ||
| 366 | setAddingCatalogId(null); | ||
| 367 | }; | ||
| 368 | |||
| 369 | const loadCatalogPage = async ({ | ||
| 370 | mode, | ||
| 371 | query, | ||
| 372 | sort, | ||
| 373 | }: { | ||
| 374 | mode: "reset" | "next"; | ||
| 375 | query?: string; | ||
| 376 | sort?: "popular" | "recent"; | ||
| 377 | }) => { | ||
| 378 | const resolvedQuery = (query ?? catalogQuery).trim(); | ||
| 379 | const resolvedSort = sort ?? catalogSort; | ||
| 380 | const nextOffset = mode === "reset" ? 0 : catalogOffset; | ||
| 381 | |||
| 382 | if (mode === "next") { | ||
| 383 | if (catalogLoading || catalogLoadingMore || catalogInFlightRef.current || !catalogHasMore) { | ||
| 384 | return; | ||
| 385 | } | ||
| 386 | setCatalogLoadingMore(true); | ||
| 387 | } else { | ||
| 388 | setCatalogLoading(true); | ||
| 389 | setCatalogLoadingMore(false); | ||
| 390 | setCatalogError(null); | ||
| 391 | } | ||
| 392 | |||
| 393 | const requestId = catalogRequestIdRef.current + 1; | ||
| 394 | catalogRequestIdRef.current = requestId; | ||
| 395 | catalogInFlightRef.current = true; | ||
| 396 | |||
| 397 | try { | ||
| 398 | const params = new URLSearchParams({ | ||
| 399 | sort: resolvedSort, | ||
| 400 | limit: String(CATALOG_PAGE_SIZE), | ||
| 401 | offset: String(nextOffset), | ||
| 402 | }); | ||
| 403 | if (resolvedQuery.length > 0) { | ||
| 404 | params.set("q", resolvedQuery); | ||
| 405 | } | ||
| 406 | |||
| 407 | const catalogBase = process.env.NEXT_PUBLIC_SOURCES_URL ?? "http://127.0.0.1:4343"; | ||
| 408 | const response = await fetch(`${catalogBase}/collections?${params.toString()}`, { | ||
| 409 | cache: "no-store", | ||
| 410 | }); | ||
| 411 | const body = await response.json() as CatalogCollectionsResponse; | ||
| 412 | |||
| 413 | if (requestId !== catalogRequestIdRef.current) { | ||
| 414 | return; | ||
| 415 | } | ||
| 416 | |||
| 417 | if (!response.ok) { | ||
| 418 | throw new Error(body.error || "Failed to load API catalog"); | ||
| 419 | } | ||
| 420 | |||
| 421 | const nextItems = Array.isArray(body.items) ? body.items : []; | ||
| 422 | setCatalogItems((current) => { | ||
| 423 | if (mode === "reset") { | ||
| 424 | return nextItems; | ||
| 425 | } | ||
| 426 | |||
| 427 | const merged = [...current]; | ||
| 428 | const seen = new Set(current.map((entry) => entry.id)); | ||
| 429 | for (const item of nextItems) { | ||
| 430 | if (seen.has(item.id)) continue; | ||
| 431 | seen.add(item.id); | ||
| 432 | merged.push(item); | ||
| 433 | } | ||
| 434 | return merged; | ||
| 435 | }); | ||
| 436 | setCatalogOffset(nextOffset + nextItems.length); | ||
| 437 | setCatalogHasMore(typeof body.hasMore === "boolean" ? body.hasMore : nextItems.length >= CATALOG_PAGE_SIZE); | ||
| 438 | setCatalogTotalCount(typeof body.totalCount === "number" ? body.totalCount : null); | ||
| 439 | setCatalogError(null); | ||
| 440 | } catch (error) { | ||
| 441 | if (requestId !== catalogRequestIdRef.current) { | ||
| 442 | return; | ||
| 443 | } | ||
| 444 | |||
| 445 | if (mode === "reset") { | ||
| 446 | setCatalogItems([]); | ||
| 447 | setCatalogOffset(0); | ||
| 448 | } | ||
| 449 | setCatalogHasMore(false); | ||
| 450 | setCatalogTotalCount(null); | ||
| 451 | setCatalogError(error instanceof Error ? error.message : "Failed to load API catalog"); | ||
| 452 | } finally { | ||
| 453 | if (requestId === catalogRequestIdRef.current) { | ||
| 454 | setCatalogLoading(false); | ||
| 455 | setCatalogLoadingMore(false); | ||
| 456 | catalogInFlightRef.current = false; | ||
| 457 | } | ||
| 458 | } | ||
| 410 | }; | 459 | }; |
| 411 | 460 | ||
| 412 | const handleOpenChange = (isOpen: boolean) => { | 461 | const handleOpenChange = (isOpen: boolean) => { |
| 413 | setOpen(isOpen); | 462 | setOpen(isOpen); |
| 414 | if (!isOpen) resetForm(); | 463 | if (!isOpen) { |
| 464 | resetDialogState(); | ||
| 465 | return; | ||
| 466 | } | ||
| 467 | |||
| 468 | resetDialogState(); | ||
| 469 | void loadCatalogPage({ mode: "reset", query: "", sort: "popular" }); | ||
| 415 | }; | 470 | }; |
| 416 | 471 | ||
| 417 | const addSource = async ( | 472 | const addSource = async ( |
| @@ -430,31 +485,35 @@ function AddSourceDialog({ · 12 unmodified lines | |||
| 430 | toast.success(`Source "${sourceName}" added — loading tools…`); | 485 | toast.success(`Source "${sourceName}" added — loading tools…`); |
| 431 | }; | 486 | }; |
| 432 | 487 | ||
| 433 | const handlePresetAdd = async (preset: ApiPreset) => { | 488 | const handleCatalogAdd = async (item: CatalogCollectionItem) => { |
| 434 | setAddingPreset(preset.name); | 489 | if (!item.specUrl.trim()) { |
| 490 | toast.error("Missing OpenAPI spec URL for this API source"); | ||
| 491 | return; | ||
| 492 | } | ||
| 493 | |||
| 494 | setAddingCatalogId(item.id); | ||
| 435 | try { | 495 | try { |
| 436 | const config: Record<string, unknown> = | 496 | const sourceName = getUniqueAutoSourceName(catalogSourceName(item)); |
| 437 | preset.type === "mcp" | 497 | await addSource(sourceName, "openapi", { |
| 438 | ? { url: preset.url } | 498 | spec: item.specUrl, |
| 439 | : preset.type === "graphql" | 499 | }); |
| 440 | ? { endpoint: preset.endpoint } | 500 | reserveSourceName(sourceName); |
| 441 | : { | 501 | } catch (error) { |
| 442 | spec: preset.spec, | 502 | toast.error(error instanceof Error ? error.message : "Failed to add API source"); |
| 443 | ...(preset.baseUrl ? { baseUrl: preset.baseUrl } : {}), | ||
| 444 | }; | ||
| 445 | await addSource(preset.name, preset.type, config); | ||
| 446 | if (preset.authNote) { | ||
| 447 | toast.info(preset.authNote, { duration: 6000 }); | ||
| 448 | } | ||
| 449 | } catch (err) { | ||
| 450 | toast.error(err instanceof Error ? err.message : "Failed to add source"); | ||
| 451 | } finally { | 503 | } finally { |
| 452 | setAddingPreset(null); | 504 | setAddingCatalogId(null); |
| 453 | } | 505 | } |
| 454 | }; | 506 | }; |
| 455 | 507 | ||
| 456 | const handleCustomSubmit = async () => { | 508 | const handleCustomSubmit = async () => { |
| 457 | if (!context || !name.trim() || !endpoint.trim()) return; | 509 | if (!context || !name.trim() || !endpoint.trim()) return; |
| 510 | |||
| 511 | const takenNames = [...getTakenSourceNames()].map((entry) => entry.toLowerCase()); | ||
| 512 | if (takenNames.includes(name.trim().toLowerCase())) { | ||
| 513 | toast.error(`Source name "${name.trim()}" already exists`); | ||
| 514 | return; | ||
| 515 | } | ||
| 516 | |||
| 458 | setSubmitting(true); | 517 | setSubmitting(true); |
| 459 | try { | 518 | try { |
| 460 | const config: Record<string, unknown> = | 519 | const config: Record<string, unknown> = |
| @@ -470,7 +529,8 @@ function AddSourceDialog({ · 9 unmodified lines | |||
| 470 | ? { endpoint: endpoint } | 529 | ? { endpoint: endpoint } |
| 471 | : { spec: endpoint, ...(baseUrl ? { baseUrl } : {}) }; | 530 | : { spec: endpoint, ...(baseUrl ? { baseUrl } : {}) }; |
| 472 | await addSource(name.trim(), type, config); | 531 | await addSource(name.trim(), type, config); |
| 473 | resetForm(); | 532 | reserveSourceName(name.trim()); |
| 533 | resetDialogState(); | ||
| 474 | setOpen(false); | 534 | setOpen(false); |
| 475 | } catch (err) { | 535 | } catch (err) { |
| 476 | toast.error(err instanceof Error ? err.message : "Failed to add source"); | 536 | toast.error(err instanceof Error ? err.message : "Failed to add source"); |
| @@ -495,180 +555,261 @@ function AddSourceDialog({ · 18 unmodified lines | |||
| 495 | </DialogHeader> | 555 | </DialogHeader> |
| 496 | 556 | ||
| 497 | <div className="p-5 space-y-4"> | 557 | <div className="p-5 space-y-4"> |
| 498 | {/* Custom source form — always visible */} | 558 | {view === "catalog" ? ( |
| 499 | <div className="space-y-3"> | 559 | <div className="space-y-3"> |
| 500 | <div className="space-y-1.5"> | 560 | <div className="flex items-center gap-2 flex-wrap"> |
| 501 | <Label className="text-xs text-muted-foreground">Type</Label> | 561 | <Input |
| 502 | <Select | 562 | value={catalogQuery} |
| 503 | value={type} | 563 | onChange={(event) => setCatalogQuery(event.target.value)} |
| 504 | onValueChange={(v) => setType(v as "mcp" | "openapi" | "graphql")} | 564 | placeholder="Search APIs" |
| 565 | className="h-8 text-xs font-mono bg-background flex-1 min-w-[150px]" | ||
| 566 | onKeyDown={(event) => { | ||
| 567 | if (event.key === "Enter") { | ||
| 568 | event.preventDefault(); | ||
| 569 | void loadCatalogPage({ mode: "reset" }); | ||
| 570 | } | ||
| 571 | }} | ||
| 572 | /> | ||
| 573 | <Select | ||
| 574 | value={catalogSort} | ||
| 575 | onValueChange={(value) => { | ||
| 576 | const nextSort = value as "popular" | "recent"; | ||
| 577 | setCatalogSort(nextSort); | ||
| 578 | void loadCatalogPage({ mode: "reset", sort: nextSort }); | ||
| 579 | }} | ||
| 580 | > | ||
| 581 | <SelectTrigger className="h-8 w-[105px] text-xs bg-background shrink-0"> | ||
| 582 | <SelectValue /> | ||
| 583 | </SelectTrigger> | ||
| 584 | <SelectContent> | ||
| 585 | <SelectItem value="popular" className="text-xs">Popular</SelectItem> | ||
| 586 | <SelectItem value="recent" className="text-xs">Recent</SelectItem> | ||
| 587 | </SelectContent> | ||
| 588 | </Select> | ||
| 589 | <Button | ||
| 590 | type="button" | ||
| 591 | variant="outline" | ||
| 592 | size="sm" | ||
| 593 | className="h-8 px-3 text-xs shrink-0" | ||
| 594 | onClick={() => void loadCatalogPage({ mode: "reset" })} | ||
| 595 | disabled={catalogLoading} | ||
| 596 | > | ||
| 597 | {catalogLoading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Search"} | ||
| 598 | </Button> | ||
| 599 | </div> | ||
| 600 | |||
| 601 | <p className="text-[11px] text-muted-foreground"> | ||
| 602 | Browse API collections and add them as tool sources. | ||
| 603 | {catalogTotalCount !== null ? ` Found ${catalogTotalCount.toLocaleString()} total.` : ""} | ||
| 604 | </p> | ||
| 605 | |||
| 606 | <Separator /> | ||
| 607 | |||
| 608 | <div | ||
| 609 | className="max-h-80 overflow-y-auto overflow-x-hidden space-y-1 pr-1" | ||
| 610 | onScroll={(event) => { | ||
| 611 | const target = event.currentTarget; | ||
| 612 | const nearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 120; | ||
| 613 | if (nearBottom) { | ||
| 614 | void loadCatalogPage({ mode: "next" }); | ||
| 615 | } | ||
| 616 | }} | ||
| 505 | > | 617 | > |
| 506 | <SelectTrigger className="h-8 text-xs bg-background"> | 618 | <button |
| 507 | <SelectValue /> | 619 | type="button" |
| 508 | </SelectTrigger> | 620 | onClick={() => setView("custom")} |
| 509 | <SelectContent> | 621 | className="w-full max-w-full overflow-hidden text-left px-3 py-2 rounded-md border border-border/70 bg-muted/40 hover:bg-accent/30 transition-colors" |
| 510 | <SelectItem value="mcp" className="text-xs"> | 622 | > |
| 511 | MCP Server | 623 | <div className="flex items-center gap-2"> |
| 512 | </SelectItem> | 624 | <span className="inline-flex h-6 w-6 items-center justify-center rounded bg-muted"> |
| 513 | <SelectItem value="openapi" className="text-xs"> | 625 | <Plus className="h-3.5 w-3.5 text-muted-foreground" /> |
| 514 | OpenAPI Spec | 626 | </span> |
| 515 | </SelectItem> | 627 | <div className="min-w-0 w-0 flex-1 overflow-hidden"> |
| 516 | <SelectItem value="graphql" className="text-xs"> | 628 | <p className="text-xs font-medium">Add custom source</p> |
| 517 | GraphQL | 629 | <p className="text-[10px] text-muted-foreground">MCP, OpenAPI, or GraphQL endpoint</p> |
| 518 | </SelectItem> | 630 | </div> |
| 519 | </SelectContent> | 631 | </div> |
| 520 | </Select> | 632 | </button> |
| 521 | </div> | 633 | |
| 522 | <div className="space-y-1.5"> | 634 | {catalogItems.map((item) => ( |
| 523 | <Label className="text-xs text-muted-foreground"> | 635 | <div |
| 524 | {type === "mcp" ? "Endpoint URL" : type === "graphql" ? "GraphQL Endpoint" : "Spec URL"} | 636 | key={item.id} |
| 525 | </Label> | 637 | className="w-full max-w-full overflow-hidden flex items-start gap-2 px-2 py-2 rounded-md border border-border/50" |
| 526 | <Input | 638 | > |
| 527 | value={endpoint} | 639 | {item.logoUrl && ( |
| 528 | onChange={(e) => handleEndpointChange(e.target.value)} | 640 | <img |
| 529 | placeholder={ | 641 | src={item.logoUrl} |
| 530 | type === "mcp" | 642 | alt="" |
| 531 | ? "https://mcp-server.example.com/sse" | 643 | className="w-5 h-5 rounded shrink-0 mt-0.5 object-contain" |
| 532 | : type === "graphql" | 644 | loading="lazy" |
| 533 | ? "https://api.example.com/graphql" | 645 | /> |
| 534 | : "https://api.example.com/openapi.json" | 646 | )} |
| 535 | } | 647 | <div className="flex-1 min-w-0 w-0 overflow-hidden"> |
| 536 | className="h-8 text-xs font-mono bg-background" | 648 | <p className="text-xs font-medium truncate">{item.name}</p> |
| 537 | /> | 649 | <p className="text-[10px] text-muted-foreground truncate"> |
| 538 | </div> | 650 | {item.providerName} |
| 539 | <div className="space-y-1.5"> | 651 | {item.version ? ` · v${item.version}` : ""} |
| 540 | <Label className="text-xs text-muted-foreground">Name</Label> | 652 | </p> |
| 541 | <Input | 653 | {item.summary && ( |
| 542 | value={name} | 654 | <div |
| 543 | onChange={(e) => handleNameChange(e.target.value)} | 655 | className="mt-0.5 w-full min-w-0 max-w-full overflow-hidden text-[10px] text-muted-foreground/90 leading-relaxed break-words [overflow-wrap:anywhere] [&_*]:max-w-full [&_*]:min-w-0 [&_*]:break-words [&_a]:break-all [&_a]:whitespace-normal [&_code]:break-all [&_code]:whitespace-pre-wrap [&_p]:m-0 [&_pre]:max-w-full [&_pre]:overflow-x-hidden [&_pre]:whitespace-pre-wrap [&_pre]:break-words line-clamp-2" |
| 544 | placeholder="e.g. my-service" | 656 | style={{ overflowWrap: "anywhere", wordBreak: "break-word" }} |
| 545 | className="h-8 text-xs font-mono bg-background" | 657 | > |
| 546 | /> | 658 | <Streamdown controls={false}>{item.summary}</Streamdown> |
| 659 | </div> | ||
| 660 | )} | ||
| 661 | </div> | ||
| 662 | <div className="flex items-center gap-1 shrink-0"> | ||
| 663 | <Button | ||
| 664 | type="button" | ||
| 665 | size="sm" | ||
| 666 | className="h-7 px-2 text-[11px]" | ||
| 667 | onClick={() => void handleCatalogAdd(item)} | ||
| 668 | disabled={Boolean(addingCatalogId)} | ||
| 669 | > | ||
| 670 | {addingCatalogId === item.id ? "Adding..." : "Add"} | ||
| 671 | </Button> | ||
| 672 | </div> | ||
| 673 | </div> | ||
| 674 | ))} | ||
| 675 | |||
| 676 | {catalogLoadingMore && ( | ||
| 677 | <div className="flex items-center justify-center py-2 text-[11px] text-muted-foreground"> | ||
| 678 | <Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" /> | ||
| 679 | Loading more... | ||
| 680 | </div> | ||
| 681 | )} | ||
| 682 | |||
| 683 | {!catalogLoading && !catalogLoadingMore && catalogItems.length === 0 && !catalogError && ( | ||
| 684 | <p className="text-[11px] text-muted-foreground px-1 py-1"> | ||
| 685 | No collections found for this query. | ||
| 686 | </p> | ||
| 687 | )} | ||
| 688 | |||
| 689 | {catalogError && ( | ||
| 690 | <p className="text-[11px] text-terminal-red px-1 py-1"> | ||
| 691 | {catalogError} | ||
| 692 | </p> | ||
| 693 | )} | ||
| 694 | |||
| 695 | {!catalogHasMore && catalogItems.length > 0 && ( | ||
| 696 | <p className="text-[10px] text-muted-foreground/70 px-1 py-1"> | ||
| 697 | End of results. | ||
| 698 | </p> | ||
| 699 | )} | ||
| 700 | </div> | ||
| 547 | </div> | 701 | </div> |
| 548 | {type === "openapi" && ( | 702 | ) : ( |
| 703 | <div className="space-y-3"> | ||
| 704 | <Button | ||
| 705 | type="button" | ||
| 706 | variant="ghost" | ||
| 707 | size="sm" | ||
| 708 | className="h-7 px-2 text-xs" | ||
| 709 | onClick={() => setView("catalog")} | ||
| 710 | > | ||
| 711 | <ChevronRight className="h-3.5 w-3.5 mr-1 rotate-180" /> | ||
| 712 | Back to API list | ||
| 713 | </Button> | ||
| 714 | |||
| 715 | <div className="space-y-1.5"> | ||
| 716 | <Label className="text-xs text-muted-foreground">Type</Label> | ||
| 717 | <Select | ||
| 718 | value={type} | ||
| 719 | onValueChange={(value) => setType(value as "mcp" | "openapi" | "graphql")} | ||
| 720 | > | ||
| 721 | <SelectTrigger className="h-8 text-xs bg-background"> | ||
| 722 | <SelectValue /> | ||
| 723 | </SelectTrigger> | ||
| 724 | <SelectContent> | ||
| 725 | <SelectItem value="mcp" className="text-xs">MCP Server</SelectItem> | ||
| 726 | <SelectItem value="openapi" className="text-xs">OpenAPI Spec</SelectItem> | ||
| 727 | <SelectItem value="graphql" className="text-xs">GraphQL</SelectItem> | ||
| 728 | </SelectContent> | ||
| 729 | </Select> | ||
| 730 | </div> | ||
| 731 | |||
| 549 | <div className="space-y-1.5"> | 732 | <div className="space-y-1.5"> |
| 550 | <Label className="text-xs text-muted-foreground"> | 733 | <Label className="text-xs text-muted-foreground"> |
| 551 | Base URL (optional) | 734 | {type === "mcp" ? "Endpoint URL" : type === "graphql" ? "GraphQL Endpoint" : "Spec URL"} |
| 552 | </Label> | 735 | </Label> |
| 553 | <Input | 736 | <Input |
| 554 | value={baseUrl} | 737 | value={endpoint} |
| 555 | onChange={(e) => setBaseUrl(e.target.value)} | 738 | onChange={(event) => handleEndpointChange(event.target.value)} |
| 556 | placeholder="https://api.example.com" | 739 | placeholder={ |
| 740 | type === "mcp" | ||
| 741 | ? "https://mcp-server.example.com/sse" | ||
| 742 | : type === "graphql" | ||
| 743 | ? "https://api.example.com/graphql" | ||
| 744 | : "https://api.example.com/openapi.json" | ||
| 745 | } | ||
| 557 | className="h-8 text-xs font-mono bg-background" | 746 | className="h-8 text-xs font-mono bg-background" |
| 558 | /> | 747 | /> |
| 559 | </div> | 748 | </div> |
| 560 | )} | 749 | |
| 561 | {type === "mcp" && ( | 750 | <div className="space-y-1.5"> |
| 562 | <> | 751 | <Label className="text-xs text-muted-foreground">Name</Label> |
| 563 | <div className="space-y-1.5"> | 752 | <Input |
| 564 | <Label className="text-xs text-muted-foreground">Transport</Label> | 753 | value={name} |
| 565 | <Select | 754 | onChange={(event) => handleNameChange(event.target.value)} |
| 566 | value={mcpTransport} | 755 | placeholder="e.g. my-service" |
| 567 | onValueChange={(v) => setMcpTransport(v as "auto" | "streamable-http" | "sse")} | 756 | className="h-8 text-xs font-mono bg-background" |
| 568 | > | 757 | /> |
| 569 | <SelectTrigger className="h-8 text-xs bg-background"> | 758 | </div> |
| 570 | <SelectValue /> | 759 | |
| 571 | </SelectTrigger> | 760 | {type === "openapi" && ( |
| 572 | <SelectContent> | ||
| 573 | <SelectItem value="auto" className="text-xs"> | ||
| 574 | Auto (streamable, then SSE) | ||
| 575 | </SelectItem> | ||
| 576 | <SelectItem value="streamable-http" className="text-xs"> | ||
| 577 | Streamable HTTP | ||
| 578 | </SelectItem> | ||
| 579 | <SelectItem value="sse" className="text-xs"> | ||
| 580 | SSE | ||
| 581 | </SelectItem> | ||
| 582 | </SelectContent> | ||
| 583 | </Select> | ||
| 584 | </div> | ||
| 585 | <div className="space-y-1.5"> | 761 | <div className="space-y-1.5"> |
| 586 | <Label className="text-xs text-muted-foreground"> | 762 | <Label className="text-xs text-muted-foreground">Base URL (optional)</Label> |
| 587 | Anon actor query key (optional) | ||
| 588 | </Label> | ||
| 589 | <Input | 763 | <Input |
| 590 | value={mcpActorQueryParamKey} | 764 | value={baseUrl} |
| 591 | onChange={(e) => setMcpActorQueryParamKey(e.target.value)} | 765 | onChange={(event) => setBaseUrl(event.target.value)} |
| 592 | placeholder="userId" | 766 | placeholder="https://api.example.com" |
| 593 | className="h-8 text-xs font-mono bg-background" | 767 | className="h-8 text-xs font-mono bg-background" |
| 594 | /> | 768 | /> |
| 595 | </div> | 769 | </div> |
| 596 | </> | 770 | )} |
| 597 | )} | ||
| 598 | <Button | ||
| 599 | onClick={handleCustomSubmit} | ||
| 600 | disabled={submitting || !name.trim() || !endpoint.trim()} | ||
| 601 | className="w-full h-9" | ||
| 602 | size="sm" | ||
| 603 | > | ||
| 604 | {submitting ? "Adding..." : "Add Source"} | ||
| 605 | </Button> | ||
| 606 | </div> | ||
| 607 | 771 | ||
| 608 | {/* Collapsible presets */} | 772 | {type === "mcp" && ( |
| 609 | <Separator /> | 773 | <> |
| 610 | <Collapsible open={presetsOpen} onOpenChange={setPresetsOpen}> | 774 | <div className="space-y-1.5"> |
| 611 | <CollapsibleTrigger className="flex items-center gap-2 w-full py-1 text-xs text-muted-foreground hover:text-foreground transition-colors group"> | 775 | <Label className="text-xs text-muted-foreground">Transport</Label> |
| 612 | <ChevronRight | 776 | <Select |
| 613 | className={cn( | 777 | value={mcpTransport} |
| 614 | "h-3.5 w-3.5 transition-transform duration-200", | 778 | onValueChange={(value) => setMcpTransport(value as "auto" | "streamable-http" | "sse")} |
| 615 | presetsOpen && "rotate-90", | ||
| 616 | )} | ||
| 617 | /> | ||
| 618 | <span>Quick add from catalog</span> | ||
| 619 | <span className="text-[10px] font-mono text-muted-foreground/60"> | ||
| 620 | {API_PRESETS.length} | ||
| 621 | </span> | ||
| 622 | </CollapsibleTrigger> | ||
| 623 | <CollapsibleContent> | ||
| 624 | <div className="pt-3 space-y-1"> | ||
| 625 | {API_PRESETS.map((preset) => { | ||
| 626 | const alreadyAdded = existingSourceNames.has(preset.name); | ||
| 627 | return ( | ||
| 628 | <button | ||
| 629 | key={preset.name} | ||
| 630 | onClick={() => !alreadyAdded && handlePresetAdd(preset)} | ||
| 631 | disabled={alreadyAdded || addingPreset !== null} | ||
| 632 | className={cn( | ||
| 633 | "flex items-center gap-3 w-full px-3 py-2 rounded-md text-left transition-colors", | ||
| 634 | alreadyAdded | ||
| 635 | ? "opacity-50 cursor-default" | ||
| 636 | : "hover:bg-accent/30 cursor-pointer", | ||
| 637 | )} | ||
| 638 | > | 779 | > |
| 639 | <span className="flex items-center justify-center h-6 w-6 rounded bg-muted shrink-0 overflow-hidden"> | 780 | <SelectTrigger className="h-8 text-xs bg-background"> |
| 640 | {(() => { | 781 | <SelectValue /> |
| 641 | const favicon = getFaviconUrl(preset); | 782 | </SelectTrigger> |
| 642 | return favicon ? ( | 783 | <SelectContent> |
| 643 | <img | 784 | <SelectItem value="auto" className="text-xs">Auto (streamable, then SSE)</SelectItem> |
| 644 | src={favicon} | 785 | <SelectItem value="streamable-http" className="text-xs">Streamable HTTP</SelectItem> |
| 645 | alt="" | 786 | <SelectItem value="sse" className="text-xs">SSE</SelectItem> |
| 646 | width={16} | 787 | </SelectContent> |
| 647 | height={16} | 788 | </Select> |
| 648 | className="w-4 h-4" | 789 | </div> |
| 649 | loading="lazy" | 790 | |
| 650 | /> | 791 | <div className="space-y-1.5"> |
| 651 | ) : ( | 792 | <Label className="text-xs text-muted-foreground">Anon actor query key (optional)</Label> |
| 652 | <span className="text-[9px] font-bold font-mono text-muted-foreground"> | 793 | <Input |
| 653 | {preset.name.slice(0, 2).toUpperCase()} | 794 | value={mcpActorQueryParamKey} |
| 654 | </span> | 795 | onChange={(event) => setMcpActorQueryParamKey(event.target.value)} |
| 655 | ); | 796 | placeholder="userId" |
| 656 | })()} | 797 | className="h-8 text-xs font-mono bg-background" |
| 657 | </span> | 798 | /> |
| 658 | <div className="flex-1 min-w-0"> | 799 | </div> |
| 659 | <span className="text-xs font-medium text-foreground"> | 800 | </> |
| 660 | {preset.label} | 801 | )} |
| 661 | </span> | 802 | |
| 662 | <span className="text-[11px] text-muted-foreground ml-2"> | 803 | <Button |
| 663 | {preset.description} | 804 | onClick={handleCustomSubmit} |
| 664 | </span> | 805 | disabled={submitting || !name.trim() || !endpoint.trim()} |
| 665 | </div> | 806 | className="w-full h-9" |
| 666 | </button> | 807 | size="sm" |
| 667 | ); | 808 | > |
| 668 | })} | 809 | {submitting ? "Adding..." : "Add Source"} |
| 669 | </div> | 810 | </Button> |
| 670 | </CollapsibleContent> | 811 | </div> |
| 671 | </Collapsible> | 812 | )} |
| 672 | </div> | 813 | </div> |
| 673 | </DialogContent> | 814 | </DialogContent> |
| 674 | </Dialog> | 815 | </Dialog> |
| @@ -918,11 +1059,7 @@ function SourceCard({ · 243 unmodified lines | |||
| 918 | )} | 1059 | )} |
| 919 | </div> | 1060 | </div> |
| 920 | <span className="text-[11px] text-muted-foreground font-mono truncate block"> | 1061 | <span className="text-[11px] text-muted-foreground font-mono truncate block"> |
| 921 | {source.type === "mcp" | 1062 | {sourceEndpointLabel(source)} |
| 922 | ? (source.config.url as string) | ||
| 923 | : source.type === "graphql" | ||
| 924 | ? (source.config.endpoint as string) | ||
| 925 | : (source.config.spec as string)} | ||
| 926 | </span> | 1063 | </span> |
| 927 | {source.type === "openapi" && quality && ( | 1064 | {source.type === "openapi" && quality && ( |
| 928 | <span className="text-[10px] text-muted-foreground/90 font-mono truncate block mt-0.5"> | 1065 | <span className="text-[10px] text-muted-foreground/90 font-mono truncate block mt-0.5"> |
| @@ -5,12 +5,17 @@ · 4 unmodified lines | |||
| 5 | "": { | 5 | "": { |
| 6 | "name": "executor", | 6 | "name": "executor", |
| 7 | "dependencies": { | 7 | "dependencies": { |
| 8 | "better-result": "^2.7.0", | ||
| 8 | "convex": "^1.31.7", | 9 | "convex": "^1.31.7", |
| 9 | "graphql": "^16.12.0", | 10 | "graphql": "^16.12.0", |
| 10 | }, | 11 | }, |
| 11 | }, | 12 | }, |
| 12 | }, | 13 | }, |
| 13 | "packages": { | 14 | "packages": { |
| 15 | "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="], | ||
| 16 | |||
| 17 | "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], | ||
| 18 | |||
| 14 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], | 19 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], |
| 15 | 20 | ||
| 16 | "@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], | 21 | "@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], |
| @@ -63,12 +68,18 @@ · 46 unmodified lines | |||
| 63 | 68 | ||
| 64 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], | 69 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], |
| 65 | 70 | ||
| 71 | "better-result": ["better-result@2.7.0", "", { "dependencies": { "@clack/prompts": "^0.11.0" }, "bin": { "better-result": "bin/cli.mjs" } }, "sha512-7zrmXjAK8u8Z6SOe4R65XObOR5X+Y2I/VVku3t5cPOGQ8/WsBcfFmfnIPiEl5EBMDOzPHRwbiPbMtQBKYdw7RA=="], | ||
| 72 | |||
| 66 | "convex": ["convex@1.31.7", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-PtNMe1mAIOvA8Yz100QTOaIdgt2rIuWqencVXrb4McdhxBHZ8IJ1eXTnrgCC9HydyilGT1pOn+KNqT14mqn9fQ=="], | 73 | "convex": ["convex@1.31.7", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-PtNMe1mAIOvA8Yz100QTOaIdgt2rIuWqencVXrb4McdhxBHZ8IJ1eXTnrgCC9HydyilGT1pOn+KNqT14mqn9fQ=="], |
| 67 | 74 | ||
| 68 | "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=="], | 75 | "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=="], |
| 69 | 76 | ||
| 70 | "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="], | 77 | "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="], |
| 71 | 78 | ||
| 79 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], | ||
| 80 | |||
| 72 | "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], | 81 | "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], |
| 82 | |||
| 83 | "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], | ||
| 73 | } | 84 | } |
| 74 | } | 85 | } |
| @@ -276,6 +276,47 @@ export declare const api: { · 275 unmodified lines | |||
| 276 | any | 276 | any |
| 277 | >; | 277 | >; |
| 278 | }; | 278 | }; |
| 279 | runtimeCallbacks: { | ||
| 280 | appendOutput: FunctionReference< | ||
| 281 | "mutation", | ||
| 282 | "public", | ||
| 283 | { | ||
| 284 | internalSecret: string; | ||
| 285 | line: string; | ||
| 286 | runId: string; | ||
| 287 | stream: "stdout" | "stderr"; | ||
| 288 | timestamp?: number; | ||
| 289 | }, | ||
| 290 | any | ||
| 291 | >; | ||
| 292 | completeRun: FunctionReference< | ||
| 293 | "mutation", | ||
| 294 | "public", | ||
| 295 | { | ||
| 296 | durationMs?: number; | ||
| 297 | error?: string; | ||
| 298 | exitCode?: number; | ||
| 299 | internalSecret: string; | ||
| 300 | runId: string; | ||
| 301 | status: "completed" | "failed" | "timed_out" | "denied"; | ||
| 302 | stderr?: string; | ||
| 303 | stdout?: string; | ||
| 304 | }, | ||
| 305 | any | ||
| 306 | >; | ||
| 307 | handleToolCall: FunctionReference< | ||
| 308 | "action", | ||
| 309 | "public", | ||
| 310 | { | ||
| 311 | callId: string; | ||
| 312 | input?: any; | ||
| 313 | internalSecret: string; | ||
| 314 | runId: string; | ||
| 315 | toolPath: string; | ||
| 316 | }, | ||
| 317 | any | ||
| 318 | >; | ||
| 319 | }; | ||
| 279 | workspace: { | 320 | workspace: { |
| 280 | bootstrapAnonymousSession: FunctionReference< | 321 | bootstrapAnonymousSession: FunctionReference< |
| 281 | "mutation", | 322 | "mutation", |
| @@ -520,6 +561,18 @@ export declare const internal: { · 238 unmodified lines | |||
| 520 | { sourceId: string; workspaceId: Id<"workspaces"> }, | 561 | { sourceId: string; workspaceId: Id<"workspaces"> }, |
| 521 | any | 562 | any |
| 522 | >; | 563 | >; |
| 564 | finishToolCall: FunctionReference< | ||
| 565 | "mutation", | ||
| 566 | "internal", | ||
| 567 | { | ||
| 568 | callId: string; | ||
| 569 | error?: string; | ||
| 570 | output?: any; | ||
| 571 | status: "completed" | "failed" | "denied"; | ||
| 572 | taskId: string; | ||
| 573 | }, | ||
| 574 | any | ||
| 575 | >; | ||
| 523 | getActiveAnonymousOauthSigningKey: FunctionReference< | 576 | getActiveAnonymousOauthSigningKey: FunctionReference< |
| 524 | "query", | 577 | "query", |
| 525 | "internal", | 578 | "internal", |
| @@ -551,6 +604,12 @@ export declare const internal: { · 25 unmodified lines | |||
| 551 | { taskId: string; workspaceId: Id<"workspaces"> }, | 604 | { taskId: string; workspaceId: Id<"workspaces"> }, |
| 552 | any | 605 | any |
| 553 | >; | 606 | >; |
| 607 | getToolCall: FunctionReference< | ||
| 608 | "query", | ||
| 609 | "internal", | ||
| 610 | { callId: string; taskId: string }, | ||
| 611 | any | ||
| 612 | >; | ||
| 554 | listAccessPolicies: FunctionReference< | 613 | listAccessPolicies: FunctionReference< |
| 555 | "query", | 614 | "query", |
| 556 | "internal", | 615 | "internal", |
| @@ -598,6 +657,12 @@ export declare const internal: { · 41 unmodified lines | |||
| 598 | { workspaceId: Id<"workspaces"> }, | 657 | { workspaceId: Id<"workspaces"> }, |
| 599 | any | 658 | any |
| 600 | >; | 659 | >; |
| 660 | listToolCalls: FunctionReference< | ||
| 661 | "query", | ||
| 662 | "internal", | ||
| 663 | { taskId: string }, | ||
| 664 | any | ||
| 665 | >; | ||
| 601 | listToolSources: FunctionReference< | 666 | listToolSources: FunctionReference< |
| 602 | "query", | 667 | "query", |
| 603 | "internal", | 668 | "internal", |
| @@ -651,6 +716,12 @@ export declare const internal: { · 47 unmodified lines | |||
| 651 | }, | 716 | }, |
| 652 | any | 717 | any |
| 653 | >; | 718 | >; |
| 719 | setToolCallPendingApproval: FunctionReference< | ||
| 720 | "mutation", | ||
| 721 | "internal", | ||
| 722 | { approvalId: string; callId: string; taskId: string }, | ||
| 723 | any | ||
| 724 | >; | ||
| 654 | storeAnonymousOauthSigningKey: FunctionReference< | 725 | storeAnonymousOauthSigningKey: FunctionReference< |
| 655 | "mutation", | 726 | "mutation", |
| 656 | "internal", | 727 | "internal", |
| @@ -690,6 +761,18 @@ export declare const internal: { · 33 unmodified lines | |||
| 690 | }, | 761 | }, |
| 691 | any | 762 | any |
| 692 | >; | 763 | >; |
| 764 | upsertToolCallRequested: FunctionReference< | ||
| 765 | "mutation", | ||
| 766 | "internal", | ||
| 767 | { | ||
| 768 | callId: string; | ||
| 769 | input?: any; | ||
| 770 | taskId: string; | ||
| 771 | toolPath: string; | ||
| 772 | workspaceId: Id<"workspaces">; | ||
| 773 | }, | ||
| 774 | any | ||
| 775 | >; | ||
| 693 | upsertToolSource: FunctionReference< | 776 | upsertToolSource: FunctionReference< |
| 694 | "mutation", | 777 | "mutation", |
| 695 | "internal", | 778 | "internal", |
| @@ -716,6 +799,20 @@ export declare const internal: { · 20 unmodified lines | |||
| 716 | }, | 799 | }, |
| 717 | any | 800 | any |
| 718 | >; | 801 | >; |
| 802 | completeRuntimeRun: FunctionReference< | ||
| 803 | "mutation", | ||
| 804 | "internal", | ||
| 805 | { | ||
| 806 | durationMs?: number; | ||
| 807 | error?: string; | ||
| 808 | exitCode?: number; | ||
| 809 | runId: string; | ||
| 810 | status: "completed" | "failed" | "timed_out" | "denied"; | ||
| 811 | stderr?: string; | ||
| 812 | stdout?: string; | ||
| 813 | }, | ||
| 814 | any | ||
| 815 | >; | ||
| 719 | createTaskInternal: FunctionReference< | 816 | createTaskInternal: FunctionReference< |
| 720 | "mutation", | 817 | "mutation", |
| 721 | "internal", | 818 | "internal", |
| @@ -16,6 +16,11 @@ const completedTaskStatusValidator = v.union( · 15 unmodified lines | |||
| 16 | v.literal("denied"), | 16 | v.literal("denied"), |
| 17 | ); | 17 | ); |
| 18 | const approvalStatusValidator = v.union(v.literal("pending"), v.literal("approved"), v.literal("denied")); | 18 | const approvalStatusValidator = v.union(v.literal("pending"), v.literal("approved"), v.literal("denied")); |
| 19 | const terminalToolCallStatusValidator = v.union( | ||
| 20 | v.literal("completed"), | ||
| 21 | v.literal("failed"), | ||
| 22 | v.literal("denied"), | ||
| 23 | ); | ||
| 19 | const policyDecisionValidator = v.union(v.literal("allow"), v.literal("require_approval"), v.literal("deny")); | 24 | const policyDecisionValidator = v.union(v.literal("allow"), v.literal("require_approval"), v.literal("deny")); |
| 20 | const credentialScopeValidator = v.union(v.literal("workspace"), v.literal("actor")); | 25 | const credentialScopeValidator = v.union(v.literal("workspace"), v.literal("actor")); |
| 21 | const credentialProviderValidator = v.union( | 26 | const credentialProviderValidator = v.union( |
| @@ -127,6 +132,23 @@ function mapApproval(doc: Doc<"approvals">) { · 105 unmodified lines | |||
| 127 | }; | 132 | }; |
| 128 | } | 133 | } |
| 129 | 134 | ||
| 135 | function mapToolCall(doc: Doc<"toolCalls">) { | ||
| 136 | return { | ||
| 137 | taskId: doc.taskId, | ||
| 138 | callId: doc.callId, | ||
| 139 | workspaceId: doc.workspaceId, | ||
| 140 | toolPath: doc.toolPath, | ||
| 141 | input: doc.input, | ||
| 142 | status: doc.status, | ||
| 143 | approvalId: doc.approvalId, | ||
| 144 | output: doc.output, | ||
| 145 | error: doc.error, | ||
| 146 | createdAt: doc.createdAt, | ||
| 147 | updatedAt: doc.updatedAt, | ||
| 148 | completedAt: doc.completedAt, | ||
| 149 | }; | ||
| 150 | } | ||
| 151 | |||
| 130 | function mapPolicy(doc: Doc<"accessPolicies">) { | 152 | function mapPolicy(doc: Doc<"accessPolicies">) { |
| 131 | return { | 153 | return { |
| 132 | id: doc.policyId, | 154 | id: doc.policyId, |
| @@ -309,6 +331,17 @@ async function getApprovalDoc(ctx: { db: QueryCtx["db"] }, approvalId: string) { · 176 unmodified lines | |||
| 309 | .unique(); | 331 | .unique(); |
| 310 | } | 332 | } |
| 311 | 333 | ||
| 334 | async function getToolCallDoc( | ||
| 335 | ctx: { db: QueryCtx["db"] }, | ||
| 336 | taskId: string, | ||
| 337 | callId: string, | ||
| 338 | ) { | ||
| 339 | return await ctx.db | ||
| 340 | .query("toolCalls") | ||
| 341 | .withIndex("by_task_call", (q) => q.eq("taskId", taskId).eq("callId", callId)) | ||
| 342 | .unique(); | ||
| 343 | } | ||
| 344 | |||
| 312 | export const createTask = internalMutation({ | 345 | export const createTask = internalMutation({ |
| 313 | args: { | 346 | args: { |
| 314 | id: v.string(), | 347 | id: v.string(), |
| @@ -391,6 +424,11 @@ export const listRuntimeTargets = internalQuery({ · 76 unmodified lines | |||
| 391 | label: "Local JS Runtime", | 424 | label: "Local JS Runtime", |
| 392 | description: "Runs generated code in-process using Bun", | 425 | description: "Runs generated code in-process using Bun", |
| 393 | }, | 426 | }, |
| 427 | { | ||
| 428 | id: "cloudflare-worker-loader", | ||
| 429 | label: "Cloudflare Worker Loader", | ||
| 430 | description: "Runs generated code in a Cloudflare Worker", | ||
| 431 | } | ||
| 394 | ]; | 432 | ]; |
| 395 | }, | 433 | }, |
| 396 | }); | 434 | }); |
| @@ -441,6 +479,10 @@ export const markTaskFinished = internalMutation({ · 44 unmodified lines | |||
| 441 | return null; | 479 | return null; |
| 442 | } | 480 | } |
| 443 | 481 | ||
| 482 | if (doc.status === "completed" || doc.status === "failed" || doc.status === "timed_out" || doc.status === "denied") { | ||
| 483 | return mapTask(doc); | ||
| 484 | } | ||
| 485 | |||
| 444 | const now = Date.now(); | 486 | const now = Date.now(); |
| 445 | await ctx.db.patch(doc._id, { | 487 | await ctx.db.patch(doc._id, { |
| 446 | status: args.status, | 488 | status: args.status, |
| @@ -607,6 +649,132 @@ export const getApprovalInWorkspace = internalQuery({ · 160 unmodified lines | |||
| 607 | }, | 649 | }, |
| 608 | }); | 650 | }); |
| 609 | 651 | ||
| 652 | export const upsertToolCallRequested = internalMutation({ | ||
| 653 | args: { | ||
| 654 | taskId: v.string(), | ||
| 655 | callId: v.string(), | ||
| 656 | workspaceId: v.id("workspaces"), | ||
| 657 | toolPath: v.string(), | ||
| 658 | input: v.optional(v.any()), | ||
| 659 | }, | ||
| 660 | handler: async (ctx, args) => { | ||
| 661 | const existing = await getToolCallDoc(ctx, args.taskId, args.callId); | ||
| 662 | if (existing) { | ||
| 663 | return mapToolCall(existing); | ||
| 664 | } | ||
| 665 | |||
| 666 | const now = Date.now(); | ||
| 667 | await ctx.db.insert("toolCalls", { | ||
| 668 | taskId: args.taskId, | ||
| 669 | callId: args.callId, | ||
| 670 | workspaceId: args.workspaceId, | ||
| 671 | toolPath: args.toolPath, | ||
| 672 | input: args.input ?? {}, | ||
| 673 | status: "requested", | ||
| 674 | createdAt: now, | ||
| 675 | updatedAt: now, | ||
| 676 | }); | ||
| 677 | |||
| 678 | const created = await getToolCallDoc(ctx, args.taskId, args.callId); | ||
| 679 | if (!created) { | ||
| 680 | throw new Error(`Failed to create tool call ${args.taskId}/${args.callId}`); | ||
| 681 | } | ||
| 682 | return mapToolCall(created); | ||
| 683 | }, | ||
| 684 | }); | ||
| 685 | |||
| 686 | export const getToolCall = internalQuery({ | ||
| 687 | args: { | ||
| 688 | taskId: v.string(), | ||
| 689 | callId: v.string(), | ||
| 690 | }, | ||
| 691 | handler: async (ctx, args) => { | ||
| 692 | const doc = await getToolCallDoc(ctx, args.taskId, args.callId); | ||
| 693 | return doc ? mapToolCall(doc) : null; | ||
| 694 | }, | ||
| 695 | }); | ||
| 696 | |||
| 697 | export const setToolCallPendingApproval = internalMutation({ | ||
| 698 | args: { | ||
| 699 | taskId: v.string(), | ||
| 700 | callId: v.string(), | ||
| 701 | approvalId: v.string(), | ||
| 702 | }, | ||
| 703 | handler: async (ctx, args) => { | ||
| 704 | const doc = await getToolCallDoc(ctx, args.taskId, args.callId); | ||
| 705 | if (!doc) { | ||
| 706 | throw new Error(`Tool call not found: ${args.taskId}/${args.callId}`); | ||
| 707 | } | ||
| 708 | |||
| 709 | if (doc.status === "completed" || doc.status === "failed" || doc.status === "denied") { | ||
| 710 | return mapToolCall(doc); | ||
| 711 | } | ||
| 712 | |||
| 713 | const now = Date.now(); | ||
| 714 | await ctx.db.patch(doc._id, { | ||
| 715 | status: "pending_approval", | ||
| 716 | approvalId: args.approvalId, | ||
| 717 | updatedAt: now, | ||
| 718 | }); | ||
| 719 | |||
| 720 | const updated = await getToolCallDoc(ctx, args.taskId, args.callId); | ||
| 721 | if (!updated) { | ||
| 722 | throw new Error(`Failed to read tool call ${args.taskId}/${args.callId}`); | ||
| 723 | } | ||
| 724 | return mapToolCall(updated); | ||
| 725 | }, | ||
| 726 | }); | ||
| 727 | |||
| 728 | export const finishToolCall = internalMutation({ | ||
| 729 | args: { | ||
| 730 | taskId: v.string(), | ||
| 731 | callId: v.string(), | ||
| 732 | status: terminalToolCallStatusValidator, | ||
| 733 | output: v.optional(v.any()), | ||
| 734 | error: v.optional(v.string()), | ||
| 735 | }, | ||
| 736 | handler: async (ctx, args) => { | ||
| 737 | const doc = await getToolCallDoc(ctx, args.taskId, args.callId); | ||
| 738 | if (!doc) { | ||
| 739 | throw new Error(`Tool call not found: ${args.taskId}/${args.callId}`); | ||
| 740 | } | ||
| 741 | |||
| 742 | if (doc.status === "completed" || doc.status === "failed" || doc.status === "denied") { | ||
| 743 | return mapToolCall(doc); | ||
| 744 | } | ||
| 745 | |||
| 746 | const now = Date.now(); | ||
| 747 | await ctx.db.patch(doc._id, { | ||
| 748 | status: args.status, | ||
| 749 | output: args.output, | ||
| 750 | error: args.error, | ||
| 751 | updatedAt: now, | ||
| 752 | completedAt: now, | ||
| 753 | }); | ||
| 754 | |||
| 755 | const updated = await getToolCallDoc(ctx, args.taskId, args.callId); | ||
| 756 | if (!updated) { | ||
| 757 | throw new Error(`Failed to read tool call ${args.taskId}/${args.callId}`); | ||
| 758 | } | ||
| 759 | return mapToolCall(updated); | ||
| 760 | }, | ||
| 761 | }); | ||
| 762 | |||
| 763 | export const listToolCalls = internalQuery({ | ||
| 764 | args: { | ||
| 765 | taskId: v.string(), | ||
| 766 | }, | ||
| 767 | handler: async (ctx, args) => { | ||
| 768 | const docs = await ctx.db | ||
| 769 | .query("toolCalls") | ||
| 770 | .withIndex("by_task_created", (q) => q.eq("taskId", args.taskId)) | ||
| 771 | .order("asc") | ||
| 772 | .collect(); | ||
| 773 | |||
| 774 | return docs.map(mapToolCall); | ||
| 775 | }, | ||
| 776 | }); | ||
| 777 | |||
| 610 | export const bootstrapAnonymousSession = internalMutation({ | 778 | export const bootstrapAnonymousSession = internalMutation({ |
| 611 | args: { sessionId: v.optional(v.string()) }, | 779 | args: { sessionId: v.optional(v.string()) }, |
| 612 | handler: async (ctx, args) => { | 780 | handler: async (ctx, args) => { |
| @@ -5,6 +5,7 @@ import type { MutationCtx } from "./_generated/server"; · 4 unmodified lines | |||
| 5 | import { internalMutation } from "./_generated/server"; | 5 | import { internalMutation } from "./_generated/server"; |
| 6 | import { workspaceMutation } from "../lib/functionBuilders"; | 6 | import { workspaceMutation } from "../lib/functionBuilders"; |
| 7 | import { actorIdForAccount } from "../lib/identity"; | 7 | import { actorIdForAccount } from "../lib/identity"; |
| 8 | import { isKnownRuntimeId } from "../lib/runtimes/runtime_catalog"; | ||
| 8 | import type { ApprovalRecord, TaskRecord } from "../lib/types"; | 9 | import type { ApprovalRecord, TaskRecord } from "../lib/types"; |
| 9 | 10 | ||
| 10 | const DEFAULT_TIMEOUT_MS = 300_000; | 11 | const DEFAULT_TIMEOUT_MS = 300_000; |
| @@ -21,6 +22,14 @@ async function publishTaskEvent( · 10 unmodified lines | |||
| 21 | await ctx.runMutation(internal.database.createTaskEvent, input); | 22 | await ctx.runMutation(internal.database.createTaskEvent, input); |
| 22 | } | 23 | } |
| 23 | 24 | ||
| 25 | function terminalEventForStatus(status: "completed" | "failed" | "timed_out" | "denied"): | ||
| 26 | "task.completed" | "task.failed" | "task.timed_out" | "task.denied" { | ||
| 27 | if (status === "completed") return "task.completed"; | ||
| 28 | if (status === "timed_out") return "task.timed_out"; | ||
| 29 | if (status === "denied") return "task.denied"; | ||
| 30 | return "task.failed"; | ||
| 31 | } | ||
| 32 | |||
| 24 | async function createTaskRecord( | 33 | async function createTaskRecord( |
| 25 | ctx: MutationCtx, | 34 | ctx: MutationCtx, |
| 26 | args: { | 35 | args: { |
| @@ -38,7 +47,7 @@ async function createTaskRecord( · 11 unmodified lines | |||
| 38 | } | 47 | } |
| 39 | 48 | ||
| 40 | const runtimeId = args.runtimeId ?? "local-bun"; | 49 | const runtimeId = args.runtimeId ?? "local-bun"; |
| 41 | if (runtimeId !== "local-bun") { | 50 | if (!isKnownRuntimeId(runtimeId)) { |
| 42 | throw new Error(`Unsupported runtime: ${runtimeId}`); | 51 | throw new Error(`Unsupported runtime: ${runtimeId}`); |
| 43 | } | 52 | } |
| 44 | 53 | ||
| @@ -235,3 +244,54 @@ export const appendRuntimeOutput = internalMutation({ · 190 unmodified lines | |||
| 235 | return { ok: true as const }; | 244 | return { ok: true as const }; |
| 236 | }, | 245 | }, |
| 237 | }); | 246 | }); |
| 247 | |||
| 248 | export const completeRuntimeRun = internalMutation({ | ||
| 249 | args: { | ||
| 250 | runId: v.string(), | ||
| 251 | status: v.union(v.literal("completed"), v.literal("failed"), v.literal("timed_out"), v.literal("denied")), | ||
| 252 | stdout: v.optional(v.string()), | ||
| 253 | stderr: v.optional(v.string()), | ||
| 254 | exitCode: v.optional(v.number()), | ||
| 255 | error: v.optional(v.string()), | ||
| 256 | durationMs: v.optional(v.number()), | ||
| 257 | }, | ||
| 258 | handler: async (ctx, args) => { | ||
| 259 | const task = (await ctx.runQuery(internal.database.getTask, { taskId: args.runId })) as TaskRecord | null; | ||
| 260 | if (!task) { | ||
| 261 | return { ok: false as const, error: `Run not found: ${args.runId}` }; | ||
| 262 | } | ||
| 263 | |||
| 264 | if (task.status === "completed" || task.status === "failed" || task.status === "timed_out" || task.status === "denied") { | ||
| 265 | return { ok: true as const, alreadyFinal: true as const, task }; | ||
| 266 | } | ||
| 267 | |||
| 268 | const finished = await ctx.runMutation(internal.database.markTaskFinished, { | ||
| 269 | taskId: args.runId, | ||
| 270 | status: args.status, | ||
| 271 | stdout: args.stdout ?? "", | ||
| 272 | stderr: args.stderr ?? "", | ||
| 273 | exitCode: args.exitCode, | ||
| 274 | error: args.error, | ||
| 275 | }); | ||
| 276 | |||
| 277 | if (!finished) { | ||
| 278 | return { ok: false as const, error: `Failed to mark run finished: ${args.runId}` }; | ||
| 279 | } | ||
| 280 | |||
| 281 | await publishTaskEvent(ctx, { | ||
| 282 | taskId: args.runId, | ||
| 283 | eventName: "task", | ||
| 284 | type: terminalEventForStatus(args.status), | ||
| 285 | payload: { | ||
| 286 | taskId: args.runId, | ||
| 287 | status: finished.status, | ||
| 288 | exitCode: finished.exitCode, | ||
| 289 | durationMs: args.durationMs, | ||
| 290 | error: finished.error, | ||
| 291 | completedAt: finished.completedAt, | ||
| 292 | }, | ||
| 293 | }); | ||
| 294 | |||
| 295 | return { ok: true as const, alreadyFinal: false as const, task: finished }; | ||
| 296 | }, | ||
| 297 | }); | ||
| @@ -7,8 +7,15 @@ import { action, internalAction } from "./_generated/server"; · 6 unmodified lines | |||
| 7 | import type { ActionCtx } from "./_generated/server"; | 7 | import type { ActionCtx } from "./_generated/server"; |
| 8 | import { InProcessExecutionAdapter } from "../lib/adapters/in_process_execution_adapter"; | 8 | import { InProcessExecutionAdapter } from "../lib/adapters/in_process_execution_adapter"; |
| 9 | import { resolveCredentialPayload } from "../lib/credential_providers"; | 9 | import { resolveCredentialPayload } from "../lib/credential_providers"; |
| 10 | import { APPROVAL_DENIED_PREFIX } from "../lib/execution_constants"; | 10 | import { APPROVAL_DENIED_PREFIX, APPROVAL_PENDING_PREFIX } from "../lib/execution_constants"; |
| 11 | import { actorIdForAccount } from "../lib/identity"; | 11 | import { actorIdForAccount } from "../lib/identity"; |
| 12 | import { dispatchCodeWithCloudflareWorkerLoader } from "../lib/runtimes/cloudflare_worker_loader_runtime"; | ||
| 13 | import { | ||
| 14 | CLOUDFLARE_WORKER_LOADER_RUNTIME_ID, | ||
| 15 | isCloudflareWorkerLoaderConfigured, | ||
| 16 | isKnownRuntimeId, | ||
| 17 | LOCAL_BUN_RUNTIME_ID, | ||
| 18 | } from "../lib/runtimes/runtime_catalog"; | ||
| 12 | import { runCodeWithAdapter } from "../lib/runtimes/runtime_core"; | 19 | import { runCodeWithAdapter } from "../lib/runtimes/runtime_core"; |
| 13 | import { createDiscoverTool } from "../lib/tool_discovery"; | 20 | import { createDiscoverTool } from "../lib/tool_discovery"; |
| 14 | import { | 21 | import { |
| @@ -34,6 +41,7 @@ import type { · 19 unmodified lines | |||
| 34 | ResolvedToolCredential, | 41 | ResolvedToolCredential, |
| 35 | TaskRecord, | 42 | TaskRecord, |
| 36 | ToolCallRequest, | 43 | ToolCallRequest, |
| 44 | ToolCallRecord, | ||
| 37 | ToolCallResult, | 45 | ToolCallResult, |
| 38 | ToolCredentialSpec, | 46 | ToolCredentialSpec, |
| 39 | ToolDefinition, | 47 | ToolDefinition, |
| @@ -263,10 +271,6 @@ function normalizeExternalToolSource(raw: { · 223 unmodified lines | |||
| 263 | return result; | 271 | return result; |
| 264 | } | 272 | } |
| 265 | 273 | ||
| 266 | async function sleep(ms: number): Promise<void> { | ||
| 267 | await new Promise((resolve) => setTimeout(resolve, ms)); | ||
| 268 | } | ||
| 269 | |||
| 270 | const baseTools = new Map<string, ToolDefinition>(DEFAULT_TOOLS.map((tool) => [tool.path, tool])); | 274 | const baseTools = new Map<string, ToolDefinition>(DEFAULT_TOOLS.map((tool) => [tool.path, tool])); |
| 271 | interface DtsStorageEntry { | 275 | interface DtsStorageEntry { |
| 272 | sourceKey: string; | 276 | sourceKey: string; |
| @@ -293,21 +297,6 @@ async function publish( · 20 unmodified lines | |||
| 293 | }); | 297 | }); |
| 294 | } | 298 | } |
| 295 | 299 | ||
| 296 | async function waitForApproval(ctx: ActionCtx, approvalId: string): Promise<"approved" | "denied"> { | ||
| 297 | while (true) { | ||
| 298 | const approval = await ctx.runQuery(internal.database.getApproval, { approvalId }); | ||
| 299 | if (!approval) { | ||
| 300 | throw new Error(`Approval ${approvalId} not found`); | ||
| 301 | } | ||
| 302 | |||
| 303 | if (approval.status !== "pending") { | ||
| 304 | return approval.status as "approved" | "denied"; | ||
| 305 | } | ||
| 306 | |||
| 307 | await sleep(500); | ||
| 308 | } | ||
| 309 | } | ||
| 310 | |||
| 311 | /** | 300 | /** |
| 312 | * Load a prepared OpenAPI spec, using Convex file storage as a persistent cache. | 301 | * Load a prepared OpenAPI spec, using Convex file storage as a persistent cache. |
| 313 | * | 302 | * |
| @@ -840,6 +829,26 @@ function getGraphqlDecision( · 526 unmodified lines | |||
| 840 | 829 | ||
| 841 | async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCallRequest): Promise<unknown> { | 830 | async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCallRequest): Promise<unknown> { |
| 842 | const { toolPath, input, callId } = call; | 831 | const { toolPath, input, callId } = call; |
| 832 | const persistedCall = (await ctx.runMutation(internal.database.upsertToolCallRequested, { | ||
| 833 | taskId: task.id, | ||
| 834 | callId, | ||
| 835 | workspaceId: task.workspaceId, | ||
| 836 | toolPath, | ||
| 837 | input, | ||
| 838 | })) as ToolCallRecord; | ||
| 839 | |||
| 840 | if (persistedCall.status === "completed") { | ||
| 841 | return persistedCall.output; | ||
| 842 | } | ||
| 843 | |||
| 844 | if (persistedCall.status === "failed") { | ||
| 845 | throw new Error(persistedCall.error ?? `Tool call failed: ${callId}`); | ||
| 846 | } | ||
| 847 | |||
| 848 | if (persistedCall.status === "denied") { | ||
| 849 | throw new Error(`${APPROVAL_DENIED_PREFIX}${persistedCall.error ?? persistedCall.toolPath}`); | ||
| 850 | } | ||
| 851 | |||
| 843 | const policies = await ctx.runQuery(internal.database.listAccessPolicies, { workspaceId: task.workspaceId }); | 852 | const policies = await ctx.runQuery(internal.database.listAccessPolicies, { workspaceId: task.workspaceId }); |
| 844 | const typedPolicies = policies as AccessPolicyRecord[]; | 853 | const typedPolicies = policies as AccessPolicyRecord[]; |
| 845 | 854 | ||
| @@ -885,14 +894,23 @@ async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCallReques · 39 unmodified lines | |||
| 885 | decision = getToolDecision(task, tool, typedPolicies); | 894 | decision = getToolDecision(task, tool, typedPolicies); |
| 886 | } | 895 | } |
| 887 | 896 | ||
| 897 | const publishToolStarted = persistedCall.status === "requested"; | ||
| 898 | |||
| 888 | if (decision === "deny") { | 899 | if (decision === "deny") { |
| 900 | const deniedMessage = `${effectiveToolPath} (policy denied)`; | ||
| 901 | await ctx.runMutation(internal.database.finishToolCall, { | ||
| 902 | taskId: task.id, | ||
| 903 | callId, | ||
| 904 | status: "denied", | ||
| 905 | error: deniedMessage, | ||
| 906 | }); | ||
| 889 | await publish(ctx, task.id, "task", "tool.call.denied", { | 907 | await publish(ctx, task.id, "task", "tool.call.denied", { |
| 890 | taskId: task.id, | 908 | taskId: task.id, |
| 891 | callId, | 909 | callId, |
| 892 | toolPath: effectiveToolPath, | 910 | toolPath: effectiveToolPath, |
| 893 | reason: "policy_deny", | 911 | reason: "policy_deny", |
| 894 | }); | 912 | }); |
| 895 | throw new Error(`${APPROVAL_DENIED_PREFIX}${effectiveToolPath} (policy denied)`); | 913 | throw new Error(`${APPROVAL_DENIED_PREFIX}${deniedMessage}`); |
| 896 | } | 914 | } |
| 897 | 915 | ||
| 898 | let credential: ResolvedToolCredential | undefined; | 916 | let credential: ResolvedToolCredential | undefined; |
| @@ -904,40 +922,98 @@ async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCallReques · 5 unmodified lines | |||
| 904 | credential = resolved; | 922 | credential = resolved; |
| 905 | } | 923 | } |
| 906 | 924 | ||
| 907 | await publish(ctx, task.id, "task", "tool.call.started", { | 925 | if (publishToolStarted) { |
| 908 | taskId: task.id, | 926 | await publish(ctx, task.id, "task", "tool.call.started", { |
| 909 | callId, | ||
| 910 | toolPath: effectiveToolPath, | ||
| 911 | approval: decision === "require_approval" ? "required" : "auto", | ||
| 912 | input: asPayload(input), | ||
| 913 | }); | ||
| 914 | |||
| 915 | if (decision === "require_approval") { | ||
| 916 | const approval = await ctx.runMutation(internal.database.createApproval, { | ||
| 917 | id: createApprovalId(), | ||
| 918 | taskId: task.id, | 927 | taskId: task.id, |
| 928 | callId, | ||
| 919 | toolPath: effectiveToolPath, | 929 | toolPath: effectiveToolPath, |
| 920 | input, | 930 | approval: decision === "require_approval" ? "required" : "auto", |
| 931 | input: asPayload(input), | ||
| 921 | }); | 932 | }); |
| 933 | } | ||
| 922 | 934 | ||
| 923 | await publish(ctx, task.id, "approval", "approval.requested", { | 935 | let approvalSatisfied = false; |
| 924 | approvalId: approval.id, | 936 | if (persistedCall.approvalId) { |
| 937 | const existingApproval = await ctx.runQuery(internal.database.getApproval, { | ||
| 938 | approvalId: persistedCall.approvalId, | ||
| 939 | }); | ||
| 940 | if (!existingApproval) { | ||
| 941 | throw new Error(`Approval ${persistedCall.approvalId} not found for call ${callId}`); | ||
| 942 | } | ||
| 943 | |||
| 944 | if (existingApproval.status === "pending") { | ||
| 945 | throw new Error(`${APPROVAL_PENDING_PREFIX}${existingApproval.id}`); | ||
| 946 | } | ||
| 947 | |||
| 948 | if (existingApproval.status === "denied") { | ||
| 949 | const deniedMessage = `${effectiveToolPath} (${existingApproval.id})`; | ||
| 950 | await ctx.runMutation(internal.database.finishToolCall, { | ||
| 951 | taskId: task.id, | ||
| 952 | callId, | ||
| 953 | status: "denied", | ||
| 954 | error: deniedMessage, | ||
| 955 | }); | ||
| 956 | await publish(ctx, task.id, "task", "tool.call.denied", { | ||
| 957 | taskId: task.id, | ||
| 958 | callId, | ||
| 959 | toolPath: effectiveToolPath, | ||
| 960 | approvalId: existingApproval.id, | ||
| 961 | }); | ||
| 962 | throw new Error(`${APPROVAL_DENIED_PREFIX}${deniedMessage}`); | ||
| 963 | } | ||
| 964 | |||
| 965 | approvalSatisfied = existingApproval.status === "approved"; | ||
| 966 | } | ||
| 967 | |||
| 968 | if (decision === "require_approval" && !approvalSatisfied) { | ||
| 969 | const approvalId = persistedCall.approvalId ?? createApprovalId(); | ||
| 970 | let approval = await ctx.runQuery(internal.database.getApproval, { | ||
| 971 | approvalId, | ||
| 972 | }); | ||
| 973 | |||
| 974 | if (!approval) { | ||
| 975 | approval = await ctx.runMutation(internal.database.createApproval, { | ||
| 976 | id: approvalId, | ||
| 977 | taskId: task.id, | ||
| 978 | toolPath: effectiveToolPath, | ||
| 979 | input, | ||
| 980 | }); | ||
| 981 | |||
| 982 | await publish(ctx, task.id, "approval", "approval.requested", { | ||
| 983 | approvalId: approval.id, | ||
| 984 | taskId: task.id, | ||
| 985 | callId, | ||
| 986 | toolPath: approval.toolPath, | ||
| 987 | input: asPayload(approval.input), | ||
| 988 | createdAt: approval.createdAt, | ||
| 989 | }); | ||
| 990 | } | ||
| 991 | |||
| 992 | await ctx.runMutation(internal.database.setToolCallPendingApproval, { | ||
| 925 | taskId: task.id, | 993 | taskId: task.id, |
| 926 | callId, | 994 | callId, |
| 927 | toolPath: approval.toolPath, | 995 | approvalId: approval.id, |
| 928 | input: asPayload(approval.input), | ||
| 929 | createdAt: approval.createdAt, | ||
| 930 | }); | 996 | }); |
| 931 | 997 | ||
| 932 | const approvalDecision = await waitForApproval(ctx, approval.id); | 998 | if (approval.status === "pending") { |
| 933 | if (approvalDecision === "denied") { | 999 | throw new Error(`${APPROVAL_PENDING_PREFIX}${approval.id}`); |
| 1000 | } | ||
| 1001 | |||
| 1002 | if (approval.status === "denied") { | ||
| 1003 | const deniedMessage = `${effectiveToolPath} (${approval.id})`; | ||
| 1004 | await ctx.runMutation(internal.database.finishToolCall, { | ||
| 1005 | taskId: task.id, | ||
| 1006 | callId, | ||
| 1007 | status: "denied", | ||
| 1008 | error: deniedMessage, | ||
| 1009 | }); | ||
| 934 | await publish(ctx, task.id, "task", "tool.call.denied", { | 1010 | await publish(ctx, task.id, "task", "tool.call.denied", { |
| 935 | taskId: task.id, | 1011 | taskId: task.id, |
| 936 | callId, | 1012 | callId, |
| 937 | toolPath: effectiveToolPath, | 1013 | toolPath: effectiveToolPath, |
| 938 | approvalId: approval.id, | 1014 | approvalId: approval.id, |
| 939 | }); | 1015 | }); |
| 940 | throw new Error(`${APPROVAL_DENIED_PREFIX}${effectiveToolPath} (${approval.id})`); | 1016 | throw new Error(`${APPROVAL_DENIED_PREFIX}${deniedMessage}`); |
| 941 | } | 1017 | } |
| 942 | } | 1018 | } |
| 943 | 1019 | ||
| @@ -951,6 +1027,12 @@ async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCallReques · 7 unmodified lines | |||
| 951 | isToolAllowed: (path) => isToolAllowedForTask(task, path, workspaceTools ?? baseTools, typedPolicies), | 1027 | isToolAllowed: (path) => isToolAllowedForTask(task, path, workspaceTools ?? baseTools, typedPolicies), |
| 952 | }; | 1028 | }; |
| 953 | const value = await tool.run(input, context); | 1029 | const value = await tool.run(input, context); |
| 1030 | await ctx.runMutation(internal.database.finishToolCall, { | ||
| 1031 | taskId: task.id, | ||
| 1032 | callId, | ||
| 1033 | status: "completed", | ||
| 1034 | output: value, | ||
| 1035 | }); | ||
| 954 | await publish(ctx, task.id, "task", "tool.call.completed", { | 1036 | await publish(ctx, task.id, "task", "tool.call.completed", { |
| 955 | taskId: task.id, | 1037 | taskId: task.id, |
| 956 | callId, | 1038 | callId, |
| @@ -960,6 +1042,12 @@ async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCallReques · 3 unmodified lines | |||
| 960 | return value; | 1042 | return value; |
| 961 | } catch (error) { | 1043 | } catch (error) { |
| 962 | const message = describeError(error); | 1044 | const message = describeError(error); |
| 1045 | await ctx.runMutation(internal.database.finishToolCall, { | ||
| 1046 | taskId: task.id, | ||
| 1047 | callId, | ||
| 1048 | status: "failed", | ||
| 1049 | error: message, | ||
| 1050 | }); | ||
| 963 | await publish(ctx, task.id, "task", "tool.call.failed", { | 1051 | await publish(ctx, task.id, "task", "tool.call.failed", { |
| 964 | taskId: task.id, | 1052 | taskId: task.id, |
| 965 | callId, | 1053 | callId, |
| @@ -1051,6 +1139,7 @@ export const handleExternalToolCall = internalAction({ · 85 unmodified lines | |||
| 1051 | if (!task) { | 1139 | if (!task) { |
| 1052 | return { | 1140 | return { |
| 1053 | ok: false, | 1141 | ok: false, |
| 1142 | kind: "failed", | ||
| 1054 | error: `Run not found: ${args.runId}`, | 1143 | error: `Run not found: ${args.runId}`, |
| 1055 | }; | 1144 | }; |
| 1056 | } | 1145 | } |
| @@ -1065,16 +1154,27 @@ export const handleExternalToolCall = internalAction({ · 8 unmodified lines | |||
| 1065 | return { ok: true, value }; | 1154 | return { ok: true, value }; |
| 1066 | } catch (error) { | 1155 | } catch (error) { |
| 1067 | const message = describeError(error); | 1156 | const message = describeError(error); |
| 1157 | if (message.startsWith(APPROVAL_PENDING_PREFIX)) { | ||
| 1158 | const approvalId = message.replace(APPROVAL_PENDING_PREFIX, "").trim(); | ||
| 1159 | return { | ||
| 1160 | ok: false, | ||
| 1161 | kind: "pending", | ||
| 1162 | approvalId, | ||
| 1163 | retryAfterMs: 500, | ||
| 1164 | error: "Approval pending", | ||
| 1165 | }; | ||
| 1166 | } | ||
| 1068 | if (message.startsWith(APPROVAL_DENIED_PREFIX)) { | 1167 | if (message.startsWith(APPROVAL_DENIED_PREFIX)) { |
| 1069 | return { | 1168 | return { |
| 1070 | ok: false, | 1169 | ok: false, |
| 1071 | denied: true, | 1170 | kind: "denied", |
| 1072 | error: message.replace(APPROVAL_DENIED_PREFIX, "").trim(), | 1171 | error: message.replace(APPROVAL_DENIED_PREFIX, "").trim(), |
| 1073 | }; | 1172 | }; |
| 1074 | } | 1173 | } |
| 1075 | 1174 | ||
| 1076 | return { | 1175 | return { |
| 1077 | ok: false, | 1176 | ok: false, |
| 1177 | kind: "failed", | ||
| 1078 | error: message, | 1178 | error: message, |
| 1079 | }; | 1179 | }; |
| 1080 | } | 1180 | } |
| @@ -1089,7 +1189,7 @@ export const runTask = internalAction({ · 8 unmodified lines | |||
| 1089 | return null; | 1189 | return null; |
| 1090 | } | 1190 | } |
| 1091 | 1191 | ||
| 1092 | if (task.runtimeId !== "local-bun") { | 1192 | if (!isKnownRuntimeId(task.runtimeId)) { |
| 1093 | const failed = await ctx.runMutation(internal.database.markTaskFinished, { | 1193 | const failed = await ctx.runMutation(internal.database.markTaskFinished, { |
| 1094 | taskId: args.taskId, | 1194 | taskId: args.taskId, |
| 1095 | status: "failed", | 1195 | status: "failed", |
| @@ -1108,6 +1208,25 @@ export const runTask = internalAction({ · 12 unmodified lines | |||
| 1108 | return null; | 1208 | return null; |
| 1109 | } | 1209 | } |
| 1110 | 1210 | ||
| 1211 | if (task.runtimeId === CLOUDFLARE_WORKER_LOADER_RUNTIME_ID && !isCloudflareWorkerLoaderConfigured()) { | ||
| 1212 | const failed = await ctx.runMutation(internal.database.markTaskFinished, { | ||
| 1213 | taskId: args.taskId, | ||
| 1214 | status: "failed", | ||
| 1215 | stdout: "", | ||
| 1216 | stderr: "", | ||
| 1217 | error: `Runtime is not configured: ${task.runtimeId}`, | ||
| 1218 | }); | ||
| 1219 | |||
| 1220 | if (failed) { | ||
| 1221 | await publish(ctx, args.taskId, "task", "task.failed", { | ||
| 1222 | taskId: args.taskId, | ||
| 1223 | status: failed.status, | ||
| 1224 | error: failed.error, | ||
| 1225 | }); | ||
| 1226 | } | ||
| 1227 | return null; | ||
| 1228 | } | ||
| 1229 | |||
| 1111 | try { | 1230 | try { |
| 1112 | const running = (await ctx.runMutation(internal.database.markTaskRunning, { | 1231 | const running = (await ctx.runMutation(internal.database.markTaskRunning, { |
| 1113 | taskId: args.taskId, | 1232 | taskId: args.taskId, |
| @@ -1122,27 +1241,65 @@ export const runTask = internalAction({ · 8 unmodified lines | |||
| 1122 | startedAt: running.startedAt, | 1241 | startedAt: running.startedAt, |
| 1123 | }); | 1242 | }); |
| 1124 | 1243 | ||
| 1125 | const adapter = new InProcessExecutionAdapter({ | 1244 | if (running.runtimeId === CLOUDFLARE_WORKER_LOADER_RUNTIME_ID) { |
| 1126 | runId: args.taskId, | 1245 | const dispatchResult = await dispatchCodeWithCloudflareWorkerLoader({ |
| 1127 | invokeTool: async (call) => await invokeTool(ctx, running, call), | ||
| 1128 | emitOutput: async (event) => { | ||
| 1129 | await ctx.runMutation(internal.executor.appendRuntimeOutput, { | ||
| 1130 | runId: event.runId, | ||
| 1131 | stream: event.stream, | ||
| 1132 | line: event.line, | ||
| 1133 | timestamp: event.timestamp, | ||
| 1134 | }); | ||
| 1135 | }, | ||
| 1136 | }); | ||
| 1137 | |||
| 1138 | const runtimeResult = await runCodeWithAdapter( | ||
| 1139 | { | ||
| 1140 | taskId: args.taskId, | 1246 | taskId: args.taskId, |
| 1141 | code: running.code, | 1247 | code: running.code, |
| 1142 | timeoutMs: running.timeoutMs, | 1248 | timeoutMs: running.timeoutMs, |
| 1143 | }, | 1249 | }); |
| 1144 | adapter, | 1250 | |
| 1145 | ); | 1251 | if (!dispatchResult.ok) { |
| 1252 | const failed = await ctx.runMutation(internal.database.markTaskFinished, { | ||
| 1253 | taskId: args.taskId, | ||
| 1254 | status: "failed", | ||
| 1255 | stdout: "", | ||
| 1256 | stderr: "", | ||
| 1257 | error: dispatchResult.error, | ||
| 1258 | }); | ||
| 1259 | |||
| 1260 | if (failed) { | ||
| 1261 | await publish(ctx, args.taskId, "task", "task.failed", { | ||
| 1262 | taskId: args.taskId, | ||
| 1263 | status: failed.status, | ||
| 1264 | error: failed.error, | ||
| 1265 | completedAt: failed.completedAt, | ||
| 1266 | }); | ||
| 1267 | } | ||
| 1268 | return null; | ||
| 1269 | } | ||
| 1270 | |||
| 1271 | await publish(ctx, args.taskId, "task", "task.dispatched", { | ||
| 1272 | taskId: args.taskId, | ||
| 1273 | runtimeId: running.runtimeId, | ||
| 1274 | dispatchId: dispatchResult.dispatchId, | ||
| 1275 | durationMs: dispatchResult.durationMs, | ||
| 1276 | }); | ||
| 1277 | return null; | ||
| 1278 | } | ||
| 1279 | |||
| 1280 | const runtimeResult = await (async () => { | ||
| 1281 | const adapter = new InProcessExecutionAdapter({ | ||
| 1282 | runId: args.taskId, | ||
| 1283 | invokeTool: async (call) => await invokeTool(ctx, running, call), | ||
| 1284 | emitOutput: async (event) => { | ||
| 1285 | await ctx.runMutation(internal.executor.appendRuntimeOutput, { | ||
| 1286 | runId: event.runId, | ||
| 1287 | stream: event.stream, | ||
| 1288 | line: event.line, | ||
| 1289 | timestamp: event.timestamp, | ||
| 1290 | }); | ||
| 1291 | }, | ||
| 1292 | }); | ||
| 1293 | |||
| 1294 | return await runCodeWithAdapter( | ||
| 1295 | { | ||
| 1296 | taskId: args.taskId, | ||
| 1297 | code: running.code, | ||
| 1298 | timeoutMs: running.timeoutMs, | ||
| 1299 | }, | ||
| 1300 | adapter, | ||
| 1301 | ); | ||
| 1302 | })(); | ||
| 1146 | 1303 | ||
| 1147 | const finished = await ctx.runMutation(internal.database.markTaskFinished, { | 1304 | const finished = await ctx.runMutation(internal.database.markTaskFinished, { |
| 1148 | taskId: args.taskId, | 1305 | taskId: args.taskId, |
| @@ -9,7 +9,6 @@ import type { AnonymousContext, PendingApprovalRecord, TaskRecord, ToolDescripto · 8 unmodified lines | |||
| 9 | import type { Id } from "./_generated/dataModel"; | 9 | import type { Id } from "./_generated/dataModel"; |
| 10 | 10 | ||
| 11 | const http = httpRouter(); | 11 | const http = httpRouter(); |
| 12 | const internalToken = process.env.EXECUTOR_INTERNAL_TOKEN ?? null; | ||
| 13 | 12 | ||
| 14 | function getMcpAuthorizationServer(): string | null { | 13 | function getMcpAuthorizationServer(): string | null { |
| 15 | return process.env.MCP_AUTHORIZATION_SERVER | 14 | return process.env.MCP_AUTHORIZATION_SERVER |
| @@ -121,28 +120,6 @@ function parseMcpContext(url: URL): { · 105 unmodified lines | |||
| 121 | return { workspaceId, clientId, sessionId }; | 120 | return { workspaceId, clientId, sessionId }; |
| 122 | } | 121 | } |
| 123 | 122 | ||
| 124 | function isInternalAuthorized(request: Request): boolean { | ||
| 125 | if (!internalToken) return false; | ||
| 126 | const header = request.headers.get("authorization"); | ||
| 127 | if (!header || !header.startsWith("Bearer ")) return false; | ||
| 128 | return header.slice("Bearer ".length) === internalToken; | ||
| 129 | } | ||
| 130 | |||
| 131 | function parseInternalRunPath(pathname: string): { runId: string; endpoint: "tool-call" | "output" } | null { | ||
| 132 | const parts = pathname.split("/").filter(Boolean); | ||
| 133 | if (parts.length !== 4 || parts[0] !== "internal" || parts[1] !== "runs") { | ||
| 134 | return null; | ||
| 135 | } | ||
| 136 | |||
| 137 | const runId = parts[2]; | ||
| 138 | const endpoint = parts[3]; | ||
| 139 | if (!runId || (endpoint !== "tool-call" && endpoint !== "output")) { | ||
| 140 | return null; | ||
| 141 | } | ||
| 142 | |||
| 143 | return { runId, endpoint }; | ||
| 144 | } | ||
| 145 | |||
| 146 | const mcpHandler = httpAction(async (ctx, request) => { | 123 | const mcpHandler = httpAction(async (ctx, request) => { |
| 147 | const url = new URL(request.url); | 124 | const url = new URL(request.url); |
| 148 | const mcpAuthConfig = getMcpAuthConfig(); | 125 | const mcpAuthConfig = getMcpAuthConfig(); |
| @@ -313,62 +290,6 @@ const oauthAuthorizationServerProxyHandler = httpAction(async (_ctx, request) => · 164 unmodified lines | |||
| 313 | }); | 290 | }); |
| 314 | }); | 291 | }); |
| 315 | 292 | ||
| 316 | const internalRunsHandler = httpAction(async (ctx, request) => { | ||
| 317 | if (!isInternalAuthorized(request)) { | ||
| 318 | return Response.json({ error: "Unauthorized internal call" }, { status: 401 }); | ||
| 319 | } | ||
| 320 | |||
| 321 | const url = new URL(request.url); | ||
| 322 | const parsed = parseInternalRunPath(url.pathname); | ||
| 323 | if (!parsed) { | ||
| 324 | return Response.json({ error: "Invalid internal route" }, { status: 404 }); | ||
| 325 | } | ||
| 326 | |||
| 327 | let body: unknown; | ||
| 328 | try { | ||
| 329 | body = await request.json(); | ||
| 330 | } catch { | ||
| 331 | body = {}; | ||
| 332 | } | ||
| 333 | const payload = body && typeof body === "object" ? (body as Record<string, unknown>) : {}; | ||
| 334 | |||
| 335 | if (parsed.endpoint === "tool-call") { | ||
| 336 | const callId = payload.callId; | ||
| 337 | const toolPath = payload.toolPath; | ||
| 338 | if (typeof callId !== "string" || typeof toolPath !== "string") { | ||
| 339 | return Response.json({ error: "callId and toolPath are required" }, { status: 400 }); | ||
| 340 | } | ||
| 341 | |||
| 342 | const result = await ctx.runAction(internal.executorNode.handleExternalToolCall, { | ||
| 343 | runId: parsed.runId, | ||
| 344 | callId, | ||
| 345 | toolPath, | ||
| 346 | input: payload.input, | ||
| 347 | }); | ||
| 348 | return Response.json(result, { status: 200 }); | ||
| 349 | } | ||
| 350 | |||
| 351 | const stream = payload.stream; | ||
| 352 | const line = payload.line; | ||
| 353 | if ((stream !== "stdout" && stream !== "stderr") || typeof line !== "string") { | ||
| 354 | return Response.json({ error: "stream and line are required" }, { status: 400 }); | ||
| 355 | } | ||
| 356 | |||
| 357 | const task = await ctx.runQuery(internal.database.getTask, { taskId: parsed.runId }); | ||
| 358 | if (!task) { | ||
| 359 | return Response.json({ error: `Run not found: ${parsed.runId}` }, { status: 404 }); | ||
| 360 | } | ||
| 361 | |||
| 362 | await ctx.runMutation(internal.executor.appendRuntimeOutput, { | ||
| 363 | runId: parsed.runId, | ||
| 364 | stream, | ||
| 365 | line, | ||
| 366 | timestamp: typeof payload.timestamp === "number" ? payload.timestamp : Date.now(), | ||
| 367 | }); | ||
| 368 | |||
| 369 | return Response.json({ ok: true }, { status: 200 }); | ||
| 370 | }); | ||
| 371 | |||
| 372 | authKit.registerRoutes(http); | 293 | authKit.registerRoutes(http); |
| 373 | registerStripeRoutes(http, components.stripe, { | 294 | registerStripeRoutes(http, components.stripe, { |
| 374 | webhookPath: "/stripe/webhook", | 295 | webhookPath: "/stripe/webhook", |
| @@ -380,10 +301,4 @@ http.route({ path: "/mcp", method: "DELETE", handler: mcpHandler }); · 5 unmodified lines | |||
| 380 | http.route({ path: "/.well-known/oauth-protected-resource", method: "GET", handler: oauthProtectedResourceHandler }); | 301 | http.route({ path: "/.well-known/oauth-protected-resource", method: "GET", handler: oauthProtectedResourceHandler }); |
| 381 | http.route({ path: "/.well-known/oauth-authorization-server", method: "GET", handler: oauthAuthorizationServerProxyHandler }); | 302 | http.route({ path: "/.well-known/oauth-authorization-server", method: "GET", handler: oauthAuthorizationServerProxyHandler }); |
| 382 | 303 | ||
| 383 | http.route({ | ||
| 384 | pathPrefix: "/internal/runs/", | ||
| 385 | method: "POST", | ||
| 386 | handler: internalRunsHandler, | ||
| 387 | }); | ||
| 388 | |||
| 389 | export default http; | 304 | export default http; |
| @@ -0,0 +1,88 @@ | |||
| 1 | import { v } from "convex/values"; | ||
| 2 | import { internal } from "./_generated/api"; | ||
| 3 | import { action, mutation } from "./_generated/server"; | ||
| 4 | import type { ToolCallResult } from "../lib/types"; | ||
| 5 | |||
| 6 | function requireInternalSecret(secret: string): void { | ||
| 7 | const expected = process.env.EXECUTOR_INTERNAL_TOKEN; | ||
| 8 | if (!expected) { | ||
| 9 | throw new Error("EXECUTOR_INTERNAL_TOKEN is not configured"); | ||
| 10 | } | ||
| 11 | if (secret !== expected) { | ||
| 12 | throw new Error("Unauthorized: invalid internal secret"); | ||
| 13 | } | ||
| 14 | } | ||
| 15 | |||
| 16 | export const handleToolCall = action({ | ||
| 17 | args: { | ||
| 18 | internalSecret: v.string(), | ||
| 19 | runId: v.string(), | ||
| 20 | callId: v.string(), | ||
| 21 | toolPath: v.string(), | ||
| 22 | input: v.optional(v.any()), | ||
| 23 | }, | ||
| 24 | handler: async (ctx, args): Promise<ToolCallResult> => { | ||
| 25 | requireInternalSecret(args.internalSecret); | ||
| 26 | return await ctx.runAction(internal.executorNode.handleExternalToolCall, { | ||
| 27 | runId: args.runId, | ||
| 28 | callId: args.callId, | ||
| 29 | toolPath: args.toolPath, | ||
| 30 | input: args.input, | ||
| 31 | }); | ||
| 32 | }, | ||
| 33 | }); | ||
| 34 | |||
| 35 | export const appendOutput = mutation({ | ||
| 36 | args: { | ||
| 37 | internalSecret: v.string(), | ||
| 38 | runId: v.string(), | ||
| 39 | stream: v.union(v.literal("stdout"), v.literal("stderr")), | ||
| 40 | line: v.string(), | ||
| 41 | timestamp: v.optional(v.number()), | ||
| 42 | }, | ||
| 43 | handler: async (ctx, args) => { | ||
| 44 | requireInternalSecret(args.internalSecret); | ||
| 45 | |||
| 46 | const task = await ctx.runQuery(internal.database.getTask, { | ||
| 47 | taskId: args.runId, | ||
| 48 | }); | ||
| 49 | if (!task) { | ||
| 50 | return { ok: false as const, error: `Run not found: ${args.runId}` }; | ||
| 51 | } | ||
| 52 | |||
| 53 | await ctx.runMutation(internal.executor.appendRuntimeOutput, { | ||
| 54 | runId: args.runId, | ||
| 55 | stream: args.stream, | ||
| 56 | line: args.line, | ||
| 57 | timestamp: args.timestamp, | ||
| 58 | }); | ||
| 59 | |||
| 60 | return { ok: true as const }; | ||
| 61 | }, | ||
| 62 | }); | ||
| 63 | |||
| 64 | export const completeRun = mutation({ | ||
| 65 | args: { | ||
| 66 | internalSecret: v.string(), | ||
| 67 | runId: v.string(), | ||
| 68 | status: v.union(v.literal("completed"), v.literal("failed"), v.literal("timed_out"), v.literal("denied")), | ||
| 69 | stdout: v.optional(v.string()), | ||
| 70 | stderr: v.optional(v.string()), | ||
| 71 | exitCode: v.optional(v.number()), | ||
| 72 | error: v.optional(v.string()), | ||
| 73 | durationMs: v.optional(v.number()), | ||
| 74 | }, | ||
| 75 | handler: async (ctx, args) => { | ||
| 76 | requireInternalSecret(args.internalSecret); | ||
| 77 | |||
| 78 | return await ctx.runMutation(internal.executor.completeRuntimeRun, { | ||
| 79 | runId: args.runId, | ||
| 80 | status: args.status, | ||
| 81 | stdout: args.stdout, | ||
| 82 | stderr: args.stderr, | ||
| 83 | exitCode: args.exitCode, | ||
| 84 | error: args.error, | ||
| 85 | durationMs: args.durationMs, | ||
| 86 | }); | ||
| 87 | }, | ||
| 88 | }); | ||
| @@ -34,6 +34,13 @@ const taskStatus = v.union( · 33 unmodified lines | |||
| 34 | v.literal("denied"), | 34 | v.literal("denied"), |
| 35 | ); | 35 | ); |
| 36 | const approvalStatus = v.union(v.literal("pending"), v.literal("approved"), v.literal("denied")); | 36 | const approvalStatus = v.union(v.literal("pending"), v.literal("approved"), v.literal("denied")); |
| 37 | const toolCallStatus = v.union( | ||
| 38 | v.literal("requested"), | ||
| 39 | v.literal("pending_approval"), | ||
| 40 | v.literal("completed"), | ||
| 41 | v.literal("failed"), | ||
| 42 | v.literal("denied"), | ||
| 43 | ); | ||
| 37 | const policyDecision = v.union(v.literal("allow"), v.literal("require_approval"), v.literal("deny")); | 44 | const policyDecision = v.union(v.literal("allow"), v.literal("require_approval"), v.literal("deny")); |
| 38 | const credentialScope = v.union(v.literal("workspace"), v.literal("actor")); | 45 | const credentialScope = v.union(v.literal("workspace"), v.literal("actor")); |
| 39 | const credentialProvider = v.union( | 46 | const credentialProvider = v.union( |
| @@ -207,6 +214,25 @@ export default defineSchema({ · 167 unmodified lines | |||
| 207 | .index("by_workspace_created", ["workspaceId", "createdAt"]) | 214 | .index("by_workspace_created", ["workspaceId", "createdAt"]) |
| 208 | .index("by_workspace_status_created", ["workspaceId", "status", "createdAt"]), | 215 | .index("by_workspace_status_created", ["workspaceId", "status", "createdAt"]), |
| 209 | 216 | ||
| 217 | toolCalls: defineTable({ | ||
| 218 | taskId: v.string(), | ||
| 219 | callId: v.string(), | ||
| 220 | workspaceId: v.id("workspaces"), | ||
| 221 | toolPath: v.string(), | ||
| 222 | input: v.any(), | ||
| 223 | status: toolCallStatus, | ||
| 224 | approvalId: v.optional(v.string()), | ||
| 225 | output: v.optional(v.any()), | ||
| 226 | error: v.optional(v.string()), | ||
| 227 | createdAt: v.number(), | ||
| 228 | updatedAt: v.number(), | ||
| 229 | completedAt: v.optional(v.number()), | ||
| 230 | }) | ||
| 231 | .index("by_task_call", ["taskId", "callId"]) | ||
| 232 | .index("by_task_created", ["taskId", "createdAt"]) | ||
| 233 | .index("by_workspace_created", ["workspaceId", "createdAt"]) | ||
| 234 | .index("by_approval_id", ["approvalId"]), | ||
| 235 | |||
| 210 | taskEvents: defineTable({ | 236 | taskEvents: defineTable({ |
| 211 | sequence: v.number(), | 237 | sequence: v.number(), |
| 212 | taskId: v.string(), // references tasks.taskId (not tasks._id) | 238 | taskId: v.string(), // references tasks.taskId (not tasks._id) |
| @@ -45,7 +45,7 @@ test("maps approval denied errors to denied result", async () => { · 44 unmodified lines | |||
| 45 | 45 | ||
| 46 | expect(result).toEqual({ | 46 | expect(result).toEqual({ |
| 47 | ok: false, | 47 | ok: false, |
| 48 | denied: true, | 48 | kind: "denied", |
| 49 | error: "approval required", | 49 | error: "approval required", |
| 50 | }); | 50 | }); |
| 51 | }); | 51 | }); |
| @@ -1,4 +1,4 @@ | |||
| 1 | import { APPROVAL_DENIED_PREFIX } from "../execution_constants"; | 1 | import { APPROVAL_DENIED_PREFIX, APPROVAL_PENDING_PREFIX } from "../execution_constants"; |
| 2 | import type { | 2 | import type { |
| 3 | ExecutionAdapter, | 3 | ExecutionAdapter, |
| 4 | RuntimeOutputEvent, | 4 | RuntimeOutputEvent, |
| @@ -20,6 +20,7 @@ export class InProcessExecutionAdapter implements ExecutionAdapter { · 15 unmodified lines | |||
| 20 | if (call.runId !== this.options.runId) { | 20 | if (call.runId !== this.options.runId) { |
| 21 | return { | 21 | return { |
| 22 | ok: false, | 22 | ok: false, |
| 23 | kind: "failed", | ||
| 23 | error: `Run mismatch for call ${call.callId}`, | 24 | error: `Run mismatch for call ${call.callId}`, |
| 24 | }; | 25 | }; |
| 25 | } | 26 | } |
| @@ -32,13 +33,24 @@ export class InProcessExecutionAdapter implements ExecutionAdapter { · 6 unmodified lines | |||
| 32 | if (message.startsWith(APPROVAL_DENIED_PREFIX)) { | 33 | if (message.startsWith(APPROVAL_DENIED_PREFIX)) { |
| 33 | return { | 34 | return { |
| 34 | ok: false, | 35 | ok: false, |
| 35 | denied: true, | 36 | kind: "denied", |
| 36 | error: message.replace(APPROVAL_DENIED_PREFIX, "").trim(), | 37 | error: message.replace(APPROVAL_DENIED_PREFIX, "").trim(), |
| 37 | }; | 38 | }; |
| 38 | } | 39 | } |
| 39 | 40 | ||
| 41 | if (message.startsWith(APPROVAL_PENDING_PREFIX)) { | ||
| 42 | return { | ||
| 43 | ok: false, | ||
| 44 | kind: "pending", | ||
| 45 | approvalId: message.replace(APPROVAL_PENDING_PREFIX, "").trim(), | ||
| 46 | retryAfterMs: 500, | ||
| 47 | error: "Approval pending", | ||
| 48 | }; | ||
| 49 | } | ||
| 50 | |||
| 40 | return { | 51 | return { |
| 41 | ok: false, | 52 | ok: false, |
| 53 | kind: "failed", | ||
| 42 | error: message, | 54 | error: message, |
| 43 | }; | 55 | }; |
| 44 | } | 56 | } |
| @@ -1,2 +1,3 @@ | |||
| 1 | export const APPROVAL_DENIED_PREFIX = "APPROVAL_DENIED:"; | 1 | export const APPROVAL_DENIED_PREFIX = "APPROVAL_DENIED:"; |
| 2 | export const APPROVAL_PENDING_PREFIX = "APPROVAL_PENDING:"; | ||
| 2 | export const TASK_TIMEOUT_MARKER = "TASK_TIMEOUT"; | 3 | export const TASK_TIMEOUT_MARKER = "TASK_TIMEOUT"; |
| @@ -191,7 +191,8 @@ describe("prepareOpenApiSpec with large specs", () => { · 190 unmodified lines | |||
| 191 | expect(tools.length).toBeGreaterThan(700); | 191 | expect(tools.length).toBeGreaterThan(700); |
| 192 | const deleteTool = tools.find((tool) => tool.metadata?.operationId?.startsWith("delete_")); | 192 | const deleteTool = tools.find((tool) => tool.metadata?.operationId?.startsWith("delete_")); |
| 193 | expect(deleteTool).toBeDefined(); | 193 | expect(deleteTool).toBeDefined(); |
| 194 | expect(deleteTool?.metadata?.displayReturnsType).toContain("ToolOutput<operations["); | 194 | expect(deleteTool?.metadata?.displayReturnsType).toBeDefined(); |
| 195 | expect(deleteTool?.metadata?.displayReturnsType).not.toContain("ToolOutput<operations["); | ||
| 195 | expect(elapsed).toBeLessThan(20_000); | 196 | expect(elapsed).toBeLessThan(20_000); |
| 196 | }); | 197 | }); |
| 197 | 198 | ||
| @@ -0,0 +1,182 @@ | |||
| 1 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | ||
| 2 | import { dispatchCodeWithCloudflareWorkerLoader } from "./cloudflare_worker_loader_runtime"; | ||
| 3 | |||
| 4 | let fakeHostServer: ReturnType<typeof Bun.serve>; | ||
| 5 | let fakeCallbackServer: ReturnType<typeof Bun.serve>; | ||
| 6 | |||
| 7 | const AUTH_TOKEN = "test-sandbox-token"; | ||
| 8 | const CALLBACK_TOKEN = "test-callback-token"; | ||
| 9 | |||
| 10 | let hostResponseStatus = 202; | ||
| 11 | let hostResponseBody: Record<string, unknown> = { accepted: true, dispatchId: "dispatch_test" }; | ||
| 12 | type HostRequestBody = { | ||
| 13 | taskId: string; | ||
| 14 | code: string; | ||
| 15 | timeoutMs: number; | ||
| 16 | callback: { convexUrl: string; internalSecret: string }; | ||
| 17 | }; | ||
| 18 | |||
| 19 | let lastHostRequestBody: HostRequestBody | null = null; | ||
| 20 | |||
| 21 | beforeAll(() => { | ||
| 22 | fakeCallbackServer = Bun.serve({ | ||
| 23 | port: 0, | ||
| 24 | fetch: () => Response.json({ ok: true }), | ||
| 25 | }); | ||
| 26 | |||
| 27 | fakeHostServer = Bun.serve({ | ||
| 28 | port: 0, | ||
| 29 | fetch: async (req) => { | ||
| 30 | const url = new URL(req.url); | ||
| 31 | if (url.pathname !== "/v1/runs" || req.method !== "POST") { | ||
| 32 | return Response.json({ error: "Not found" }, { status: 404 }); | ||
| 33 | } | ||
| 34 | |||
| 35 | const auth = req.headers.get("authorization"); | ||
| 36 | if (auth !== `Bearer ${AUTH_TOKEN}`) { | ||
| 37 | return Response.json({ error: "Unauthorized" }, { status: 401 }); | ||
| 38 | } | ||
| 39 | |||
| 40 | lastHostRequestBody = await req.json(); | ||
| 41 | return Response.json(hostResponseBody, { status: hostResponseStatus }); | ||
| 42 | }, | ||
| 43 | }); | ||
| 44 | |||
| 45 | process.env.CLOUDFLARE_SANDBOX_RUN_URL = `http://127.0.0.1:${fakeHostServer.port}/v1/runs`; | ||
| 46 | process.env.CLOUDFLARE_SANDBOX_AUTH_TOKEN = AUTH_TOKEN; | ||
| 47 | process.env.CONVEX_SITE_URL = `http://127.0.0.1:${fakeCallbackServer.port}`; | ||
| 48 | process.env.EXECUTOR_INTERNAL_TOKEN = CALLBACK_TOKEN; | ||
| 49 | process.env.CLOUDFLARE_SANDBOX_REQUEST_TIMEOUT_MS = "10000"; | ||
| 50 | }); | ||
| 51 | |||
| 52 | afterAll(() => { | ||
| 53 | fakeHostServer?.stop(true); | ||
| 54 | fakeCallbackServer?.stop(true); | ||
| 55 | delete process.env.CLOUDFLARE_SANDBOX_RUN_URL; | ||
| 56 | delete process.env.CLOUDFLARE_SANDBOX_AUTH_TOKEN; | ||
| 57 | delete process.env.CONVEX_SITE_URL; | ||
| 58 | delete process.env.EXECUTOR_INTERNAL_TOKEN; | ||
| 59 | delete process.env.CLOUDFLARE_SANDBOX_REQUEST_TIMEOUT_MS; | ||
| 60 | }); | ||
| 61 | |||
| 62 | describe("cloudflare worker loader dispatch", () => { | ||
| 63 | test("dispatches run request and returns accepted response", async () => { | ||
| 64 | hostResponseStatus = 202; | ||
| 65 | hostResponseBody = { accepted: true, dispatchId: "dispatch_abc" }; | ||
| 66 | lastHostRequestBody = null; | ||
| 67 | |||
| 68 | const result = await dispatchCodeWithCloudflareWorkerLoader({ | ||
| 69 | taskId: `task_${crypto.randomUUID()}`, | ||
| 70 | code: `console.log("hello");`, | ||
| 71 | timeoutMs: 5_000, | ||
| 72 | }); | ||
| 73 | |||
| 74 | expect(result.ok).toBe(true); | ||
| 75 | if (result.ok) { | ||
| 76 | expect(result.accepted).toBe(true); | ||
| 77 | expect(result.dispatchId).toBe("dispatch_abc"); | ||
| 78 | } | ||
| 79 | |||
| 80 | expect(lastHostRequestBody).not.toBeNull(); | ||
| 81 | const requestBody = lastHostRequestBody as unknown as HostRequestBody; | ||
| 82 | expect(requestBody.callback.convexUrl).toContain(String(fakeCallbackServer.port)); | ||
| 83 | expect(requestBody.callback.internalSecret).toBe(CALLBACK_TOKEN); | ||
| 84 | }); | ||
| 85 | |||
| 86 | test("transpiles TypeScript before dispatching", async () => { | ||
| 87 | hostResponseStatus = 202; | ||
| 88 | hostResponseBody = { accepted: true, dispatchId: "dispatch_ts" }; | ||
| 89 | lastHostRequestBody = null; | ||
| 90 | |||
| 91 | const result = await dispatchCodeWithCloudflareWorkerLoader({ | ||
| 92 | taskId: `task_${crypto.randomUUID()}`, | ||
| 93 | code: ` | ||
| 94 | interface User { name: string } | ||
| 95 | const user: User = { name: "Ada" }; | ||
| 96 | console.log(user.name); | ||
| 97 | `, | ||
| 98 | timeoutMs: 5_000, | ||
| 99 | }); | ||
| 100 | |||
| 101 | expect(result.ok).toBe(true); | ||
| 102 | expect(lastHostRequestBody).not.toBeNull(); | ||
| 103 | const requestBody = lastHostRequestBody as unknown as HostRequestBody; | ||
| 104 | expect(requestBody.code.includes("interface User")).toBe(false); | ||
| 105 | expect(requestBody.code.includes("const user")).toBe(true); | ||
| 106 | }); | ||
| 107 | |||
| 108 | test("fails when host does not accept dispatch", async () => { | ||
| 109 | hostResponseStatus = 500; | ||
| 110 | hostResponseBody = { error: "host failure" }; | ||
| 111 | |||
| 112 | const result = await dispatchCodeWithCloudflareWorkerLoader({ | ||
| 113 | taskId: `task_${crypto.randomUUID()}`, | ||
| 114 | code: `console.log("x");`, | ||
| 115 | timeoutMs: 5_000, | ||
| 116 | }); | ||
| 117 | |||
| 118 | expect(result.ok).toBe(false); | ||
| 119 | if (!result.ok) { | ||
| 120 | expect(result.error).toContain("500"); | ||
| 121 | } | ||
| 122 | }); | ||
| 123 | |||
| 124 | test("reports TypeScript transpile errors", async () => { | ||
| 125 | const result = await dispatchCodeWithCloudflareWorkerLoader({ | ||
| 126 | taskId: `task_${crypto.randomUUID()}`, | ||
| 127 | code: `const x: = 5;`, | ||
| 128 | timeoutMs: 5_000, | ||
| 129 | }); | ||
| 130 | |||
| 131 | expect(result.ok).toBe(false); | ||
| 132 | if (!result.ok) { | ||
| 133 | expect(result.error).toContain("TypeScript transpile error"); | ||
| 134 | } | ||
| 135 | }); | ||
| 136 | }); | ||
| 137 | |||
| 138 | describe("runtime catalog", () => { | ||
| 139 | test("isKnownRuntimeId recognizes both runtimes", async () => { | ||
| 140 | const { | ||
| 141 | isKnownRuntimeId, | ||
| 142 | LOCAL_BUN_RUNTIME_ID, | ||
| 143 | CLOUDFLARE_WORKER_LOADER_RUNTIME_ID, | ||
| 144 | } = await import("./runtime_catalog"); | ||
| 145 | |||
| 146 | expect(isKnownRuntimeId(LOCAL_BUN_RUNTIME_ID)).toBe(true); | ||
| 147 | expect(isKnownRuntimeId(CLOUDFLARE_WORKER_LOADER_RUNTIME_ID)).toBe(true); | ||
| 148 | expect(isKnownRuntimeId("unknown-runtime")).toBe(false); | ||
| 149 | }); | ||
| 150 | |||
| 151 | test("isCloudflareWorkerLoaderConfigured checks env vars", async () => { | ||
| 152 | const { isCloudflareWorkerLoaderConfigured } = await import("./runtime_catalog"); | ||
| 153 | expect(isCloudflareWorkerLoaderConfigured()).toBe(true); | ||
| 154 | }); | ||
| 155 | |||
| 156 | test("getCloudflareWorkerLoaderConfig reads env vars", async () => { | ||
| 157 | const { getCloudflareWorkerLoaderConfig } = await import("./runtime_catalog"); | ||
| 158 | const config = getCloudflareWorkerLoaderConfig(); | ||
| 159 | |||
| 160 | expect(config.runUrl).toContain("/v1/runs"); | ||
| 161 | expect(config.authToken).toBe(AUTH_TOKEN); | ||
| 162 | expect(config.callbackConvexUrl).toContain(String(fakeCallbackServer.port)); | ||
| 163 | expect(config.callbackInternalSecret).toBe(CALLBACK_TOKEN); | ||
| 164 | expect(config.requestTimeoutMs).toBe(10_000); | ||
| 165 | }); | ||
| 166 | |||
| 167 | test("prefers CONVEX_URL over CONVEX_SITE_URL for callback RPC", async () => { | ||
| 168 | const { getCloudflareWorkerLoaderConfig } = await import("./runtime_catalog"); | ||
| 169 | const previousConvexUrl = process.env.CONVEX_URL; | ||
| 170 | const previousSiteUrl = process.env.CONVEX_SITE_URL; | ||
| 171 | try { | ||
| 172 | process.env.CONVEX_URL = "https://example-convex-cloud-url.test"; | ||
| 173 | process.env.CONVEX_SITE_URL = "https://example-convex-site-url.test"; | ||
| 174 | |||
| 175 | const config = getCloudflareWorkerLoaderConfig(); | ||
| 176 | expect(config.callbackConvexUrl).toBe("https://example-convex-cloud-url.test"); | ||
| 177 | } finally { | ||
| 178 | process.env.CONVEX_URL = previousConvexUrl; | ||
| 179 | process.env.CONVEX_SITE_URL = previousSiteUrl; | ||
| 180 | } | ||
| 181 | }); | ||
| 182 | }); | ||
| @@ -0,0 +1,130 @@ | |||
| 1 | "use node"; | ||
| 2 | |||
| 3 | import { Result } from "better-result"; | ||
| 4 | import type { SandboxExecutionRequest } from "../types"; | ||
| 5 | import { getCloudflareWorkerLoaderConfig } from "./runtime_catalog"; | ||
| 6 | import { transpileForRuntime } from "./transpile"; | ||
| 7 | |||
| 8 | /** | ||
| 9 | * Run agent-generated code via a Cloudflare Worker that uses the Dynamic | ||
| 10 | * Worker Loader API to spawn a sandboxed isolate. | ||
| 11 | * | ||
| 12 | * ## Architecture | ||
| 13 | * | ||
| 14 | * 1. This function (running inside a Convex action) POSTs the code + config to | ||
| 15 | * a **host Worker** deployed on Cloudflare. | ||
| 16 | * | ||
| 17 | * 2. The host Worker uses `env.LOADER.get(id, callback)` to create a dynamic | ||
| 18 | * isolate containing the user code. | ||
| 19 | * | ||
| 20 | * 3. The dynamic isolate's `tools` proxy calls are intercepted by a | ||
| 21 | * `ToolBridge` entrypoint in the host Worker (passed via `env` bindings), | ||
| 22 | * which in turn calls Convex callback RPCs to resolve tools. | ||
| 23 | * | ||
| 24 | * 4. Console output from the isolate is similarly relayed back via callback | ||
| 25 | * RPC mutation. | ||
| 26 | * | ||
| 27 | * 5. The host Worker accepts the run immediately and finishes execution | ||
| 28 | * asynchronously, reporting terminal results back through callback RPC. | ||
| 29 | * | ||
| 30 | * ## Callback authentication | ||
| 31 | * | ||
| 32 | * The host Worker authenticates callback RPCs using `EXECUTOR_INTERNAL_TOKEN`. | ||
| 33 | */ | ||
| 34 | export interface CloudflareDispatchResult { | ||
| 35 | ok: true; | ||
| 36 | accepted: true; | ||
| 37 | dispatchId: string; | ||
| 38 | durationMs: number; | ||
| 39 | } | ||
| 40 | |||
| 41 | export interface CloudflareDispatchError { | ||
| 42 | ok: false; | ||
| 43 | error: string; | ||
| 44 | durationMs: number; | ||
| 45 | } | ||
| 46 | |||
| 47 | export async function dispatchCodeWithCloudflareWorkerLoader( | ||
| 48 | request: SandboxExecutionRequest, | ||
| 49 | ): Promise<CloudflareDispatchResult | CloudflareDispatchError> { | ||
| 50 | const config = getCloudflareWorkerLoaderConfig(); | ||
| 51 | const startedAt = Date.now(); | ||
| 52 | |||
| 53 | const mkError = (error: string): CloudflareDispatchError => ({ | ||
| 54 | ok: false, | ||
| 55 | error, | ||
| 56 | durationMs: Date.now() - startedAt, | ||
| 57 | }); | ||
| 58 | |||
| 59 | // ── Transpile TS → JS on the Convex side ───────────────────────────── | ||
| 60 | const transpiled = transpileForRuntime(request.code); | ||
| 61 | if (transpiled.isErr()) { | ||
| 62 | return mkError(transpiled.error.message); | ||
| 63 | } | ||
| 64 | |||
| 65 | // ── POST to CF host worker ──────────────────────────────────────────── | ||
| 66 | const controller = new AbortController(); | ||
| 67 | const timeout = setTimeout(() => controller.abort(), config.requestTimeoutMs); | ||
| 68 | |||
| 69 | const response = await Result.tryPromise(() => | ||
| 70 | fetch(config.runUrl, { | ||
| 71 | method: "POST", | ||
| 72 | headers: { | ||
| 73 | "Content-Type": "application/json", | ||
| 74 | Authorization: `Bearer ${config.authToken}`, | ||
| 75 | }, | ||
| 76 | body: JSON.stringify({ | ||
| 77 | taskId: request.taskId, | ||
| 78 | code: transpiled.value, | ||
| 79 | timeoutMs: request.timeoutMs, | ||
| 80 | callback: { | ||
| 81 | convexUrl: config.callbackConvexUrl, | ||
| 82 | internalSecret: config.callbackInternalSecret, | ||
| 83 | }, | ||
| 84 | }), | ||
| 85 | signal: controller.signal, | ||
| 86 | }), | ||
| 87 | ); | ||
| 88 | |||
| 89 | clearTimeout(timeout); | ||
| 90 | |||
| 91 | if (response.isErr()) { | ||
| 92 | const cause = response.error.cause; | ||
| 93 | const isAbort = cause instanceof DOMException && cause.name === "AbortError"; | ||
| 94 | if (isAbort) { | ||
| 95 | return mkError(`Cloudflare sandbox dispatch timed out after ${config.requestTimeoutMs}ms`); | ||
| 96 | } | ||
| 97 | const message = cause instanceof Error ? cause.message : String(cause); | ||
| 98 | return mkError(`Cloudflare sandbox dispatch failed: ${message}`); | ||
| 99 | } | ||
| 100 | |||
| 101 | // ── Handle non-accepted HTTP status ─────────────────────────────────── | ||
| 102 | if (response.value.status !== 202) { | ||
| 103 | const text = await Result.tryPromise(() => response.value.text()); | ||
| 104 | const body = text.unwrapOr(response.value.statusText); | ||
| 105 | return mkError(`Cloudflare sandbox dispatch returned ${response.value.status}: ${body}`); | ||
| 106 | } | ||
| 107 | |||
| 108 | // ── Parse accepted response JSON ────────────────────────────────────── | ||
| 109 | const body = await Result.tryPromise(() => | ||
| 110 | response.value.json() as Promise<{ | ||
| 111 | accepted?: boolean; | ||
| 112 | dispatchId?: string; | ||
| 113 | }>, | ||
| 114 | ); | ||
| 115 | |||
| 116 | if (body.isErr()) { | ||
| 117 | return mkError("Cloudflare sandbox dispatch returned invalid JSON"); | ||
| 118 | } | ||
| 119 | |||
| 120 | if (!body.value.accepted || !body.value.dispatchId) { | ||
| 121 | return mkError("Cloudflare sandbox dispatch response missing accepted/dispatchId"); | ||
| 122 | } | ||
| 123 | |||
| 124 | return { | ||
| 125 | ok: true, | ||
| 126 | accepted: true, | ||
| 127 | dispatchId: body.value.dispatchId, | ||
| 128 | durationMs: Date.now() - startedAt, | ||
| 129 | }; | ||
| 130 | } | ||
| @@ -28,6 +28,7 @@ function createRuntimeAdapter( · 27 unmodified lines | |||
| 28 | if (!tool) { | 28 | if (!tool) { |
| 29 | return { | 29 | return { |
| 30 | ok: false, | 30 | ok: false, |
| 31 | kind: "failed", | ||
| 31 | error: `Tool not found: ${call.toolPath}`, | 32 | error: `Tool not found: ${call.toolPath}`, |
| 32 | }; | 33 | }; |
| 33 | } | 34 | } |
| @@ -45,6 +46,7 @@ function createRuntimeAdapter( · 11 unmodified lines | |||
| 45 | } catch (error) { | 46 | } catch (error) { |
| 46 | return { | 47 | return { |
| 47 | ok: false, | 48 | ok: false, |
| 49 | kind: "failed", | ||
| 48 | error: error instanceof Error ? error.message : String(error), | 50 | error: error instanceof Error ? error.message : String(error), |
| 49 | }; | 51 | }; |
| 50 | } | 52 | } |
| @@ -89,7 +91,7 @@ test("returns denied when adapter marks tool call denied", async () => { · 38 unmodified lines | |||
| 89 | async invokeTool() { | 91 | async invokeTool() { |
| 90 | return { | 92 | return { |
| 91 | ok: false, | 93 | ok: false, |
| 92 | denied: true, | 94 | kind: "denied", |
| 93 | error: "policy denied", | 95 | error: "policy denied", |
| 94 | }; | 96 | }; |
| 95 | }, | 97 | }, |
| @@ -0,0 +1,80 @@ | |||
| 1 | // ── Runtime ID constants ────────────────────────────────────────────────────── | ||
| 2 | |||
| 3 | export const LOCAL_BUN_RUNTIME_ID = "local-bun"; | ||
| 4 | export const CLOUDFLARE_WORKER_LOADER_RUNTIME_ID = "cloudflare-worker-loader"; | ||
| 5 | |||
| 6 | const KNOWN_RUNTIME_IDS = new Set([ | ||
| 7 | LOCAL_BUN_RUNTIME_ID, | ||
| 8 | CLOUDFLARE_WORKER_LOADER_RUNTIME_ID, | ||
| 9 | ]); | ||
| 10 | |||
| 11 | export function isKnownRuntimeId(runtimeId: string): boolean { | ||
| 12 | return KNOWN_RUNTIME_IDS.has(runtimeId); | ||
| 13 | } | ||
| 14 | |||
| 15 | // ── Cloudflare Worker Loader config ────────────────────────────────────────── | ||
| 16 | |||
| 17 | export interface CloudflareWorkerLoaderConfig { | ||
| 18 | /** The URL of the CF host worker's /v1/runs endpoint. */ | ||
| 19 | runUrl: string; | ||
| 20 | /** Shared-secret bearer token for authenticating with the host worker. */ | ||
| 21 | authToken: string; | ||
| 22 | /** Dispatch request timeout in ms (how long we wait for /v1/runs accepted response). */ | ||
| 23 | requestTimeoutMs: number; | ||
| 24 | /** Convex deployment URL used for runtime callback RPC invocations. */ | ||
| 25 | callbackConvexUrl: string; | ||
| 26 | /** Internal auth secret used for runtime callback RPC auth. */ | ||
| 27 | callbackInternalSecret: string; | ||
| 28 | } | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Returns true if all required env vars for the Cloudflare Worker Loader | ||
| 32 | * runtime are present. | ||
| 33 | */ | ||
| 34 | export function isCloudflareWorkerLoaderConfigured(): boolean { | ||
| 35 | return Boolean( | ||
| 36 | process.env.CLOUDFLARE_SANDBOX_RUN_URL | ||
| 37 | && process.env.CLOUDFLARE_SANDBOX_AUTH_TOKEN, | ||
| 38 | ); | ||
| 39 | } | ||
| 40 | |||
| 41 | /** | ||
| 42 | * Reads Cloudflare Worker Loader config from environment variables. | ||
| 43 | * Throws if required vars are missing. | ||
| 44 | */ | ||
| 45 | export function getCloudflareWorkerLoaderConfig(): CloudflareWorkerLoaderConfig { | ||
| 46 | const runUrl = process.env.CLOUDFLARE_SANDBOX_RUN_URL; | ||
| 47 | const authToken = process.env.CLOUDFLARE_SANDBOX_AUTH_TOKEN; | ||
| 48 | |||
| 49 | if (!runUrl || !authToken) { | ||
| 50 | throw new Error( | ||
| 51 | "Cloudflare Worker Loader runtime requires CLOUDFLARE_SANDBOX_RUN_URL and CLOUDFLARE_SANDBOX_AUTH_TOKEN", | ||
| 52 | ); | ||
| 53 | } | ||
| 54 | |||
| 55 | const callbackConvexUrl = process.env.CONVEX_URL ?? process.env.CONVEX_SITE_URL; | ||
| 56 | if (!callbackConvexUrl) { | ||
| 57 | throw new Error( | ||
| 58 | "Cloudflare Worker Loader runtime requires CONVEX_SITE_URL or CONVEX_URL for callback RPC", | ||
| 59 | ); | ||
| 60 | } | ||
| 61 | |||
| 62 | const callbackInternalSecret = process.env.EXECUTOR_INTERNAL_TOKEN; | ||
| 63 | if (!callbackInternalSecret) { | ||
| 64 | throw new Error( | ||
| 65 | "Cloudflare Worker Loader runtime requires EXECUTOR_INTERNAL_TOKEN for authenticated callback RPC", | ||
| 66 | ); | ||
| 67 | } | ||
| 68 | |||
| 69 | const requestTimeoutMs = Number( | ||
| 70 | process.env.CLOUDFLARE_SANDBOX_REQUEST_TIMEOUT_MS ?? "90000", | ||
| 71 | ); | ||
| 72 | |||
| 73 | return { | ||
| 74 | runUrl, | ||
| 75 | authToken, | ||
| 76 | requestTimeoutMs, | ||
| 77 | callbackConvexUrl, | ||
| 78 | callbackInternalSecret, | ||
| 79 | }; | ||
| 80 | } | ||
| @@ -5,50 +5,20 @@ · 4 unmodified lines | |||
| 5 | // the core helpers here (formatArgs, createToolsProxy, console proxy, execution | 5 | // the core helpers here (formatArgs, createToolsProxy, console proxy, execution |
| 6 | // loop, result mapping) should be mirrored there. | 6 | // loop, result mapping) should be mirrored there. |
| 7 | import { APPROVAL_DENIED_PREFIX, TASK_TIMEOUT_MARKER } from "../execution_constants"; | 7 | import { APPROVAL_DENIED_PREFIX, TASK_TIMEOUT_MARKER } from "../execution_constants"; |
| 8 | import { Result } from "better-result"; | ||
| 8 | import { Script, createContext } from "node:vm"; | 9 | import { Script, createContext } from "node:vm"; |
| 9 | import type { | 10 | import type { |
| 10 | ExecutionAdapter, | 11 | ExecutionAdapter, |
| 11 | SandboxExecutionRequest, | 12 | SandboxExecutionRequest, |
| 12 | SandboxExecutionResult, | 13 | SandboxExecutionResult, |
| 13 | } from "../types"; | 14 | } from "../types"; |
| 14 | 15 | import { transpileForRuntime } from "./transpile"; | |
| 15 | async function transpileForRuntime(code: string): Promise<string> { | ||
| 16 | let ts: typeof import("typescript"); | ||
| 17 | try { | ||
| 18 | ts = require("typescript"); | ||
| 19 | } catch { | ||
| 20 | return code; | ||
| 21 | } | ||
| 22 | |||
| 23 | const target = ts.ScriptTarget?.ES2022 ?? ts.ScriptTarget?.ESNext; | ||
| 24 | const moduleKind = ts.ModuleKind?.ESNext; | ||
| 25 | |||
| 26 | const result = ts.transpileModule(code, { | ||
| 27 | compilerOptions: { | ||
| 28 | ...(target !== undefined ? { target } : {}), | ||
| 29 | ...(moduleKind !== undefined ? { module: moduleKind } : {}), | ||
| 30 | }, | ||
| 31 | reportDiagnostics: true, | ||
| 32 | }); | ||
| 33 | |||
| 34 | if (result.diagnostics && result.diagnostics.length > 0) { | ||
| 35 | const first = result.diagnostics[0]; | ||
| 36 | const message = ts.flattenDiagnosticMessageText(first.messageText, "\n"); | ||
| 37 | throw new Error(`TypeScript transpile error: ${message}`); | ||
| 38 | } | ||
| 39 | |||
| 40 | return result.outputText || code; | ||
| 41 | } | ||
| 42 | 16 | ||
| 43 | function formatArgs(args: unknown[]): string { | 17 | function formatArgs(args: unknown[]): string { |
| 44 | return args | 18 | return args |
| 45 | .map((value) => { | 19 | .map((value) => { |
| 46 | if (typeof value === "string") return value; | 20 | if (typeof value === "string") return value; |
| 47 | try { | 21 | return Result.try(() => JSON.stringify(value)).unwrapOr(String(value)); |
| 48 | return JSON.stringify(value); | ||
| 49 | } catch { | ||
| 50 | return String(value); | ||
| 51 | } | ||
| 52 | }) | 22 | }) |
| 53 | .join(" "); | 23 | .join(" "); |
| 54 | } | 24 | } |
| @@ -59,6 +29,10 @@ function fireAndForget(promise: void | Promise<void>): void { · 4 unmodified lines | |||
| 59 | } | 29 | } |
| 60 | } | 30 | } |
| 61 | 31 | ||
| 32 | async function sleep(ms: number): Promise<void> { | ||
| 33 | await new Promise((resolve) => setTimeout(resolve, ms)); | ||
| 34 | } | ||
| 35 | |||
| 62 | function createToolsProxy( | 36 | function createToolsProxy( |
| 63 | adapter: ExecutionAdapter, | 37 | adapter: ExecutionAdapter, |
| 64 | runId: string, | 38 | runId: string, |
| @@ -78,26 +52,59 @@ function createToolsProxy( · 13 unmodified lines | |||
| 78 | } | 52 | } |
| 79 | 53 | ||
| 80 | const input = args.length > 0 ? args[0] : {}; | 54 | const input = args.length > 0 ? args[0] : {}; |
| 81 | const result = await adapter.invokeTool({ | 55 | const callId = `call_${crypto.randomUUID()}`; |
| 82 | runId, | 56 | |
| 83 | callId: `call_${crypto.randomUUID()}`, | 57 | while (true) { |
| 84 | toolPath, | 58 | const result = await adapter.invokeTool({ |
| 85 | input, | 59 | runId, |
| 86 | }); | 60 | callId, |
| 87 | 61 | toolPath, | |
| 88 | if (result.ok) { | 62 | input, |
| 89 | return result.value; | 63 | }); |
| 90 | } | 64 | |
| 91 | 65 | if (result.ok) { | |
| 92 | if (result.denied) { | 66 | return result.value; |
| 93 | throw new Error(`${APPROVAL_DENIED_PREFIX}${result.error}`); | 67 | } |
| 68 | |||
| 69 | switch (result.kind) { | ||
| 70 | case "pending": | ||
| 71 | await sleep(Math.max(50, result.retryAfterMs ?? 500)); | ||
| 72 | continue; | ||
| 73 | case "denied": | ||
| 74 | throw new Error(`${APPROVAL_DENIED_PREFIX}${result.error}`); | ||
| 75 | case "failed": | ||
| 76 | default: | ||
| 77 | throw new Error(result.error); | ||
| 78 | } | ||
| 94 | } | 79 | } |
| 95 | |||
| 96 | throw new Error(result.error); | ||
| 97 | }, | 80 | }, |
| 98 | }); | 81 | }); |
| 99 | } | 82 | } |
| 100 | 83 | ||
| 84 | /** Classify a caught error from VM execution into a result status. */ | ||
| 85 | function classifyExecutionError( | ||
| 86 | error: unknown, | ||
| 87 | request: SandboxExecutionRequest, | ||
| 88 | ): { status: SandboxExecutionResult["status"]; message: string } { | ||
| 89 | const message = error instanceof Error ? error.message : String(error); | ||
| 90 | |||
| 91 | if (message === TASK_TIMEOUT_MARKER || message.includes("Script execution timed out")) { | ||
| 92 | return { | ||
| 93 | status: "timed_out", | ||
| 94 | message: `Execution timed out after ${request.timeoutMs}ms`, | ||
| 95 | }; | ||
| 96 | } | ||
| 97 | |||
| 98 | if (message.startsWith(APPROVAL_DENIED_PREFIX)) { | ||
| 99 | return { | ||
| 100 | status: "denied", | ||
| 101 | message: message.replace(APPROVAL_DENIED_PREFIX, "").trim(), | ||
| 102 | }; | ||
| 103 | } | ||
| 104 | |||
| 105 | return { status: "failed", message }; | ||
| 106 | } | ||
| 107 | |||
| 101 | export async function runCodeWithAdapter( | 108 | export async function runCodeWithAdapter( |
| 102 | request: SandboxExecutionRequest, | 109 | request: SandboxExecutionRequest, |
| 103 | adapter: ExecutionAdapter, | 110 | adapter: ExecutionAdapter, |
| @@ -130,6 +137,26 @@ export async function runCodeWithAdapter( · 26 unmodified lines | |||
| 130 | ); | 137 | ); |
| 131 | }; | 138 | }; |
| 132 | 139 | ||
| 140 | const mkResult = ( | ||
| 141 | status: SandboxExecutionResult["status"], | ||
| 142 | opts?: { error?: string; exitCode?: number }, | ||
| 143 | ): SandboxExecutionResult => ({ | ||
| 144 | status, | ||
| 145 | stdout: stdoutLines.join("\n"), | ||
| 146 | stderr: stderrLines.join("\n"), | ||
| 147 | exitCode: opts?.exitCode, | ||
| 148 | error: opts?.error, | ||
| 149 | durationMs: Date.now() - startedAt, | ||
| 150 | }); | ||
| 151 | |||
| 152 | // ── Transpile ────────────────────────────────────────────────────────── | ||
| 153 | const transpiled = transpileForRuntime(request.code); | ||
| 154 | if (transpiled.isErr()) { | ||
| 155 | appendStderr(transpiled.error.message); | ||
| 156 | return mkResult("failed", { error: transpiled.error.message }); | ||
| 157 | } | ||
| 158 | |||
| 159 | // ── Sandbox setup ────────────────────────────────────────────────────── | ||
| 133 | const tools = createToolsProxy(adapter, request.taskId); | 160 | const tools = createToolsProxy(adapter, request.taskId); |
| 134 | const consoleProxy = { | 161 | const consoleProxy = { |
| 135 | log: (...args: unknown[]) => appendStdout(formatArgs(args)), | 162 | log: (...args: unknown[]) => appendStdout(formatArgs(args)), |
| @@ -145,15 +172,14 @@ export async function runCodeWithAdapter( · 9 unmodified lines | |||
| 145 | clearTimeout, | 172 | clearTimeout, |
| 146 | }); | 173 | }); |
| 147 | const context = createContext(sandbox, { | 174 | const context = createContext(sandbox, { |
| 148 | codeGeneration: { | 175 | codeGeneration: { strings: false, wasm: false }, |
| 149 | strings: false, | ||
| 150 | wasm: false, | ||
| 151 | }, | ||
| 152 | }); | 176 | }); |
| 153 | 177 | ||
| 154 | const executableCode = await transpileForRuntime(request.code); | 178 | const runnerScript = new Script( |
| 155 | const runnerScript = new Script(`(async () => {\n"use strict";\n${executableCode}\n})()`); | 179 | `(async () => {\n"use strict";\n${transpiled.value}\n})()`, |
| 180 | ); | ||
| 156 | 181 | ||
| 182 | // ── Execute with timeout ─────────────────────────────────────────────── | ||
| 157 | let timeoutHandle: ReturnType<typeof setTimeout> | undefined; | 183 | let timeoutHandle: ReturnType<typeof setTimeout> | undefined; |
| 158 | const timeoutPromise = new Promise<never>((_resolve, reject) => { | 184 | const timeoutPromise = new Promise<never>((_resolve, reject) => { |
| 159 | timeoutHandle = setTimeout(() => { | 185 | timeoutHandle = setTimeout(() => { |
| @@ -161,60 +187,32 @@ export async function runCodeWithAdapter( · 1 unmodified line | |||
| 161 | }, request.timeoutMs); | 187 | }, request.timeoutMs); |
| 162 | }); | 188 | }); |
| 163 | 189 | ||
| 164 | try { | 190 | const execution = await Result.tryPromise(async () => { |
| 165 | const value = await Promise.race([ | 191 | const value = await Promise.race([ |
| 166 | Promise.resolve(runnerScript.runInContext(context, { timeout: Math.max(1, request.timeoutMs) })), | 192 | Promise.resolve( |
| 193 | runnerScript.runInContext(context, { | ||
| 194 | timeout: Math.max(1, request.timeoutMs), | ||
| 195 | }), | ||
| 196 | ), | ||
| 167 | timeoutPromise, | 197 | timeoutPromise, |
| 168 | ]); | 198 | ]); |
| 199 | return value; | ||
| 200 | }); | ||
| 169 | 201 | ||
| 170 | if (value !== undefined) { | 202 | if (timeoutHandle) clearTimeout(timeoutHandle); |
| 171 | appendStdout(`result: ${formatArgs([value])}`); | ||
| 172 | } | ||
| 173 | |||
| 174 | return { | ||
| 175 | status: "completed", | ||
| 176 | stdout: stdoutLines.join("\n"), | ||
| 177 | stderr: stderrLines.join("\n"), | ||
| 178 | exitCode: 0, | ||
| 179 | durationMs: Date.now() - startedAt, | ||
| 180 | }; | ||
| 181 | } catch (error) { | ||
| 182 | const message = error instanceof Error ? error.message : String(error); | ||
| 183 | if (message === TASK_TIMEOUT_MARKER || message.includes("Script execution timed out")) { | ||
| 184 | const timeoutMessage = `Execution timed out after ${request.timeoutMs}ms`; | ||
| 185 | appendStderr(timeoutMessage); | ||
| 186 | return { | ||
| 187 | status: "timed_out", | ||
| 188 | stdout: stdoutLines.join("\n"), | ||
| 189 | stderr: stderrLines.join("\n"), | ||
| 190 | error: timeoutMessage, | ||
| 191 | durationMs: Date.now() - startedAt, | ||
| 192 | }; | ||
| 193 | } | ||
| 194 | |||
| 195 | if (message.startsWith(APPROVAL_DENIED_PREFIX)) { | ||
| 196 | const deniedMessage = message.replace(APPROVAL_DENIED_PREFIX, "").trim(); | ||
| 197 | appendStderr(deniedMessage); | ||
| 198 | return { | ||
| 199 | status: "denied", | ||
| 200 | stdout: stdoutLines.join("\n"), | ||
| 201 | stderr: stderrLines.join("\n"), | ||
| 202 | error: deniedMessage, | ||
| 203 | durationMs: Date.now() - startedAt, | ||
| 204 | }; | ||
| 205 | } | ||
| 206 | 203 | ||
| 204 | if (execution.isErr()) { | ||
| 205 | const { status, message } = classifyExecutionError( | ||
| 206 | execution.error.cause, | ||
| 207 | request, | ||
| 208 | ); | ||
| 207 | appendStderr(message); | 209 | appendStderr(message); |
| 208 | return { | 210 | return mkResult(status, { error: message }); |
| 209 | status: "failed", | ||
| 210 | stdout: stdoutLines.join("\n"), | ||
| 211 | stderr: stderrLines.join("\n"), | ||
| 212 | error: message, | ||
| 213 | durationMs: Date.now() - startedAt, | ||
| 214 | }; | ||
| 215 | } finally { | ||
| 216 | if (timeoutHandle) { | ||
| 217 | clearTimeout(timeoutHandle); | ||
| 218 | } | ||
| 219 | } | 211 | } |
| 212 | |||
| 213 | if (execution.value !== undefined) { | ||
| 214 | appendStdout(`result: ${formatArgs([execution.value])}`); | ||
| 215 | } | ||
| 216 | |||
| 217 | return mkResult("completed", { exitCode: 0 }); | ||
| 220 | } | 218 | } |
| @@ -0,0 +1,78 @@ | |||
| 1 | "use node"; | ||
| 2 | |||
| 3 | import { Result, TaggedError } from "better-result"; | ||
| 4 | |||
| 5 | // --------------------------------------------------------------------------- | ||
| 6 | // Errors | ||
| 7 | // --------------------------------------------------------------------------- | ||
| 8 | |||
| 9 | export class TranspileError extends TaggedError("TranspileError")<{ | ||
| 10 | message: string; | ||
| 11 | }>() {} | ||
| 12 | |||
| 13 | // --------------------------------------------------------------------------- | ||
| 14 | // TypeScript module loader | ||
| 15 | // --------------------------------------------------------------------------- | ||
| 16 | |||
| 17 | let cachedTypeScript: typeof import("typescript") | null | undefined; | ||
| 18 | |||
| 19 | function getTypeScriptModule(): typeof import("typescript") | null { | ||
| 20 | if (cachedTypeScript === undefined) { | ||
| 21 | const loaded = Result.try(() => require("typescript") as typeof import("typescript")); | ||
| 22 | cachedTypeScript = loaded.isOk() ? loaded.value : null; | ||
| 23 | } | ||
| 24 | return cachedTypeScript ?? null; | ||
| 25 | } | ||
| 26 | |||
| 27 | // --------------------------------------------------------------------------- | ||
| 28 | // Public API | ||
| 29 | // --------------------------------------------------------------------------- | ||
| 30 | |||
| 31 | /** | ||
| 32 | * Transpile TypeScript code to JavaScript using the `typescript` module. | ||
| 33 | * | ||
| 34 | * Returns a Result — either the transpiled JS string or a TranspileError. | ||
| 35 | * If the TypeScript module is not available, the code is returned as-is | ||
| 36 | * (graceful fallback for environments where TS isn't installed). | ||
| 37 | * | ||
| 38 | * Targets ES2022/ESNext so the output can run in modern runtimes (node:vm, | ||
| 39 | * Cloudflare Workers isolates, etc.) without further downlevelling. | ||
| 40 | */ | ||
| 41 | export function transpileForRuntime( | ||
| 42 | code: string, | ||
| 43 | ): Result<string, TranspileError> { | ||
| 44 | const ts = getTypeScriptModule(); | ||
| 45 | if (!ts) return Result.ok(code); | ||
| 46 | |||
| 47 | const target = ts.ScriptTarget?.ES2022 ?? ts.ScriptTarget?.ESNext; | ||
| 48 | const moduleKind = ts.ModuleKind?.ESNext; | ||
| 49 | |||
| 50 | return Result.try({ | ||
| 51 | try: () => { | ||
| 52 | const result = ts.transpileModule(code, { | ||
| 53 | compilerOptions: { | ||
| 54 | ...(target !== undefined ? { target } : {}), | ||
| 55 | ...(moduleKind !== undefined ? { module: moduleKind } : {}), | ||
| 56 | }, | ||
| 57 | reportDiagnostics: true, | ||
| 58 | }); | ||
| 59 | |||
| 60 | if (result.diagnostics && result.diagnostics.length > 0) { | ||
| 61 | const first = result.diagnostics[0]; | ||
| 62 | const message = ts.flattenDiagnosticMessageText( | ||
| 63 | first.messageText, | ||
| 64 | "\n", | ||
| 65 | ); | ||
| 66 | throw new TranspileError({ message: `TypeScript transpile error: ${message}` }); | ||
| 67 | } | ||
| 68 | |||
| 69 | return result.outputText || code; | ||
| 70 | }, | ||
| 71 | catch: (e) => | ||
| 72 | e instanceof TranspileError | ||
| 73 | ? e | ||
| 74 | : new TranspileError({ | ||
| 75 | message: e instanceof Error ? e.message : String(e), | ||
| 76 | }), | ||
| 77 | }); | ||
| 78 | } | ||
| @@ -321,6 +321,90 @@ test("compiled source artifacts can be materialized and executed", async () => { · 320 unmodified lines | |||
| 321 | } | 321 | } |
| 322 | }); | 322 | }); |
| 323 | 323 | ||
| 324 | test("postman collection specs load and execute request tools", async () => { | ||
| 325 | const upstream = Bun.serve({ | ||
| 326 | port: 0, | ||
| 327 | fetch: (req) => { | ||
| 328 | const url = new URL(req.url); | ||
| 329 | if (req.method === "GET" && url.pathname.startsWith("/items/")) { | ||
| 330 | const id = url.pathname.split("/").filter(Boolean)[1] ?? ""; | ||
| 331 | return Response.json({ | ||
| 332 | ok: true, | ||
| 333 | id, | ||
| 334 | expand: url.searchParams.get("expand"), | ||
| 335 | limit: url.searchParams.get("limit"), | ||
| 336 | token: req.headers.get("x-token"), | ||
| 337 | }); | ||
| 338 | } | ||
| 339 | return new Response("not found", { status: 404 }); | ||
| 340 | }, | ||
| 341 | }); | ||
| 342 | |||
| 343 | const proxy = Bun.serve({ | ||
| 344 | port: 0, | ||
| 345 | fetch: async (req) => { | ||
| 346 | const body = (await req.json()) as { service?: string; method?: string; path?: string }; | ||
| 347 | if (body.service === "sync" && body.method === "GET" && body.path === "/collection/123-demo?populate=true") { | ||
| 348 | return Response.json({ | ||
| 349 | data: { | ||
| 350 | name: "Demo collection", | ||
| 351 | variables: [{ key: "token", value: "Bearer demo-token" }], | ||
| 352 | folders: [{ id: "folder_items", name: "Items" }], | ||
| 353 | requests: [ | ||
| 354 | { | ||
| 355 | id: "request_get_item", | ||
| 356 | name: "Get item", | ||
| 357 | method: "GET", | ||
| 358 | url: `http://127.0.0.1:${upstream.port}/items/{{itemId}}`, | ||
| 359 | folder: "folder_items", | ||
| 360 | headerData: [{ key: "x-token", value: "{{token}}" }], | ||
| 361 | queryParams: [{ key: "expand", value: "details" }], | ||
| 362 | pathVariableData: [{ key: "itemId", value: "default-id" }], | ||
| 363 | }, | ||
| 364 | ], | ||
| 365 | }, | ||
| 366 | }); | ||
| 367 | } | ||
| 368 | return Response.json({ error: "unknown request" }, { status: 400 }); | ||
| 369 | }, | ||
| 370 | }); | ||
| 371 | |||
| 372 | try { | ||
| 373 | const { tools, warnings } = await loadExternalTools([ | ||
| 374 | { | ||
| 375 | type: "openapi", | ||
| 376 | name: "postman-demo", | ||
| 377 | spec: "postman:123-demo", | ||
| 378 | postmanProxyUrl: `http://127.0.0.1:${proxy.port}`, | ||
| 379 | }, | ||
| 380 | ]); | ||
| 381 | |||
| 382 | expect(warnings).toHaveLength(0); | ||
| 383 | expect(tools).toHaveLength(1); | ||
| 384 | expect(tools[0]?.source).toBe("catalog:postman-demo"); | ||
| 385 | expect(tools[0]?.path).toContain("items"); | ||
| 386 | |||
| 387 | const result = await tools[0]!.run( | ||
| 388 | { | ||
| 389 | variables: { itemId: "abc-123" }, | ||
| 390 | query: { limit: 10 }, | ||
| 391 | }, | ||
| 392 | { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | ||
| 393 | ); | ||
| 394 | |||
| 395 | expect(result).toEqual({ | ||
| 396 | ok: true, | ||
| 397 | id: "abc-123", | ||
| 398 | expand: "details", | ||
| 399 | limit: "10", | ||
| 400 | token: "Bearer demo-token", | ||
| 401 | }); | ||
| 402 | } finally { | ||
| 403 | proxy.stop(true); | ||
| 404 | upstream.stop(true); | ||
| 405 | } | ||
| 406 | }); | ||
| 407 | |||
| 324 | test("parseGraphqlOperationPaths handles aliases and fragments", async () => { | 408 | test("parseGraphqlOperationPaths handles aliases and fragments", async () => { |
| 325 | const parsed = parseGraphqlOperationPaths( | 409 | const parsed = parseGraphqlOperationPaths( |
| 326 | "linear", | 410 | "linear", |
| @@ -36,6 +36,10 @@ export interface OpenApiToolSourceConfig { · 35 unmodified lines | |||
| 36 | sourceId?: string; | 36 | sourceId?: string; |
| 37 | sourceKey?: string; | 37 | sourceKey?: string; |
| 38 | spec: string | Record<string, unknown>; | 38 | spec: string | Record<string, unknown>; |
| 39 | /** Optional canonical Postman collection URL for display/debugging. */ | ||
| 40 | collectionUrl?: string; | ||
| 41 | /** Optional override for Postman ws/proxy endpoint (primarily for tests). */ | ||
| 42 | postmanProxyUrl?: string; | ||
| 39 | baseUrl?: string; | 43 | baseUrl?: string; |
| 40 | auth?: OpenApiAuth; | 44 | auth?: OpenApiAuth; |
| 41 | defaultReadApproval?: ToolApprovalMode; | 45 | defaultReadApproval?: ToolApprovalMode; |
| @@ -62,6 +66,23 @@ export type ExternalToolSourceConfig = · 20 unmodified lines | |||
| 62 | | OpenApiToolSourceConfig | 66 | | OpenApiToolSourceConfig |
| 63 | | GraphqlToolSourceConfig; | 67 | | GraphqlToolSourceConfig; |
| 64 | 68 | ||
| 69 | interface PostmanSerializedRunSpec { | ||
| 70 | kind: "postman"; | ||
| 71 | method: string; | ||
| 72 | url: string; | ||
| 73 | headers: Record<string, string>; | ||
| 74 | queryParams: Array<{ key: string; value: string }>; | ||
| 75 | body?: | ||
| 76 | | { kind: "urlencoded"; entries: Array<{ key: string; value: string }> } | ||
| 77 | | { kind: "raw"; text: string }; | ||
| 78 | variables: Record<string, string>; | ||
| 79 | authHeaders: Record<string, string>; | ||
| 80 | } | ||
| 81 | |||
| 82 | const POSTMAN_SPEC_PREFIX = "postman:"; | ||
| 83 | const DEFAULT_POSTMAN_PROXY_URL = "https://www.postman.com/_api/ws/proxy"; | ||
| 84 | const POSTMAN_TEMPLATE_PATTERN = /\{\{([^{}]+)\}\}/g; | ||
| 85 | |||
| 65 | function sanitizeSegment(value: string): string { | 86 | function sanitizeSegment(value: string): string { |
| 66 | const cleanedBase = value | 87 | const cleanedBase = value |
| 67 | .toLowerCase() | 88 | .toLowerCase() |
| @@ -665,6 +686,161 @@ function getCredentialSourceKey(config: { · 597 unmodified lines | |||
| 665 | return config.sourceKey ?? `${config.type}:${config.name}`; | 686 | return config.sourceKey ?? `${config.type}:${config.name}`; |
| 666 | } | 687 | } |
| 667 | 688 | ||
| 689 | export function parsePostmanCollectionUid(spec: string): string | null { | ||
| 690 | if (!spec.startsWith(POSTMAN_SPEC_PREFIX)) { | ||
| 691 | return null; | ||
| 692 | } | ||
| 693 | |||
| 694 | const uid = spec.slice(POSTMAN_SPEC_PREFIX.length).trim(); | ||
| 695 | if (!uid) { | ||
| 696 | return null; | ||
| 697 | } | ||
| 698 | |||
| 699 | return uid; | ||
| 700 | } | ||
| 701 | |||
| 702 | function stringifyTemplateValue(value: unknown): string { | ||
| 703 | if (value === null || value === undefined) return ""; | ||
| 704 | if (typeof value === "string") return value; | ||
| 705 | if (typeof value === "number" || typeof value === "boolean") return String(value); | ||
| 706 | return JSON.stringify(value); | ||
| 707 | } | ||
| 708 | |||
| 709 | function interpolatePostmanTemplate(value: string, variables: Record<string, string>): string { | ||
| 710 | return value.replace(POSTMAN_TEMPLATE_PATTERN, (_, rawKey: string) => { | ||
| 711 | const key = rawKey.trim(); | ||
| 712 | return Object.prototype.hasOwnProperty.call(variables, key) | ||
| 713 | ? variables[key]! | ||
| 714 | : `{{${key}}}`; | ||
| 715 | }); | ||
| 716 | } | ||
| 717 | |||
| 718 | function findUnresolvedPostmanTemplateKeys(value: string): string[] { | ||
| 719 | const keys = new Set<string>(); | ||
| 720 | let match: RegExpExecArray | null; | ||
| 721 | const pattern = new RegExp(POSTMAN_TEMPLATE_PATTERN.source, "g"); | ||
| 722 | while ((match = pattern.exec(value)) !== null) { | ||
| 723 | const key = String(match[1] ?? "").trim(); | ||
| 724 | if (key) keys.add(key); | ||
| 725 | } | ||
| 726 | return [...keys]; | ||
| 727 | } | ||
| 728 | |||
| 729 | function asStringRecord(value: unknown): Record<string, string> { | ||
| 730 | if (!value || typeof value !== "object" || Array.isArray(value)) return {}; | ||
| 731 | const result: Record<string, string> = {}; | ||
| 732 | for (const [key, entry] of Object.entries(value as Record<string, unknown>)) { | ||
| 733 | if (!key) continue; | ||
| 734 | result[key] = stringifyTemplateValue(entry); | ||
| 735 | } | ||
| 736 | return result; | ||
| 737 | } | ||
| 738 | |||
| 739 | function detectJsonContentType(headers: Record<string, string>): boolean { | ||
| 740 | const contentType = Object.entries(headers).find(([name]) => name.toLowerCase() === "content-type")?.[1] ?? ""; | ||
| 741 | return contentType.toLowerCase().includes("json"); | ||
| 742 | } | ||
| 743 | |||
| 744 | async function executePostmanRequest( | ||
| 745 | runSpec: PostmanSerializedRunSpec, | ||
| 746 | payload: Record<string, unknown>, | ||
| 747 | credentialHeaders?: Record<string, string>, | ||
| 748 | ): Promise<unknown> { | ||
| 749 | const variables = { | ||
| 750 | ...runSpec.variables, | ||
| 751 | ...asStringRecord(payload.variables), | ||
| 752 | }; | ||
| 753 | |||
| 754 | const interpolatedUrl = interpolatePostmanTemplate(runSpec.url, variables); | ||
| 755 | const unresolvedUrlKeys = findUnresolvedPostmanTemplateKeys(interpolatedUrl); | ||
| 756 | if (unresolvedUrlKeys.length > 0) { | ||
| 757 | throw new Error(`Missing required URL variables: ${unresolvedUrlKeys.join(", ")}`); | ||
| 758 | } | ||
| 759 | |||
| 760 | let url: URL; | ||
| 761 | try { | ||
| 762 | url = new URL(interpolatedUrl); | ||
| 763 | } catch { | ||
| 764 | throw new Error(`Invalid request URL: ${interpolatedUrl}`); | ||
| 765 | } | ||
| 766 | |||
| 767 | for (const entry of runSpec.queryParams) { | ||
| 768 | if (!entry.key) continue; | ||
| 769 | const value = interpolatePostmanTemplate(entry.value, variables); | ||
| 770 | if (value.length > 0) { | ||
| 771 | url.searchParams.set(entry.key, value); | ||
| 772 | } | ||
| 773 | } | ||
| 774 | |||
| 775 | const queryOverrides = asRecord(payload.query); | ||
| 776 | for (const [key, value] of Object.entries(queryOverrides)) { | ||
| 777 | if (!key || value === undefined || value === null) continue; | ||
| 778 | url.searchParams.set(key, stringifyTemplateValue(value)); | ||
| 779 | } | ||
| 780 | |||
| 781 | const headers: Record<string, string> = {}; | ||
| 782 | for (const [name, value] of Object.entries(runSpec.headers)) { | ||
| 783 | if (!name) continue; | ||
| 784 | headers[name] = interpolatePostmanTemplate(value, variables); | ||
| 785 | } | ||
| 786 | Object.assign(headers, runSpec.authHeaders); | ||
| 787 | Object.assign(headers, credentialHeaders ?? {}); | ||
| 788 | |||
| 789 | const headerOverrides = asRecord(payload.headers); | ||
| 790 | for (const [name, value] of Object.entries(headerOverrides)) { | ||
| 791 | if (!name || value === undefined || value === null) continue; | ||
| 792 | headers[name] = stringifyTemplateValue(value); | ||
| 793 | } | ||
| 794 | |||
| 795 | const method = runSpec.method.toUpperCase(); | ||
| 796 | const readMethods = new Set(["GET", "HEAD", "OPTIONS"]); | ||
| 797 | let body: string | undefined; | ||
| 798 | |||
| 799 | if (!readMethods.has(method)) { | ||
| 800 | const hasExplicitBody = Object.prototype.hasOwnProperty.call(payload, "body"); | ||
| 801 | if (hasExplicitBody) { | ||
| 802 | const bodyValue = payload.body; | ||
| 803 | if (typeof bodyValue === "string") { | ||
| 804 | body = bodyValue; | ||
| 805 | } else if (bodyValue !== undefined) { | ||
| 806 | body = JSON.stringify(bodyValue); | ||
| 807 | if (!Object.keys(headers).some((name) => name.toLowerCase() === "content-type")) { | ||
| 808 | headers["content-type"] = "application/json"; | ||
| 809 | } | ||
| 810 | } | ||
| 811 | } else if (runSpec.body?.kind === "urlencoded") { | ||
| 812 | const params = new URLSearchParams(); | ||
| 813 | for (const entry of runSpec.body.entries) { | ||
| 814 | if (!entry.key) continue; | ||
| 815 | params.set(entry.key, interpolatePostmanTemplate(entry.value, variables)); | ||
| 816 | } | ||
| 817 | body = params.toString(); | ||
| 818 | if (!Object.keys(headers).some((name) => name.toLowerCase() === "content-type")) { | ||
| 819 | headers["content-type"] = "application/x-www-form-urlencoded"; | ||
| 820 | } | ||
| 821 | } else if (runSpec.body?.kind === "raw") { | ||
| 822 | body = interpolatePostmanTemplate(runSpec.body.text, variables); | ||
| 823 | } | ||
| 824 | } | ||
| 825 | |||
| 826 | const response = await fetch(url.toString(), { | ||
| 827 | method, | ||
| 828 | headers, | ||
| 829 | body, | ||
| 830 | }); | ||
| 831 | |||
| 832 | if (!response.ok) { | ||
| 833 | const text = await response.text().catch(() => ""); | ||
| 834 | throw new Error(`HTTP ${response.status} ${response.statusText}: ${text.slice(0, 500)}`); | ||
| 835 | } | ||
| 836 | |||
| 837 | if (detectJsonContentType(headers) || (response.headers.get("content-type") ?? "").includes("json")) { | ||
| 838 | return await response.json(); | ||
| 839 | } | ||
| 840 | |||
| 841 | return await response.text(); | ||
| 842 | } | ||
| 843 | |||
| 668 | function buildOpenApiUrl( | 844 | function buildOpenApiUrl( |
| 669 | baseUrl: string, | 845 | baseUrl: string, |
| 670 | pathTemplate: string, | 846 | pathTemplate: string, |
| @@ -828,7 +1004,6 @@ function compactOpenApiPaths( · 157 unmodified lines | |||
| 828 | ? jsonSchemaTypeHintFallback(combinedSchema, 0, compSchemas) | 1004 | ? jsonSchemaTypeHintFallback(combinedSchema, 0, compSchemas) |
| 829 | : "{}"; | 1005 | : "{}"; |
| 830 | compactOperation._returnsTypeHint = responseTypeHintFromSchema(responseSchema, responseStatus, compSchemas); | 1006 | compactOperation._returnsTypeHint = responseTypeHintFromSchema(responseSchema, responseStatus, compSchemas); |
| 831 | compactOperation._usesGeneratedTypes = true; | ||
| 832 | const previewKeys = [ | 1007 | const previewKeys = [ |
| 833 | ...mergedParameters.map((param) => String(param.name ?? "")).filter((name) => name.length > 0), | 1008 | ...mergedParameters.map((param) => String(param.name ?? "")).filter((name) => name.length > 0), |
| 834 | ...Object.keys(asRecord(requestBodySchema.properties)), | 1009 | ...Object.keys(asRecord(requestBodySchema.properties)), |
| @@ -1105,16 +1280,10 @@ export function buildOpenApiToolsFromPrepared( · 270 unmodified lines | |||
| 1105 | } | 1280 | } |
| 1106 | } | 1281 | } |
| 1107 | 1282 | ||
| 1108 | const usesGeneratedTypes = Boolean(operation._usesGeneratedTypes); | ||
| 1109 | const operationIdKey = JSON.stringify(operationIdRaw); | ||
| 1110 | const displayArgsType = argPreviewKeys.length > 0 | 1283 | const displayArgsType = argPreviewKeys.length > 0 |
| 1111 | ? compactArgKeysHint(argPreviewKeys) | 1284 | ? compactArgKeysHint(argPreviewKeys) |
| 1112 | : usesGeneratedTypes | 1285 | : compactArgTypeHint(argsType); |
| 1113 | ? `ToolInput<operations[${operationIdKey}]>` | 1286 | const displayReturnsType = compactReturnTypeHint(returnsType); |
| 1114 | : compactArgTypeHint(argsType); | ||
| 1115 | const displayReturnsType = usesGeneratedTypes | ||
| 1116 | ? `ToolOutput<operations[${operationIdKey}]>` | ||
| 1117 | : compactReturnTypeHint(returnsType); | ||
| 1118 | 1287 | ||
| 1119 | const approval = config.overrides?.[operationIdRaw]?.approval | 1288 | const approval = config.overrides?.[operationIdRaw]?.approval |
| 1120 | ?? (readMethods.has(method) | 1289 | ?? (readMethods.has(method) |
| @@ -1185,7 +1354,234 @@ export function buildOpenApiToolsFromPrepared( · 64 unmodified lines | |||
| 1185 | return tools; | 1354 | return tools; |
| 1186 | } | 1355 | } |
| 1187 | 1356 | ||
| 1357 | function buildPostmanToolPath( | ||
| 1358 | sourceName: string, | ||
| 1359 | requestName: string, | ||
| 1360 | folderPath: string[], | ||
| 1361 | usedPaths: Set<string>, | ||
| 1362 | ): string { | ||
| 1363 | const source = sanitizeSegment(sourceName); | ||
| 1364 | const segments = [ | ||
| 1365 | source, | ||
| 1366 | ...folderPath.map((segment) => sanitizeSegment(segment)).filter((segment) => segment.length > 0), | ||
| 1367 | sanitizeSnakeSegment(requestName), | ||
| 1368 | ]; | ||
| 1369 | const basePath = segments.join("."); | ||
| 1370 | |||
| 1371 | let path = basePath; | ||
| 1372 | let suffix = 2; | ||
| 1373 | while (usedPaths.has(path)) { | ||
| 1374 | path = `${basePath}_${suffix}`; | ||
| 1375 | suffix += 1; | ||
| 1376 | } | ||
| 1377 | usedPaths.add(path); | ||
| 1378 | return path; | ||
| 1379 | } | ||
| 1380 | |||
| 1381 | function resolvePostmanFolderPath( | ||
| 1382 | folderId: string | undefined, | ||
| 1383 | folderById: Map<string, { name: string; parentId?: string }>, | ||
| 1384 | ): string[] { | ||
| 1385 | const path: string[] = []; | ||
| 1386 | let cursor = folderId; | ||
| 1387 | let safety = 0; | ||
| 1388 | while (cursor && safety < 100) { | ||
| 1389 | safety += 1; | ||
| 1390 | const folder = folderById.get(cursor); | ||
| 1391 | if (!folder) break; | ||
| 1392 | path.unshift(folder.name); | ||
| 1393 | cursor = folder.parentId; | ||
| 1394 | } | ||
| 1395 | return path; | ||
| 1396 | } | ||
| 1397 | |||
| 1398 | function extractPostmanVariableMap(value: unknown): Record<string, string> { | ||
| 1399 | if (!Array.isArray(value)) return {}; | ||
| 1400 | const result: Record<string, string> = {}; | ||
| 1401 | for (const entry of value) { | ||
| 1402 | if (!entry || typeof entry !== "object") continue; | ||
| 1403 | const record = entry as Record<string, unknown>; | ||
| 1404 | const key = typeof record.key === "string" ? record.key.trim() : ""; | ||
| 1405 | if (!key) continue; | ||
| 1406 | if (record.disabled === true) continue; | ||
| 1407 | result[key] = stringifyTemplateValue(record.value); | ||
| 1408 | } | ||
| 1409 | return result; | ||
| 1410 | } | ||
| 1411 | |||
| 1412 | function extractPostmanHeaderMap(value: unknown): Record<string, string> { | ||
| 1413 | if (!Array.isArray(value)) return {}; | ||
| 1414 | const result: Record<string, string> = {}; | ||
| 1415 | for (const entry of value) { | ||
| 1416 | if (!entry || typeof entry !== "object") continue; | ||
| 1417 | const record = entry as Record<string, unknown>; | ||
| 1418 | const key = typeof record.key === "string" ? record.key.trim() : ""; | ||
| 1419 | if (!key || record.disabled === true) continue; | ||
| 1420 | result[key] = stringifyTemplateValue(record.value); | ||
| 1421 | } | ||
| 1422 | return result; | ||
| 1423 | } | ||
| 1424 | |||
| 1425 | function extractPostmanQueryEntries(value: unknown): Array<{ key: string; value: string }> { | ||
| 1426 | if (!Array.isArray(value)) return []; | ||
| 1427 | const entries: Array<{ key: string; value: string }> = []; | ||
| 1428 | for (const entry of value) { | ||
| 1429 | if (!entry || typeof entry !== "object") continue; | ||
| 1430 | const record = entry as Record<string, unknown>; | ||
| 1431 | const key = typeof record.key === "string" ? record.key.trim() : ""; | ||
| 1432 | if (!key || record.disabled === true) continue; | ||
| 1433 | entries.push({ key, value: stringifyTemplateValue(record.value) }); | ||
| 1434 | } | ||
| 1435 | return entries; | ||
| 1436 | } | ||
| 1437 | |||
| 1438 | function extractPostmanBody(record: Record<string, unknown>): PostmanSerializedRunSpec["body"] { | ||
| 1439 | const dataMode = typeof record.dataMode === "string" ? record.dataMode.toLowerCase() : ""; | ||
| 1440 | if (dataMode === "urlencoded" && Array.isArray(record.data)) { | ||
| 1441 | const entries: Array<{ key: string; value: string }> = []; | ||
| 1442 | for (const item of record.data) { | ||
| 1443 | if (!item || typeof item !== "object") continue; | ||
| 1444 | const entry = item as Record<string, unknown>; | ||
| 1445 | const key = typeof entry.key === "string" ? entry.key.trim() : ""; | ||
| 1446 | if (!key || entry.disabled === true) continue; | ||
| 1447 | entries.push({ key, value: stringifyTemplateValue(entry.value) }); | ||
| 1448 | } | ||
| 1449 | return entries.length > 0 ? { kind: "urlencoded", entries } : undefined; | ||
| 1450 | } | ||
| 1451 | |||
| 1452 | if (typeof record.rawModeData === "string" && record.rawModeData.length > 0) { | ||
| 1453 | return { kind: "raw", text: record.rawModeData }; | ||
| 1454 | } | ||
| 1455 | |||
| 1456 | return undefined; | ||
| 1457 | } | ||
| 1458 | |||
| 1459 | async function loadPostmanCollectionTools( | ||
| 1460 | config: OpenApiToolSourceConfig, | ||
| 1461 | collectionUid: string, | ||
| 1462 | ): Promise<ToolDefinition[]> { | ||
| 1463 | const proxyUrl = config.postmanProxyUrl ?? DEFAULT_POSTMAN_PROXY_URL; | ||
| 1464 | const payload = { | ||
| 1465 | service: "sync", | ||
| 1466 | method: "GET", | ||
| 1467 | path: `/collection/${collectionUid}?populate=true`, | ||
| 1468 | }; | ||
| 1469 | |||
| 1470 | const response = await fetch(proxyUrl, { | ||
| 1471 | method: "POST", | ||
| 1472 | headers: { | ||
| 1473 | "content-type": "application/json", | ||
| 1474 | }, | ||
| 1475 | body: JSON.stringify(payload), | ||
| 1476 | }); | ||
| 1477 | |||
| 1478 | if (!response.ok) { | ||
| 1479 | const text = await response.text().catch(() => ""); | ||
| 1480 | throw new Error(`Failed to fetch API collection ${collectionUid}: HTTP ${response.status} ${text.slice(0, 300)}`); | ||
| 1481 | } | ||
| 1482 | |||
| 1483 | const raw = await response.json() as Record<string, unknown>; | ||
| 1484 | const collection = asRecord(raw.data); | ||
| 1485 | const requests = Array.isArray(collection.requests) | ||
| 1486 | ? collection.requests.filter((entry): entry is Record<string, unknown> => Boolean(entry && typeof entry === "object")) | ||
| 1487 | : []; | ||
| 1488 | |||
| 1489 | const folders = Array.isArray(collection.folders) | ||
| 1490 | ? collection.folders.filter((entry): entry is Record<string, unknown> => Boolean(entry && typeof entry === "object")) | ||
| 1491 | : []; | ||
| 1492 | |||
| 1493 | const folderById = new Map<string, { name: string; parentId?: string }>(); | ||
| 1494 | for (const folder of folders) { | ||
| 1495 | const id = typeof folder.id === "string" ? folder.id : ""; | ||
| 1496 | if (!id) continue; | ||
| 1497 | const name = typeof folder.name === "string" && folder.name.trim().length > 0 ? folder.name : "folder"; | ||
| 1498 | const parentId = typeof folder.folder === "string" ? folder.folder : undefined; | ||
| 1499 | folderById.set(id, { name, parentId }); | ||
| 1500 | } | ||
| 1501 | |||
| 1502 | const sourceLabel = `catalog:${config.name}`; | ||
| 1503 | const authHeaders = buildStaticAuthHeaders(config.auth); | ||
| 1504 | const credentialSourceKey = getCredentialSourceKey(config); | ||
| 1505 | const credentialSpec = buildCredentialSpec(credentialSourceKey, config.auth); | ||
| 1506 | const readMethods = new Set(["get", "head", "options"]); | ||
| 1507 | const usedPaths = new Set<string>(); | ||
| 1508 | const collectionVariables = extractPostmanVariableMap(collection.variables); | ||
| 1509 | const argsType = "{ variables?: Record<string, string | number | boolean>; query?: Record<string, string | number | boolean>; headers?: Record<string, string>; body?: unknown }"; | ||
| 1510 | const returnsType = "unknown"; | ||
| 1511 | |||
| 1512 | const tools: ToolDefinition[] = []; | ||
| 1513 | |||
| 1514 | for (const request of requests) { | ||
| 1515 | const methodRaw = typeof request.method === "string" ? request.method.toLowerCase() : "get"; | ||
| 1516 | const method = methodRaw.length > 0 ? methodRaw : "get"; | ||
| 1517 | const url = typeof request.url === "string" ? request.url : ""; | ||
| 1518 | if (!url) continue; | ||
| 1519 | |||
| 1520 | const requestId = typeof request.id === "string" ? request.id : ""; | ||
| 1521 | const requestName = typeof request.name === "string" && request.name.trim().length > 0 | ||
| 1522 | ? request.name.trim() | ||
| 1523 | : requestId || `${method.toUpperCase()} request`; | ||
| 1524 | const folderId = typeof request.folder === "string" ? request.folder : undefined; | ||
| 1525 | const folderPath = resolvePostmanFolderPath(folderId, folderById); | ||
| 1526 | const requestVariables = { | ||
| 1527 | ...collectionVariables, | ||
| 1528 | ...extractPostmanVariableMap(request.pathVariableData), | ||
| 1529 | }; | ||
| 1530 | |||
| 1531 | const runSpec: PostmanSerializedRunSpec = { | ||
| 1532 | kind: "postman", | ||
| 1533 | method, | ||
| 1534 | url, | ||
| 1535 | headers: extractPostmanHeaderMap(request.headerData), | ||
| 1536 | queryParams: extractPostmanQueryEntries(request.queryParams), | ||
| 1537 | body: extractPostmanBody(request), | ||
| 1538 | variables: requestVariables, | ||
| 1539 | authHeaders, | ||
| 1540 | }; | ||
| 1541 | |||
| 1542 | const approval = config.overrides?.[requestId]?.approval | ||
| 1543 | ?? config.overrides?.[requestName]?.approval | ||
| 1544 | ?? (readMethods.has(method) | ||
| 1545 | ? config.defaultReadApproval ?? "auto" | ||
| 1546 | : config.defaultWriteApproval ?? "required"); | ||
| 1547 | |||
| 1548 | const tool: ToolDefinition & { _runSpec: SerializedTool["runSpec"] } = { | ||
| 1549 | path: buildPostmanToolPath(config.name, requestName, folderPath, usedPaths), | ||
| 1550 | source: sourceLabel, | ||
| 1551 | approval, | ||
| 1552 | description: typeof request.description === "string" && request.description.trim().length > 0 | ||
| 1553 | ? request.description | ||
| 1554 | : `${method.toUpperCase()} ${url}`, | ||
| 1555 | metadata: { | ||
| 1556 | argsType, | ||
| 1557 | returnsType, | ||
| 1558 | displayArgsType: compactArgTypeHint(argsType), | ||
| 1559 | displayReturnsType: compactReturnTypeHint(returnsType), | ||
| 1560 | argPreviewKeys: ["variables", "query", "headers", "body"], | ||
| 1561 | operationId: requestId || requestName, | ||
| 1562 | }, | ||
| 1563 | credential: credentialSpec, | ||
| 1564 | _runSpec: runSpec, | ||
| 1565 | run: async (input: unknown, context) => { | ||
| 1566 | const payloadRecord = asRecord(input); | ||
| 1567 | return await executePostmanRequest(runSpec, payloadRecord, context.credential?.headers); | ||
| 1568 | }, | ||
| 1569 | }; | ||
| 1570 | |||
| 1571 | tools.push(tool); | ||
| 1572 | } | ||
| 1573 | |||
| 1574 | return tools; | ||
| 1575 | } | ||
| 1576 | |||
| 1188 | async function loadOpenApiTools(config: OpenApiToolSourceConfig): Promise<ToolDefinition[]> { | 1577 | async function loadOpenApiTools(config: OpenApiToolSourceConfig): Promise<ToolDefinition[]> { |
| 1578 | if (typeof config.spec === "string") { | ||
| 1579 | const collectionUid = parsePostmanCollectionUid(config.spec); | ||
| 1580 | if (collectionUid) { | ||
| 1581 | return await loadPostmanCollectionTools(config, collectionUid); | ||
| 1582 | } | ||
| 1583 | } | ||
| 1584 | |||
| 1189 | const prepared = await prepareOpenApiSpec(config.spec, config.name); | 1585 | const prepared = await prepareOpenApiSpec(config.spec, config.name); |
| 1190 | return buildOpenApiToolsFromPrepared(config, prepared); | 1586 | return buildOpenApiToolsFromPrepared(config, prepared); |
| 1191 | } | 1587 | } |
| @@ -1895,6 +2291,7 @@ export interface SerializedTool { · 703 unmodified lines | |||
| 1895 | * Data needed to reconstruct `run()`. Shape depends on source type. | 2291 | * Data needed to reconstruct `run()`. Shape depends on source type. |
| 1896 | * OpenAPI: { kind: "openapi", baseUrl, method, pathTemplate, parameters, authHeaders } | 2292 | * OpenAPI: { kind: "openapi", baseUrl, method, pathTemplate, parameters, authHeaders } |
| 1897 | * MCP: { kind: "mcp", url, transport?, queryParams?, toolName } | 2293 | * MCP: { kind: "mcp", url, transport?, queryParams?, toolName } |
| 2294 | * Postman: { kind: "postman", method, url, headers, queryParams, body?, variables, authHeaders } | ||
| 1898 | * GraphQL raw: { kind: "graphql_raw", endpoint, authHeaders } | 2295 | * GraphQL raw: { kind: "graphql_raw", endpoint, authHeaders } |
| 1899 | * GraphQL field: { kind: "graphql_field", endpoint, operationName, operationType, queryTemplate, argNames?, authHeaders } | 2296 | * GraphQL field: { kind: "graphql_field", endpoint, operationName, operationType, queryTemplate, argNames?, authHeaders } |
| 1900 | * Builtin: { kind: "builtin" } — run comes from DEFAULT_TOOLS | 2297 | * Builtin: { kind: "builtin" } — run comes from DEFAULT_TOOLS |
| @@ -1915,6 +2312,7 @@ export interface SerializedTool { · 14 unmodified lines | |||
| 1915 | queryParams?: Record<string, string>; | 2312 | queryParams?: Record<string, string>; |
| 1916 | toolName: string; | 2313 | toolName: string; |
| 1917 | } | 2314 | } |
| 2315 | | PostmanSerializedRunSpec | ||
| 1918 | | { | 2316 | | { |
| 1919 | kind: "graphql_raw"; | 2317 | kind: "graphql_raw"; |
| 1920 | endpoint: string; | 2318 | endpoint: string; |
| @@ -2062,6 +2460,17 @@ export function rehydrateTools( · 141 unmodified lines | |||
| 2062 | }; | 2460 | }; |
| 2063 | } | 2461 | } |
| 2064 | 2462 | ||
| 2463 | if (st.runSpec.kind === "postman") { | ||
| 2464 | const runSpec = st.runSpec; | ||
| 2465 | return { | ||
| 2466 | ...base, | ||
| 2467 | run: async (input: unknown, context) => { | ||
| 2468 | const payload = asRecord(input); | ||
| 2469 | return await executePostmanRequest(runSpec, payload, context.credential?.headers); | ||
| 2470 | }, | ||
| 2471 | }; | ||
| 2472 | } | ||
| 2473 | |||
| 2065 | if (st.runSpec.kind === "mcp") { | 2474 | if (st.runSpec.kind === "mcp") { |
| 2066 | const { url, transport, queryParams, toolName } = st.runSpec; | 2475 | const { url, transport, queryParams, toolName } = st.runSpec; |
| 2067 | return { | 2476 | return { |
| @@ -307,6 +307,47 @@ export interface operations { · 306 unmodified lines | |||
| 307 | expect(bad.errors.length).toBeGreaterThan(0); | 307 | expect(bad.errors.length).toBeGreaterThan(0); |
| 308 | }); | 308 | }); |
| 309 | 309 | ||
| 310 | test("OpenAPI helper types infer vendor JSON response payloads", () => { | ||
| 311 | const tools: ToolDescriptor[] = [ | ||
| 312 | { | ||
| 313 | path: "github.activity.get_feeds", | ||
| 314 | description: "Get feeds", | ||
| 315 | approval: "auto", | ||
| 316 | source: "openapi:github", | ||
| 317 | operationId: "activity/get-feeds", | ||
| 318 | }, | ||
| 319 | ]; | ||
| 320 | |||
| 321 | const sourceDts = ` | ||
| 322 | export interface operations { | ||
| 323 | "activity/get-feeds": { | ||
| 324 | parameters: { query?: never; path?: never; header?: never; cookie?: never }; | ||
| 325 | responses: { | ||
| 326 | 200: { content: { "application/vnd.github+json": { current_user_url: string; timeline_url: string } } }; | ||
| 327 | }; | ||
| 328 | }; | ||
| 329 | } | ||
| 330 | `; | ||
| 331 | |||
| 332 | const declarations = generateToolDeclarations(tools, { | ||
| 333 | sourceDtsBySource: { | ||
| 334 | "openapi:github": sourceDts, | ||
| 335 | }, | ||
| 336 | }); | ||
| 337 | |||
| 338 | const ok = typecheckCode( | ||
| 339 | "const feed = await tools.github.activity.get_feeds({}); return feed.current_user_url;", | ||
| 340 | declarations, | ||
| 341 | ); | ||
| 342 | expect(ok.ok).toBe(true); | ||
| 343 | |||
| 344 | const bad = typecheckCode( | ||
| 345 | "const feed = await tools.github.activity.get_feeds({}); return feed.missing_field;", | ||
| 346 | declarations, | ||
| 347 | ); | ||
| 348 | expect(bad.ok).toBe(false); | ||
| 349 | }); | ||
| 350 | |||
| 310 | test("optional parameter can be provided", () => { | 351 | test("optional parameter can be provided", () => { |
| 311 | const result = typecheckCode( | 352 | const result = typecheckCode( |
| 312 | 'const r = await tools.admin.send_announcement({ message: "hi", channel: "general" }); return r.sent;', | 353 | 'const r = await tools.admin.send_announcement({ message: "hi", channel: "general" }); return r.sent;', |
| @@ -424,4 +465,3 @@ export interface operations { · 111 unmodified lines | |||
| 424 | }); | 465 | }); |
| 425 | }); | 466 | }); |
| 426 | 467 | ||
| 427 | |||
| @@ -13,6 +13,7 @@ · 12 unmodified lines | |||
| 13 | * without needing their own TypeScript setup. | 13 | * without needing their own TypeScript setup. |
| 14 | */ | 14 | */ |
| 15 | 15 | ||
| 16 | import { Result } from "better-result"; | ||
| 16 | import type { ToolDescriptor } from "./types"; | 17 | import type { ToolDescriptor } from "./types"; |
| 17 | 18 | ||
| 18 | // --------------------------------------------------------------------------- | 19 | // --------------------------------------------------------------------------- |
| @@ -23,11 +24,8 @@ let cachedTypeScript: typeof import("typescript") | null | undefined; · 4 unmodified lines | |||
| 23 | 24 | ||
| 24 | function getTypeScriptModule(): typeof import("typescript") | null { | 25 | function getTypeScriptModule(): typeof import("typescript") | null { |
| 25 | if (cachedTypeScript === undefined) { | 26 | if (cachedTypeScript === undefined) { |
| 26 | try { | 27 | const loaded = Result.try(() => require("typescript") as typeof import("typescript")); |
| 27 | cachedTypeScript = require("typescript"); | 28 | cachedTypeScript = loaded.isOk() ? loaded.value : null; |
| 28 | } catch { | ||
| 29 | cachedTypeScript = null; | ||
| 30 | } | ||
| 31 | } | 29 | } |
| 32 | return cachedTypeScript ?? null; | 30 | return cachedTypeScript ?? null; |
| 33 | } | 31 | } |
| @@ -39,7 +37,7 @@ function isValidTypeExpression(typeExpression: string): boolean { · 5 unmodified lines | |||
| 39 | return !/[\r\n`]/.test(typeExpression); | 37 | return !/[\r\n`]/.test(typeExpression); |
| 40 | } | 38 | } |
| 41 | 39 | ||
| 42 | try { | 40 | return Result.try(() => { |
| 43 | const sourceFile = ts.createSourceFile( | 41 | const sourceFile = ts.createSourceFile( |
| 44 | "_type_expr_check.ts", | 42 | "_type_expr_check.ts", |
| 45 | `type __T = ${typeExpression};`, | 43 | `type __T = ${typeExpression};`, |
| @@ -51,9 +49,7 @@ function isValidTypeExpression(typeExpression: string): boolean { · 5 unmodified lines | |||
| 51 | sourceFile as unknown as { parseDiagnostics?: import("typescript").Diagnostic[] } | 49 | sourceFile as unknown as { parseDiagnostics?: import("typescript").Diagnostic[] } |
| 52 | ).parseDiagnostics ?? []; | 50 | ).parseDiagnostics ?? []; |
| 53 | return diagnostics.length === 0; | 51 | return diagnostics.length === 0; |
| 54 | } catch { | 52 | }).unwrapOr(false); |
| 55 | return false; | ||
| 56 | } | ||
| 57 | } | 53 | } |
| 58 | 54 | ||
| 59 | function safeTypeExpression(raw: string | undefined, fallback: string): string { | 55 | function safeTypeExpression(raw: string | undefined, fallback: string): string { |
| @@ -63,20 +59,62 @@ function safeTypeExpression(raw: string | undefined, fallback: string): string { · 3 unmodified lines | |||
| 63 | } | 59 | } |
| 64 | 60 | ||
| 65 | const OPENAPI_HELPER_TYPES = ` | 61 | const OPENAPI_HELPER_TYPES = ` |
| 66 | type _OrEmpty<T> = [T] extends [never] ? {} : T; | 62 | type _Normalize<T> = Exclude<T, undefined | null>; |
| 63 | type _OrEmpty<T> = [_Normalize<T>] extends [never] ? {} : _Normalize<T>; | ||
| 67 | type _Simplify<T> = { [K in keyof T]: T[K] } & {}; | 64 | type _Simplify<T> = { [K in keyof T]: T[K] } & {}; |
| 65 | type _ParamsOf<Op> = | ||
| 66 | Op extends { parameters: infer P } ? P : | ||
| 67 | Op extends { parameters?: infer P } ? P : | ||
| 68 | never; | ||
| 69 | type _ParamAt<Op, K extends "query" | "path" | "header" | "cookie"> = | ||
| 70 | _ParamsOf<Op> extends { [P in K]?: infer V } ? V : never; | ||
| 71 | type _BodyOf<Op> = | ||
| 72 | Op extends { requestBody?: infer B } ? B : | ||
| 73 | Op extends { requestBody: infer B } ? B : | ||
| 74 | never; | ||
| 75 | type _BodyContent<B> = | ||
| 76 | B extends { content: infer C } | ||
| 77 | ? C extends Record<string, infer V> ? V : never | ||
| 78 | : never; | ||
| 68 | type ToolInput<Op> = _Simplify< | 79 | type ToolInput<Op> = _Simplify< |
| 69 | _OrEmpty<Op extends { parameters: { query: infer Q } } ? { [K in keyof Q]: Q[K] } : never> & | 80 | _OrEmpty<_ParamAt<Op, "query">> & |
| 70 | _OrEmpty<Op extends { parameters: { path: infer P } } ? { [K in keyof P]: P[K] } : never> & | 81 | _OrEmpty<_ParamAt<Op, "path">> & |
| 71 | _OrEmpty<Op extends { requestBody: { content: { "application/json": infer B } } } ? B : never> | 82 | _OrEmpty<_ParamAt<Op, "header">> & |
| 83 | _OrEmpty<_ParamAt<Op, "cookie">> & | ||
| 84 | _OrEmpty<_BodyContent<_BodyOf<Op>>> | ||
| 72 | >; | 85 | >; |
| 73 | type ToolOutput<Op> = | 86 | type _ResponsesOf<Op> = Op extends { responses: infer R } ? R : never; |
| 74 | Op extends { responses: { 200: { content: { "application/json": infer R } } } } ? R : | 87 | type _RespAt<Op, Code extends PropertyKey> = |
| 75 | Op extends { responses: { 201: { content: { "application/json": infer R } } } } ? R : | 88 | _ResponsesOf<Op> extends { [K in Code]?: infer R } ? R : never; |
| 76 | Op extends { responses: { 202: { content: { "application/json": infer R } } } } ? R : | 89 | type _ResponsePayload<R> = |
| 77 | Op extends { responses: { 204: unknown } } ? void : | 90 | [R] extends [never] ? never : |
| 78 | Op extends { responses: { 205: unknown } } ? void : | 91 | R extends { content: infer C } |
| 79 | unknown; | 92 | ? C extends Record<string, infer V> ? V : unknown |
| 93 | : R extends { schema: infer S } ? S : unknown; | ||
| 94 | type _HasStatus<Op, Code extends PropertyKey> = | ||
| 95 | [_RespAt<Op, Code>] extends [never] ? false : true; | ||
| 96 | type _PayloadAt<Op, Code extends PropertyKey> = | ||
| 97 | Code extends 204 | 205 | ||
| 98 | ? (_HasStatus<Op, Code> extends true ? void : never) | ||
| 99 | : _ResponsePayload<_RespAt<Op, Code>>; | ||
| 100 | type _FirstKnown<T extends readonly unknown[]> = | ||
| 101 | T extends readonly [infer H, ...infer Rest] | ||
| 102 | ? [H] extends [never] ? _FirstKnown<Rest> : H | ||
| 103 | : unknown; | ||
| 104 | type ToolOutput<Op> = _FirstKnown<[ | ||
| 105 | _PayloadAt<Op, 200>, | ||
| 106 | _PayloadAt<Op, 201>, | ||
| 107 | _PayloadAt<Op, 202>, | ||
| 108 | _PayloadAt<Op, 203>, | ||
| 109 | _PayloadAt<Op, 204>, | ||
| 110 | _PayloadAt<Op, 205>, | ||
| 111 | _PayloadAt<Op, 206>, | ||
| 112 | _PayloadAt<Op, 207>, | ||
| 113 | _PayloadAt<Op, 208>, | ||
| 114 | _PayloadAt<Op, 226>, | ||
| 115 | _PayloadAt<Op, "default">, | ||
| 116 | unknown | ||
| 117 | ]>; | ||
| 80 | `; | 118 | `; |
| 81 | 119 | ||
| 82 | function stripExportKeywordsForTypechecker(dts: string): string { | 120 | function stripExportKeywordsForTypechecker(dts: string): string { |
| @@ -233,6 +271,8 @@ export interface TypecheckResult { · 150 unmodified lines | |||
| 233 | readonly errors: readonly string[]; | 271 | readonly errors: readonly string[]; |
| 234 | } | 272 | } |
| 235 | 273 | ||
| 274 | const TYPECHECK_OK: TypecheckResult = { ok: true, errors: [] }; | ||
| 275 | |||
| 236 | let warnedMissingCompilerHostSupport = false; | 276 | let warnedMissingCompilerHostSupport = false; |
| 237 | let warnedSemanticFallback = false; | 277 | let warnedSemanticFallback = false; |
| 238 | 278 | ||
| @@ -245,7 +285,7 @@ function runSyntaxOnlyTypecheck( · 6 unmodified lines | |||
| 245 | headerLineCount: number, | 285 | headerLineCount: number, |
| 246 | ) => string, | 286 | ) => string, |
| 247 | ): TypecheckResult { | 287 | ): TypecheckResult { |
| 248 | try { | 288 | return Result.try(() => { |
| 249 | const sourceFile = ts.createSourceFile( | 289 | const sourceFile = ts.createSourceFile( |
| 250 | "generated.ts", | 290 | "generated.ts", |
| 251 | wrappedCode, | 291 | wrappedCode, |
| @@ -256,16 +296,72 @@ function runSyntaxOnlyTypecheck( · 4 unmodified lines | |||
| 256 | const diagnostics = ( | 296 | const diagnostics = ( |
| 257 | sourceFile as unknown as { parseDiagnostics?: import("typescript").Diagnostic[] } | 297 | sourceFile as unknown as { parseDiagnostics?: import("typescript").Diagnostic[] } |
| 258 | ).parseDiagnostics ?? []; | 298 | ).parseDiagnostics ?? []; |
| 259 | if (diagnostics.length === 0) { | 299 | if (diagnostics.length === 0) return TYPECHECK_OK; |
| 260 | return { ok: true, errors: [] }; | ||
| 261 | } | ||
| 262 | return { | 300 | return { |
| 263 | ok: false, | 301 | ok: false as const, |
| 264 | errors: diagnostics.map((d: import("typescript").Diagnostic) => formatError(d, headerLineCount)), | 302 | errors: diagnostics.map((d) => formatError(d, headerLineCount)), |
| 265 | }; | 303 | }; |
| 266 | } catch { | 304 | }).unwrapOr(TYPECHECK_OK); |
| 267 | return { ok: true, errors: [] }; | 305 | } |
| 268 | } | 306 | |
| 307 | function runSemanticTypecheck( | ||
| 308 | ts: typeof import("typescript"), | ||
| 309 | wrappedCode: string, | ||
| 310 | headerLineCount: number, | ||
| 311 | formatError: ( | ||
| 312 | diagnostic: import("typescript").Diagnostic, | ||
| 313 | headerLineCount: number, | ||
| 314 | ) => string, | ||
| 315 | ): Result<TypecheckResult, Error> { | ||
| 316 | return Result.try({ | ||
| 317 | try: () => { | ||
| 318 | const sourceFile = ts.createSourceFile( | ||
| 319 | "generated.ts", | ||
| 320 | wrappedCode, | ||
| 321 | ts.ScriptTarget.ESNext, | ||
| 322 | true, | ||
| 323 | ts.ScriptKind.TS, | ||
| 324 | ); | ||
| 325 | |||
| 326 | const compilerOptions: import("typescript").CompilerOptions = { | ||
| 327 | target: ts.ScriptTarget.ESNext, | ||
| 328 | module: ts.ModuleKind.ESNext, | ||
| 329 | strict: true, | ||
| 330 | noEmit: true, | ||
| 331 | lib: ["lib.es2022.d.ts"], | ||
| 332 | types: [], // prevent automatic @types/* from conflicting with sandbox declarations | ||
| 333 | }; | ||
| 334 | |||
| 335 | const host = ts.createCompilerHost(compilerOptions); | ||
| 336 | const originalGetSourceFile = host.getSourceFile.bind(host); | ||
| 337 | host.getSourceFile = (fileName, languageVersion) => { | ||
| 338 | if (fileName === "generated.ts") return sourceFile; | ||
| 339 | return originalGetSourceFile(fileName, languageVersion); | ||
| 340 | }; | ||
| 341 | |||
| 342 | const program = ts.createProgram(["generated.ts"], compilerOptions, host); | ||
| 343 | const diagnostics = program.getSemanticDiagnostics(sourceFile); | ||
| 344 | |||
| 345 | if (diagnostics.length === 0) return TYPECHECK_OK; | ||
| 346 | |||
| 347 | // Filter out errors from the .d.ts header — only report user code errors | ||
| 348 | const userErrors = diagnostics.filter((d) => { | ||
| 349 | if (d.start !== undefined && d.file) { | ||
| 350 | const { line } = d.file.getLineAndCharacterOfPosition(d.start); | ||
| 351 | return line + 1 > headerLineCount; | ||
| 352 | } | ||
| 353 | return false; | ||
| 354 | }); | ||
| 355 | |||
| 356 | if (userErrors.length === 0) return TYPECHECK_OK; | ||
| 357 | |||
| 358 | return { | ||
| 359 | ok: false as const, | ||
| 360 | errors: userErrors.map((d) => formatError(d, headerLineCount)), | ||
| 361 | }; | ||
| 362 | }, | ||
| 363 | catch: (e) => (e instanceof Error ? e : new Error(String(e))), | ||
| 364 | }); | ||
| 269 | } | 365 | } |
| 270 | 366 | ||
| 271 | /** | 367 | /** |
| @@ -278,13 +374,8 @@ export function typecheckCode( · 6 unmodified lines | |||
| 278 | code: string, | 374 | code: string, |
| 279 | toolDeclarations: string, | 375 | toolDeclarations: string, |
| 280 | ): TypecheckResult { | 376 | ): TypecheckResult { |
| 281 | let ts: typeof import("typescript"); | 377 | const ts = getTypeScriptModule(); |
| 282 | try { | 378 | if (!ts) return TYPECHECK_OK; |
| 283 | ts = require("typescript"); | ||
| 284 | } catch { | ||
| 285 | // TypeScript not available — skip typechecking | ||
| 286 | return { ok: true, errors: [] }; | ||
| 287 | } | ||
| 288 | 379 | ||
| 289 | // Wrap the code in an async function with the tools declaration. | 380 | // Wrap the code in an async function with the tools declaration. |
| 290 | // We declare sandbox globals (console, setTimeout, etc.) ourselves since | 381 | // We declare sandbox globals (console, setTimeout, etc.) ourselves since |
| @@ -301,12 +392,12 @@ export function typecheckCode( · 10 unmodified lines | |||
| 301 | 392 | ||
| 302 | const formatError = ( | 393 | const formatError = ( |
| 303 | diagnostic: import("typescript").Diagnostic, | 394 | diagnostic: import("typescript").Diagnostic, |
| 304 | headerLineCount: number, | 395 | hdrLineCount: number, |
| 305 | ): string => { | 396 | ): string => { |
| 306 | const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); | 397 | const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); |
| 307 | if (diagnostic.start !== undefined && diagnostic.file) { | 398 | if (diagnostic.start !== undefined && diagnostic.file) { |
| 308 | const { line } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); | 399 | const { line } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); |
| 309 | const adjustedLine = line + 1 - headerLineCount; | 400 | const adjustedLine = line + 1 - hdrLineCount; |
| 310 | if (adjustedLine > 0) { | 401 | if (adjustedLine > 0) { |
| 311 | return `Line ${adjustedLine}: ${message}`; | 402 | return `Line ${adjustedLine}: ${message}`; |
| 312 | } | 403 | } |
| @@ -328,66 +419,16 @@ export function typecheckCode( · 15 unmodified lines | |||
| 328 | return runSyntaxOnlyTypecheck(ts, wrappedCode, headerLineCount, formatError); | 419 | return runSyntaxOnlyTypecheck(ts, wrappedCode, headerLineCount, formatError); |
| 329 | } | 420 | } |
| 330 | 421 | ||
| 331 | try { | 422 | const semantic = runSemanticTypecheck(ts, wrappedCode, headerLineCount, formatError); |
| 332 | const sourceFile = ts.createSourceFile( | 423 | if (semantic.isOk()) return semantic.value; |
| 333 | "generated.ts", | ||
| 334 | wrappedCode, | ||
| 335 | ts.ScriptTarget.ESNext, | ||
| 336 | true, | ||
| 337 | ts.ScriptKind.TS, | ||
| 338 | ); | ||
| 339 | |||
| 340 | const compilerOptions: import("typescript").CompilerOptions = { | ||
| 341 | target: ts.ScriptTarget.ESNext, | ||
| 342 | module: ts.ModuleKind.ESNext, | ||
| 343 | strict: true, | ||
| 344 | noEmit: true, | ||
| 345 | lib: ["lib.es2022.d.ts"], | ||
| 346 | types: [], // prevent automatic @types/* (e.g. @types/node) from conflicting with our sandbox declarations | ||
| 347 | }; | ||
| 348 | |||
| 349 | const host = ts.createCompilerHost(compilerOptions); | ||
| 350 | const originalGetSourceFile = host.getSourceFile.bind(host); | ||
| 351 | host.getSourceFile = (fileName, languageVersion) => { | ||
| 352 | if (fileName === "generated.ts") return sourceFile; | ||
| 353 | return originalGetSourceFile(fileName, languageVersion); | ||
| 354 | }; | ||
| 355 | |||
| 356 | const program = ts.createProgram(["generated.ts"], compilerOptions, host); | ||
| 357 | const diagnostics = program.getSemanticDiagnostics(sourceFile); | ||
| 358 | |||
| 359 | if (diagnostics.length === 0) { | ||
| 360 | return { ok: true, errors: [] }; | ||
| 361 | } | ||
| 362 | |||
| 363 | // Filter out errors from the .d.ts header (circular refs, etc.) — only report user code errors | ||
| 364 | const userErrors = diagnostics.filter((d) => { | ||
| 365 | if (d.start !== undefined && d.file) { | ||
| 366 | const { line } = d.file.getLineAndCharacterOfPosition(d.start); | ||
| 367 | return line + 1 > headerLineCount; | ||
| 368 | } | ||
| 369 | return false; | ||
| 370 | }); | ||
| 371 | 424 | ||
| 372 | if (userErrors.length === 0) { | 425 | // Semantic typechecking failed to initialize — fall back to syntax-only. |
| 373 | return { ok: true, errors: [] }; | 426 | if (!warnedSemanticFallback) { |
| 374 | } | 427 | warnedSemanticFallback = true; |
| 375 | 428 | console.warn( | |
| 376 | return { | 429 | `[executor] TypeScript semantic typecheck unavailable, falling back to syntax-only checks: ${semantic.error.message}`, |
| 377 | ok: false, | 430 | ); |
| 378 | errors: userErrors.map((d) => formatError(d, headerLineCount)), | ||
| 379 | }; | ||
| 380 | } catch (error) { | ||
| 381 | // Some runtimes (e.g. Convex action isolates) can lack the full Node-backed | ||
| 382 | // TypeScript host environment. If semantic typechecking cannot initialize, | ||
| 383 | // fall back to syntax-only parsing instead of failing the MCP call. | ||
| 384 | if (!warnedSemanticFallback) { | ||
| 385 | warnedSemanticFallback = true; | ||
| 386 | console.warn( | ||
| 387 | `[executor] TypeScript semantic typecheck unavailable, falling back to syntax-only checks: ${error instanceof Error ? error.message : String(error)}`, | ||
| 388 | ); | ||
| 389 | } | ||
| 390 | |||
| 391 | return runSyntaxOnlyTypecheck(ts, wrappedCode, headerLineCount, formatError); | ||
| 392 | } | 431 | } |
| 432 | |||
| 433 | return runSyntaxOnlyTypecheck(ts, wrappedCode, headerLineCount, formatError); | ||
| 393 | } | 434 | } |
| @@ -4,6 +4,7 @@ import type { Id } from "../convex/_generated/dataModel"; · 3 unmodified lines | |||
| 4 | 4 | ||
| 5 | export type TaskStatus = "queued" | "running" | "completed" | "failed" | "timed_out" | "denied"; | 5 | export type TaskStatus = "queued" | "running" | "completed" | "failed" | "timed_out" | "denied"; |
| 6 | export type ApprovalStatus = "pending" | "approved" | "denied"; | 6 | export type ApprovalStatus = "pending" | "approved" | "denied"; |
| 7 | export type ToolCallStatus = "requested" | "pending_approval" | "completed" | "failed" | "denied"; | ||
| 7 | export type PolicyDecision = "allow" | "require_approval" | "deny"; | 8 | export type PolicyDecision = "allow" | "require_approval" | "deny"; |
| 8 | export type CredentialScope = "workspace" | "actor"; | 9 | export type CredentialScope = "workspace" | "actor"; |
| 9 | export type CredentialProvider = "managed" | "workos-vault"; | 10 | export type CredentialProvider = "managed" | "workos-vault"; |
| @@ -55,6 +56,21 @@ export interface TaskEventRecord { · 45 unmodified lines | |||
| 55 | createdAt: number; | 56 | createdAt: number; |
| 56 | } | 57 | } |
| 57 | 58 | ||
| 59 | export interface ToolCallRecord { | ||
| 60 | taskId: string; | ||
| 61 | callId: string; | ||
| 62 | workspaceId: Id<"workspaces">; | ||
| 63 | toolPath: string; | ||
| 64 | input: unknown; | ||
| 65 | status: ToolCallStatus; | ||
| 66 | approvalId?: string; | ||
| 67 | output?: unknown; | ||
| 68 | error?: string; | ||
| 69 | createdAt: number; | ||
| 70 | updatedAt: number; | ||
| 71 | completedAt?: number; | ||
| 72 | } | ||
| 73 | |||
| 58 | export interface AccessPolicyRecord { | 74 | export interface AccessPolicyRecord { |
| 59 | id: string; | 75 | id: string; |
| 60 | workspaceId: Id<"workspaces">; | 76 | workspaceId: Id<"workspaces">; |
| @@ -167,7 +183,15 @@ export interface ToolCallRequest { · 106 unmodified lines | |||
| 167 | 183 | ||
| 168 | export type ToolCallResult = | 184 | export type ToolCallResult = |
| 169 | | { ok: true; value: unknown } | 185 | | { ok: true; value: unknown } |
| 170 | | { ok: false; error: string; denied?: boolean }; | 186 | | { |
| 187 | ok: false; | ||
| 188 | kind: "pending"; | ||
| 189 | approvalId: string; | ||
| 190 | retryAfterMs?: number; | ||
| 191 | error?: string; | ||
| 192 | } | ||
| 193 | | { ok: false; kind: "denied"; error: string } | ||
| 194 | | { ok: false; kind: "failed"; error: string }; | ||
| 171 | 195 | ||
| 172 | export type RuntimeOutputStream = "stdout" | "stderr"; | 196 | export type RuntimeOutputStream = "stdout" | "stderr"; |
| 173 | 197 | ||
| @@ -14,6 +14,7 @@ · 13 unmodified lines | |||
| 14 | "build:release": "bun run scripts/build-release.ts" | 14 | "build:release": "bun run scripts/build-release.ts" |
| 15 | }, | 15 | }, |
| 16 | "dependencies": { | 16 | "dependencies": { |
| 17 | "better-result": "^2.7.0", | ||
| 17 | "convex": "^1.31.7", | 18 | "convex": "^1.31.7", |
| 18 | "graphql": "^16.12.0" | 19 | "graphql": "^16.12.0" |
| 19 | } | 20 | } |
| @@ -0,0 +1,2 @@ | |||
| 1 | # Shared-secret bearer token — must match CLOUDFLARE_SANDBOX_AUTH_TOKEN on the executor side. | ||
| 2 | AUTH_TOKEN=dev-sandbox-token | ||
| @@ -0,0 +1,20 @@ | |||
| 1 | { | ||
| 2 | "name": "@executor/sandbox-host", | ||
| 3 | "version": "0.1.0", | ||
| 4 | "private": true, | ||
| 5 | "type": "module", | ||
| 6 | "scripts": { | ||
| 7 | "dev": "wrangler dev --port 8787", | ||
| 8 | "deploy": "wrangler deploy", | ||
| 9 | "typecheck": "tsc --noEmit" | ||
| 10 | }, | ||
| 11 | "devDependencies": { | ||
| 12 | "@cloudflare/workers-types": "^4.20250214.0", | ||
| 13 | "typescript": "^5.9.3", | ||
| 14 | "wrangler": "^4.0.0" | ||
| 15 | }, | ||
| 16 | "dependencies": { | ||
| 17 | "better-result": "^2.7.0", | ||
| 18 | "convex": "^1.31.7" | ||
| 19 | } | ||
| 20 | } | ||
| @@ -0,0 +1,405 @@ | |||
| 1 | /** | ||
| 2 | * Executor Sandbox Host Worker | ||
| 3 | * | ||
| 4 | * This Cloudflare Worker uses the Dynamic Worker Loader API to run | ||
| 5 | * agent-generated code in sandboxed isolates. It exposes a single HTTP | ||
| 6 | * endpoint (`POST /v1/runs`) that the executor's Convex action calls. | ||
| 7 | * | ||
| 8 | * ## How it works | ||
| 9 | * | ||
| 10 | * 1. Receives a run request with `{ taskId, code, timeoutMs, callback }`. | ||
| 11 | * | ||
| 12 | * 2. Uses `env.LOADER.get(id, () => WorkerCode)` to spawn a dynamic isolate | ||
| 13 | * containing the user's code. | ||
| 14 | * | ||
| 15 | * 3. The isolate's network access is fully blocked (`globalOutbound: null`). | ||
| 16 | * Instead, tool calls are routed through a `ToolBridge` entrypoint class | ||
| 17 | * (passed as a loopback service binding via `ctx.exports`) which invokes | ||
| 18 | * Convex callback RPC functions to resolve them. | ||
| 19 | * | ||
| 20 | * 4. Console output is buffered in the harness and returned in the response. | ||
| 21 | * Output lines are also streamed back to Convex in real-time via the | ||
| 22 | * ToolBridge binding. | ||
| 23 | * | ||
| 24 | * 5. `/v1/runs` returns an accepted dispatch response immediately. Terminal | ||
| 25 | * result status is reported back to Convex through callback RPC. | ||
| 26 | * | ||
| 27 | * ## Code isolation | ||
| 28 | * | ||
| 29 | * User code is placed in a **separate JS module** (`user-code.js`) that | ||
| 30 | * exports a single `run(tools, console)` async function. The harness module | ||
| 31 | * (`harness.js`) imports and calls this function, passing controlled `tools` | ||
| 32 | * and `console` proxies. Because the user code is in a different module, it | ||
| 33 | * cannot access the harness's `fetch` handler scope, `req`, `env`, `ctx`, | ||
| 34 | * or `Response` — preventing IIFE escape attacks and response forgery. | ||
| 35 | */ | ||
| 36 | |||
| 37 | import { Result } from "better-result"; | ||
| 38 | import { WorkerEntrypoint } from "cloudflare:workers"; | ||
| 39 | import { ConvexHttpClient } from "convex/browser"; | ||
| 40 | |||
| 41 | const RUNTIME_CALLBACKS = { | ||
| 42 | handleToolCall: "runtimeCallbacks:handleToolCall", | ||
| 43 | appendOutput: "runtimeCallbacks:appendOutput", | ||
| 44 | completeRun: "runtimeCallbacks:completeRun", | ||
| 45 | } as const; | ||
| 46 | |||
| 47 | // Import isolate modules as raw text — these are loaded as JS modules inside | ||
| 48 | // the dynamic isolate, NOT executed in the host worker. The *.isolate.js | ||
| 49 | // extension is mapped to Text type in wrangler.jsonc rules, so wrangler | ||
| 50 | // bundles them as string constants instead of trying to execute them. | ||
| 51 | // @ts-expect-error — wrangler Text module import (no TS declarations) | ||
| 52 | import GLOBALS_MODULE from "./isolate/globals.isolate.js"; | ||
| 53 | // @ts-expect-error — wrangler Text module import (no TS declarations) | ||
| 54 | import HARNESS_CODE from "./isolate/harness.isolate.js"; | ||
| 55 | |||
| 56 | // ── Types ──────────────────────────────────────────────────────────────────── | ||
| 57 | |||
| 58 | interface Env { | ||
| 59 | LOADER: WorkerLoader; | ||
| 60 | AUTH_TOKEN: string; | ||
| 61 | } | ||
| 62 | |||
| 63 | /** Dynamic Worker Loader binding — provided by the `worker_loaders` config. */ | ||
| 64 | interface WorkerLoader { | ||
| 65 | get(id: string, getCode: () => Promise<WorkerCode>): WorkerStub; | ||
| 66 | } | ||
| 67 | |||
| 68 | interface WorkerCode { | ||
| 69 | compatibilityDate: string; | ||
| 70 | compatibilityFlags?: string[]; | ||
| 71 | mainModule: string; | ||
| 72 | modules: Record<string, string | { js: string } | { text: string } | { json: object }>; | ||
| 73 | env?: Record<string, unknown>; | ||
| 74 | globalOutbound?: unknown | null; | ||
| 75 | } | ||
| 76 | |||
| 77 | interface WorkerStub { | ||
| 78 | getEntrypoint(name?: string, options?: { props?: unknown }): EntrypointStub; | ||
| 79 | } | ||
| 80 | |||
| 81 | interface EntrypointStub { | ||
| 82 | fetch(input: string | Request, init?: RequestInit): Promise<Response>; | ||
| 83 | } | ||
| 84 | |||
| 85 | interface RunRequest { | ||
| 86 | taskId: string; | ||
| 87 | code: string; | ||
| 88 | timeoutMs: number; | ||
| 89 | callback: { | ||
| 90 | convexUrl: string; | ||
| 91 | internalSecret: string; | ||
| 92 | }; | ||
| 93 | } | ||
| 94 | |||
| 95 | interface RunResult { | ||
| 96 | status: "completed" | "failed" | "timed_out" | "denied"; | ||
| 97 | stdout: string; | ||
| 98 | stderr: string; | ||
| 99 | error?: string; | ||
| 100 | exitCode?: number; | ||
| 101 | } | ||
| 102 | |||
| 103 | interface RunDispatchResponse { | ||
| 104 | accepted: true; | ||
| 105 | dispatchId: string; | ||
| 106 | } | ||
| 107 | |||
| 108 | interface ToolCallResult { | ||
| 109 | ok: true | false; | ||
| 110 | value?: unknown; | ||
| 111 | error?: string; | ||
| 112 | kind?: "pending" | "denied" | "failed"; | ||
| 113 | approvalId?: string; | ||
| 114 | retryAfterMs?: number; | ||
| 115 | } | ||
| 116 | |||
| 117 | interface BridgeProps { | ||
| 118 | callbackConvexUrl: string; | ||
| 119 | callbackInternalSecret: string; | ||
| 120 | taskId: string; | ||
| 121 | } | ||
| 122 | |||
| 123 | // ── Auth ────────────────────────────────────────────────────────────────────── | ||
| 124 | |||
| 125 | /** Constant-time string comparison to prevent timing side-channels. */ | ||
| 126 | function timingSafeEqual(a: string, b: string): boolean { | ||
| 127 | const encoder = new TextEncoder(); | ||
| 128 | const bufA = encoder.encode(a); | ||
| 129 | const bufB = encoder.encode(b); | ||
| 130 | |||
| 131 | if (bufA.length !== bufB.length) { | ||
| 132 | // Compare against self to keep timing consistent, then return false. | ||
| 133 | let result = 0; | ||
| 134 | for (let i = 0; i < bufA.length; i++) { | ||
| 135 | result |= (bufA[i] ?? 0) ^ (bufA[i] ?? 0); | ||
| 136 | } | ||
| 137 | return false; | ||
| 138 | } | ||
| 139 | |||
| 140 | let result = 0; | ||
| 141 | for (let i = 0; i < bufA.length; i++) { | ||
| 142 | result |= (bufA[i] ?? 0) ^ (bufB[i] ?? 0); | ||
| 143 | } | ||
| 144 | return result === 0; | ||
| 145 | } | ||
| 146 | |||
| 147 | // ── Helpers ─────────────────────────────────────────────────────────────────── | ||
| 148 | |||
| 149 | const failedResult = (error: string): RunResult => ({ | ||
| 150 | status: "failed", | ||
| 151 | stdout: "", | ||
| 152 | stderr: "", | ||
| 153 | error, | ||
| 154 | }); | ||
| 155 | |||
| 156 | async function sleep(ms: number): Promise<void> { | ||
| 157 | await new Promise((resolve) => setTimeout(resolve, ms)); | ||
| 158 | } | ||
| 159 | |||
| 160 | // ── Tool Bridge Entrypoint ─────────────────────────────────────────────────── | ||
| 161 | // | ||
| 162 | // This class is exposed as a named entrypoint on the host Worker. A loopback | ||
| 163 | // service binding (via `ctx.exports.ToolBridge({props: ...})`) is passed into | ||
| 164 | // the dynamic isolate's `env`. When the isolate calls | ||
| 165 | // `env.TOOL_BRIDGE.callTool(...)`, the RPC call lands here. | ||
| 166 | // | ||
| 167 | // `this.ctx.props` carries the callback URL and auth token for the specific task. | ||
| 168 | |||
| 169 | export class ToolBridge extends WorkerEntrypoint<Env> { | ||
| 170 | private get props(): BridgeProps { | ||
| 171 | return (this.ctx as unknown as { props: BridgeProps }).props; | ||
| 172 | } | ||
| 173 | |||
| 174 | private createConvexClient(): ConvexHttpClient { | ||
| 175 | return new ConvexHttpClient(this.props.callbackConvexUrl, { | ||
| 176 | skipConvexDeploymentUrlCheck: true, | ||
| 177 | }); | ||
| 178 | } | ||
| 179 | |||
| 180 | /** Forward a tool call to the Convex callback RPC action. */ | ||
| 181 | async callTool(toolPath: string, input: unknown, callId?: string): Promise<ToolCallResult> { | ||
| 182 | const { callbackInternalSecret, taskId } = this.props; | ||
| 183 | const effectiveCallId = callId && callId.trim().length > 0 | ||
| 184 | ? callId | ||
| 185 | : `call_${crypto.randomUUID()}`; | ||
| 186 | |||
| 187 | const response = await Result.tryPromise(async () => { | ||
| 188 | const convex = this.createConvexClient(); | ||
| 189 | return await convex.action(RUNTIME_CALLBACKS.handleToolCall as any, { | ||
| 190 | internalSecret: callbackInternalSecret, | ||
| 191 | runId: taskId, | ||
| 192 | callId: effectiveCallId, | ||
| 193 | toolPath, | ||
| 194 | input, | ||
| 195 | }); | ||
| 196 | }); | ||
| 197 | |||
| 198 | if (response.isErr()) { | ||
| 199 | const cause = response.error.cause; | ||
| 200 | const message = cause instanceof Error ? cause.message : String(cause); | ||
| 201 | return { ok: false, kind: "failed", error: `Tool callback failed: ${message}` }; | ||
| 202 | } | ||
| 203 | |||
| 204 | return response.value as ToolCallResult; | ||
| 205 | } | ||
| 206 | |||
| 207 | /** Stream a console output line back to Convex (best-effort). */ | ||
| 208 | async emitOutput(stream: "stdout" | "stderr", line: string): Promise<void> { | ||
| 209 | const { callbackInternalSecret, taskId } = this.props; | ||
| 210 | await Result.tryPromise(async () => { | ||
| 211 | const convex = this.createConvexClient(); | ||
| 212 | await convex.mutation(RUNTIME_CALLBACKS.appendOutput as any, { | ||
| 213 | internalSecret: callbackInternalSecret, | ||
| 214 | runId: taskId, | ||
| 215 | stream, | ||
| 216 | line, | ||
| 217 | timestamp: Date.now(), | ||
| 218 | }); | ||
| 219 | }); | ||
| 220 | } | ||
| 221 | } | ||
| 222 | |||
| 223 | // ── Sandbox Harness ────────────────────────────────────────────────────────── | ||
| 224 | // | ||
| 225 | // The harness is a static ES module loaded as the main module of the dynamic | ||
| 226 | // isolate. User code lives in a **separate** module (`user-code.js`) and is | ||
| 227 | // imported by the harness. This prevents user code from accessing or | ||
| 228 | // manipulating the harness's fetch handler, `req`, `env`, `ctx`, or `Response`. | ||
| 229 | // | ||
| 230 | // Both HARNESS_CODE and GLOBALS_MODULE are imported as raw text from | ||
| 231 | // `./isolate/harness.js` and `./isolate/globals.js` respectively, so they | ||
| 232 | // can be authored as real JS files with proper syntax highlighting and linting. | ||
| 233 | |||
| 234 | /** | ||
| 235 | * Build the user code module. The code is wrapped in an exported async | ||
| 236 | * function `run(tools, console)` so the harness can call it with controlled | ||
| 237 | * scope bindings. The user code runs in a separate module from the harness | ||
| 238 | * and cannot access `req`, `env`, `ctx`, or `Response`. | ||
| 239 | */ | ||
| 240 | function buildUserModule(userCode: string): string { | ||
| 241 | return `export async function run(tools, console) {\n"use strict";\n${userCode}\n}\n`; | ||
| 242 | } | ||
| 243 | |||
| 244 | async function executeSandboxRun(request: RunRequest, ctx: ExecutionContext, env: Env): Promise<RunResult> { | ||
| 245 | const timeoutMs = request.timeoutMs ?? 300_000; | ||
| 246 | const isolateId = request.taskId; | ||
| 247 | |||
| 248 | const ctxExports = (ctx as unknown as { | ||
| 249 | exports: Record<string, (opts: { props: BridgeProps }) => unknown>; | ||
| 250 | }).exports; | ||
| 251 | |||
| 252 | const toolBridgeBinding = ctxExports.ToolBridge({ | ||
| 253 | props: { | ||
| 254 | callbackConvexUrl: request.callback.convexUrl, | ||
| 255 | callbackInternalSecret: request.callback.internalSecret, | ||
| 256 | taskId: request.taskId, | ||
| 257 | }, | ||
| 258 | }); | ||
| 259 | |||
| 260 | const worker = env.LOADER.get(isolateId, async () => ({ | ||
| 261 | compatibilityDate: "2025-06-01", | ||
| 262 | mainModule: "harness.js", | ||
| 263 | modules: { | ||
| 264 | "harness.js": HARNESS_CODE, | ||
| 265 | "globals.js": GLOBALS_MODULE, | ||
| 266 | "user-code.js": buildUserModule(request.code), | ||
| 267 | }, | ||
| 268 | env: { | ||
| 269 | TOOL_BRIDGE: toolBridgeBinding, | ||
| 270 | }, | ||
| 271 | globalOutbound: null, | ||
| 272 | })); | ||
| 273 | |||
| 274 | const entrypoint = worker.getEntrypoint(); | ||
| 275 | |||
| 276 | const controller = new AbortController(); | ||
| 277 | const timer = setTimeout(() => controller.abort(), timeoutMs); | ||
| 278 | |||
| 279 | const response = await Result.tryPromise(() => | ||
| 280 | entrypoint.fetch("http://sandbox.internal/run", { | ||
| 281 | method: "POST", | ||
| 282 | signal: controller.signal, | ||
| 283 | }), | ||
| 284 | ); | ||
| 285 | |||
| 286 | clearTimeout(timer); | ||
| 287 | |||
| 288 | if (response.isErr()) { | ||
| 289 | const cause = response.error.cause; | ||
| 290 | if (cause instanceof DOMException && cause.name === "AbortError") { | ||
| 291 | return { | ||
| 292 | status: "timed_out", | ||
| 293 | stdout: "", | ||
| 294 | stderr: "", | ||
| 295 | error: `Execution timed out after ${timeoutMs}ms`, | ||
| 296 | }; | ||
| 297 | } | ||
| 298 | throw cause; | ||
| 299 | } | ||
| 300 | |||
| 301 | const body = await Result.tryPromise(() => response.value.json() as Promise<RunResult>); | ||
| 302 | if (body.isErr()) { | ||
| 303 | return failedResult("Sandbox isolate returned invalid JSON"); | ||
| 304 | } | ||
| 305 | return body.value; | ||
| 306 | } | ||
| 307 | |||
| 308 | async function reportRunCompletion(request: RunRequest, result: RunResult, durationMs: number): Promise<void> { | ||
| 309 | const convex = new ConvexHttpClient(request.callback.convexUrl, { | ||
| 310 | skipConvexDeploymentUrlCheck: true, | ||
| 311 | }); | ||
| 312 | let lastError: unknown = null; | ||
| 313 | |||
| 314 | for (let attempt = 1; attempt <= 3; attempt += 1) { | ||
| 315 | const response = await Result.tryPromise(async () => { | ||
| 316 | return await convex.mutation(RUNTIME_CALLBACKS.completeRun as any, { | ||
| 317 | internalSecret: request.callback.internalSecret, | ||
| 318 | runId: request.taskId, | ||
| 319 | status: result.status, | ||
| 320 | stdout: result.stdout, | ||
| 321 | stderr: result.stderr, | ||
| 322 | exitCode: result.exitCode, | ||
| 323 | error: result.error, | ||
| 324 | durationMs, | ||
| 325 | }); | ||
| 326 | }); | ||
| 327 | |||
| 328 | if (response.isOk()) { | ||
| 329 | return; | ||
| 330 | } | ||
| 331 | |||
| 332 | lastError = response.error.cause; | ||
| 333 | if (attempt < 3) { | ||
| 334 | await sleep(200 * attempt); | ||
| 335 | } | ||
| 336 | } | ||
| 337 | |||
| 338 | console.error("Failed to report run completion", { | ||
| 339 | taskId: request.taskId, | ||
| 340 | error: lastError instanceof Error ? lastError.message : String(lastError), | ||
| 341 | }); | ||
| 342 | } | ||
| 343 | |||
| 344 | // ── Main Handler ───────────────────────────────────────────────────────────── | ||
| 345 | |||
| 346 | export default { | ||
| 347 | async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { | ||
| 348 | const url = new URL(request.url); | ||
| 349 | |||
| 350 | if (request.method === "GET" && url.pathname === "/health") { | ||
| 351 | return Response.json({ ok: true }); | ||
| 352 | } | ||
| 353 | |||
| 354 | if (request.method !== "POST" || url.pathname !== "/v1/runs") { | ||
| 355 | return Response.json({ error: "Not found" }, { status: 404 }); | ||
| 356 | } | ||
| 357 | |||
| 358 | // ── Auth ────────────────────────────────────────────────────────────── | ||
| 359 | const authHeader = request.headers.get("authorization"); | ||
| 360 | if (!authHeader?.startsWith("Bearer ")) { | ||
| 361 | return Response.json({ error: "Unauthorized" }, { status: 401 }); | ||
| 362 | } | ||
| 363 | const token = authHeader.slice("Bearer ".length); | ||
| 364 | if (!timingSafeEqual(token, env.AUTH_TOKEN)) { | ||
| 365 | return Response.json({ error: "Unauthorized" }, { status: 401 }); | ||
| 366 | } | ||
| 367 | |||
| 368 | // ── Parse body ──────────────────────────────────────────────────────── | ||
| 369 | const parsed = await Result.tryPromise(() => request.json() as Promise<RunRequest>); | ||
| 370 | if (parsed.isErr()) { | ||
| 371 | return Response.json({ error: "Invalid JSON body" }, { status: 400 }); | ||
| 372 | } | ||
| 373 | const body = parsed.value; | ||
| 374 | |||
| 375 | if (!body.taskId || !body.code || !body.callback?.convexUrl || !body.callback?.internalSecret) { | ||
| 376 | return Response.json( | ||
| 377 | { error: "Missing required fields: taskId, code, callback.convexUrl, callback.internalSecret" }, | ||
| 378 | { status: 400 }, | ||
| 379 | ); | ||
| 380 | } | ||
| 381 | |||
| 382 | const startedAt = Date.now(); | ||
| 383 | const dispatchId = `dispatch_${body.taskId}_${startedAt}`; | ||
| 384 | |||
| 385 | ctx.waitUntil((async () => { | ||
| 386 | const runResult = await Result.tryPromise(() => executeSandboxRun(body, ctx, env)); | ||
| 387 | const finalResult = runResult.isOk() | ||
| 388 | ? runResult.value | ||
| 389 | : failedResult( | ||
| 390 | `Sandbox host error: ${runResult.error.cause instanceof Error | ||
| 391 | ? runResult.error.cause.message | ||
| 392 | : String(runResult.error.cause)}`, | ||
| 393 | ); | ||
| 394 | |||
| 395 | await reportRunCompletion(body, finalResult, Date.now() - startedAt); | ||
| 396 | })()); | ||
| 397 | |||
| 398 | const response: RunDispatchResponse = { | ||
| 399 | accepted: true, | ||
| 400 | dispatchId, | ||
| 401 | }; | ||
| 402 | |||
| 403 | return Response.json(response, { status: 202 }); | ||
| 404 | }, | ||
| 405 | }; | ||
| @@ -0,0 +1,2 @@ | |||
| 1 | // Captured before user code module is evaluated (this module is imported first). | ||
| 2 | export const ResponseJson = Response.json.bind(Response); | ||
| @@ -0,0 +1,108 @@ | |||
| 1 | import { ResponseJson as _ResponseJson } from "./globals.js"; | ||
| 2 | import { run } from "./user-code.js"; | ||
| 3 | |||
| 4 | const APPROVAL_DENIED_PREFIX = "APPROVAL_DENIED:"; | ||
| 5 | |||
| 6 | function formatArgs(args) { | ||
| 7 | return args | ||
| 8 | .map((v) => { | ||
| 9 | if (typeof v === "string") return v; | ||
| 10 | try { | ||
| 11 | return JSON.stringify(v); | ||
| 12 | } catch { | ||
| 13 | return String(v); | ||
| 14 | } | ||
| 15 | }) | ||
| 16 | .join(" "); | ||
| 17 | } | ||
| 18 | |||
| 19 | async function sleep(ms) { | ||
| 20 | await new Promise((resolve) => setTimeout(resolve, ms)); | ||
| 21 | } | ||
| 22 | |||
| 23 | function createToolsProxy(bridge, path = []) { | ||
| 24 | const callable = () => {}; | ||
| 25 | return new Proxy(callable, { | ||
| 26 | get(_target, prop) { | ||
| 27 | if (prop === "then") return undefined; | ||
| 28 | if (typeof prop !== "string") return undefined; | ||
| 29 | return createToolsProxy(bridge, [...path, prop]); | ||
| 30 | }, | ||
| 31 | async apply(_target, _thisArg, args) { | ||
| 32 | const toolPath = path.join("."); | ||
| 33 | if (!toolPath) throw new Error("Tool path missing"); | ||
| 34 | const input = args.length > 0 ? args[0] : {}; | ||
| 35 | const callId = "call_" + crypto.randomUUID(); | ||
| 36 | |||
| 37 | while (true) { | ||
| 38 | const result = await bridge.callTool(toolPath, input, callId); | ||
| 39 | if (result.ok) return result.value; | ||
| 40 | if (result.kind === "pending") { | ||
| 41 | await sleep(Math.max(50, result.retryAfterMs ?? 500)); | ||
| 42 | continue; | ||
| 43 | } | ||
| 44 | if (result.kind === "denied") throw new Error(APPROVAL_DENIED_PREFIX + result.error); | ||
| 45 | throw new Error(result.error); | ||
| 46 | } | ||
| 47 | }, | ||
| 48 | }); | ||
| 49 | } | ||
| 50 | |||
| 51 | export default { | ||
| 52 | async fetch(req, env, ctx) { | ||
| 53 | const stdoutLines = []; | ||
| 54 | const stderrLines = []; | ||
| 55 | |||
| 56 | const appendStdout = (line) => { | ||
| 57 | stdoutLines.push(line); | ||
| 58 | ctx.waitUntil(env.TOOL_BRIDGE.emitOutput("stdout", line)); | ||
| 59 | }; | ||
| 60 | const appendStderr = (line) => { | ||
| 61 | stderrLines.push(line); | ||
| 62 | ctx.waitUntil(env.TOOL_BRIDGE.emitOutput("stderr", line)); | ||
| 63 | }; | ||
| 64 | |||
| 65 | const tools = createToolsProxy(env.TOOL_BRIDGE); | ||
| 66 | const console = { | ||
| 67 | log: (...args) => appendStdout(formatArgs(args)), | ||
| 68 | info: (...args) => appendStdout(formatArgs(args)), | ||
| 69 | warn: (...args) => appendStderr(formatArgs(args)), | ||
| 70 | error: (...args) => appendStderr(formatArgs(args)), | ||
| 71 | }; | ||
| 72 | |||
| 73 | try { | ||
| 74 | const value = await run(tools, console); | ||
| 75 | |||
| 76 | if (value !== undefined) { | ||
| 77 | appendStdout("result: " + formatArgs([value])); | ||
| 78 | } | ||
| 79 | |||
| 80 | return _ResponseJson({ | ||
| 81 | status: "completed", | ||
| 82 | stdout: stdoutLines.join("\n"), | ||
| 83 | stderr: stderrLines.join("\n"), | ||
| 84 | exitCode: 0, | ||
| 85 | }); | ||
| 86 | } catch (error) { | ||
| 87 | const message = | ||
| 88 | error instanceof Error ? error.message : String(error); | ||
| 89 | if (message.startsWith(APPROVAL_DENIED_PREFIX)) { | ||
| 90 | const denied = message.replace(APPROVAL_DENIED_PREFIX, "").trim(); | ||
| 91 | appendStderr(denied); | ||
| 92 | return _ResponseJson({ | ||
| 93 | status: "denied", | ||
| 94 | stdout: stdoutLines.join("\n"), | ||
| 95 | stderr: stderrLines.join("\n"), | ||
| 96 | error: denied, | ||
| 97 | }); | ||
| 98 | } | ||
| 99 | appendStderr(message); | ||
| 100 | return _ResponseJson({ | ||
| 101 | status: "failed", | ||
| 102 | stdout: stdoutLines.join("\n"), | ||
| 103 | stderr: stderrLines.join("\n"), | ||
| 104 | error: message, | ||
| 105 | }); | ||
| 106 | } | ||
| 107 | }, | ||
| 108 | }; | ||
| @@ -0,0 +1,17 @@ | |||
| 1 | { | ||
| 2 | "compilerOptions": { | ||
| 3 | "target": "ESNext", | ||
| 4 | "module": "ESNext", | ||
| 5 | "moduleResolution": "bundler", | ||
| 6 | "lib": ["ESNext"], | ||
| 7 | "types": ["@cloudflare/workers-types"], | ||
| 8 | "strict": true, | ||
| 9 | "noEmit": true, | ||
| 10 | "skipLibCheck": true, | ||
| 11 | "forceConsistentCasingInFileNames": true, | ||
| 12 | "resolveJsonModule": true, | ||
| 13 | "isolatedModules": true, | ||
| 14 | "jsx": "react-jsx" | ||
| 15 | }, | ||
| 16 | "include": ["src/**/*.ts"] | ||
| 17 | } | ||
| @@ -0,0 +1,28 @@ | |||
| 1 | { | ||
| 2 | "name": "executor-sandbox-host", | ||
| 3 | "main": "src/index.ts", | ||
| 4 | "compatibility_date": "2025-06-01", | ||
| 5 | "compatibility_flags": ["enable_ctx_exports"], | ||
| 6 | |||
| 7 | // Treat *.isolate.js files as raw text strings so they can be passed to the | ||
| 8 | // Dynamic Worker Loader API as module source code. This lets us author the | ||
| 9 | // isolate harness and globals as real JS files (with syntax highlighting) | ||
| 10 | // while importing them as text in the host worker. | ||
| 11 | "rules": [ | ||
| 12 | { "type": "Text", "globs": ["**/*.isolate.js"], "fallthrough": true } | ||
| 13 | ], | ||
| 14 | |||
| 15 | // The Worker Loader binding — this is the core of the Dynamic Worker Loader API. | ||
| 16 | // It provides `env.LOADER.get(id, callback)` to spawn sandboxed isolates. | ||
| 17 | "worker_loaders": [ | ||
| 18 | { | ||
| 19 | "binding": "LOADER" | ||
| 20 | } | ||
| 21 | ] | ||
| 22 | |||
| 23 | // Environment secrets (set via `wrangler secret put AUTH_TOKEN`): | ||
| 24 | // AUTH_TOKEN: shared-secret bearer token that the Convex executor uses | ||
| 25 | // to authenticate run requests. | ||
| 26 | // | ||
| 27 | // See .dev.vars.example for local development values. | ||
| 28 | } | ||
| @@ -6,6 +6,7 @@ · 5 unmodified lines | |||
| 6 | "executor/convex", | 6 | "executor/convex", |
| 7 | "executor/apps/*", | 7 | "executor/apps/*", |
| 8 | "executor/packages/*", | 8 | "executor/packages/*", |
| 9 | "sources", | ||
| 9 | "assistant", | 10 | "assistant", |
| 10 | "assistant/packages/*", | 11 | "assistant/packages/*", |
| 11 | "assistant/packages/reacord" | 12 | "assistant/packages/reacord" |
| @@ -14,6 +15,8 @@ · 2 unmodified lines | |||
| 14 | "with-env": "bun run --env-file=.env --", | 15 | "with-env": "bun run --env-file=.env --", |
| 15 | "dev": "bun run dev.ts", | 16 | "dev": "bun run dev.ts", |
| 16 | "kill:all": "bun run kill-all.ts", | 17 | "kill:all": "bun run kill-all.ts", |
| 18 | "dev:sources": "bun run --cwd sources dev", | ||
| 19 | "sources:sync": "bun run --cwd sources sync", | ||
| 17 | "dev:executor:convex": "bunx --cwd executor convex dev", | 20 | "dev:executor:convex": "bunx --cwd executor convex dev", |
| 18 | "dev:executor:web": "bun run --cwd executor/apps/web dev", | 21 | "dev:executor:web": "bun run --cwd executor/apps/web dev", |
| 19 | "dev:executor:all": "bun run dev:executor:convex & bun run dev:executor:web", | 22 | "dev:executor:all": "bun run dev:executor:convex & bun run dev:executor:web", |
| @@ -0,0 +1,16 @@ | |||
| 1 | { | ||
| 2 | "name": "@proto/sources", | ||
| 3 | "private": true, | ||
| 4 | "type": "module", | ||
| 5 | "scripts": { | ||
| 6 | "dev": "bun --hot server.ts", | ||
| 7 | "start": "bun server.ts", | ||
| 8 | "sync": "bun server.ts --sync-only", | ||
| 9 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" | ||
| 10 | }, | ||
| 11 | "dependencies": {}, | ||
| 12 | "devDependencies": { | ||
| 13 | "@types/bun": "latest", | ||
| 14 | "typescript": "^5.9.3" | ||
| 15 | } | ||
| 16 | } | ||
| @@ -0,0 +1,384 @@ | |||
| 1 | import { Database } from "bun:sqlite"; | ||
| 2 | import { fileURLToPath } from "node:url"; | ||
| 3 | |||
| 4 | // --------------------------------------------------------------------------- | ||
| 5 | // List.json shape (from APIs.guru / our fork) | ||
| 6 | // --------------------------------------------------------------------------- | ||
| 7 | |||
| 8 | interface ListOrigin { | ||
| 9 | format: string; | ||
| 10 | url: string; | ||
| 11 | version: string; | ||
| 12 | } | ||
| 13 | |||
| 14 | interface ListVersionInfo { | ||
| 15 | contact?: { email?: string; name?: string; url?: string }; | ||
| 16 | description?: string; | ||
| 17 | title?: string; | ||
| 18 | version?: string; | ||
| 19 | "x-apisguru-categories"?: string[]; | ||
| 20 | "x-logo"?: { url?: string }; | ||
| 21 | "x-origin"?: ListOrigin[]; | ||
| 22 | "x-providerName"?: string; | ||
| 23 | } | ||
| 24 | |||
| 25 | interface ListVersion { | ||
| 26 | added?: string; | ||
| 27 | updated?: string; | ||
| 28 | info: ListVersionInfo; | ||
| 29 | swaggerUrl: string; | ||
| 30 | swaggerYamlUrl?: string; | ||
| 31 | openapiVer?: string; | ||
| 32 | link?: string; | ||
| 33 | } | ||
| 34 | |||
| 35 | interface ListEntry { | ||
| 36 | added?: string; | ||
| 37 | preferred: string; | ||
| 38 | versions: Record<string, ListVersion>; | ||
| 39 | } | ||
| 40 | |||
| 41 | type ListJson = Record<string, ListEntry>; | ||
| 42 | |||
| 43 | // --------------------------------------------------------------------------- | ||
| 44 | // Exported types | ||
| 45 | // --------------------------------------------------------------------------- | ||
| 46 | |||
| 47 | export interface CatalogItem { | ||
| 48 | id: string; | ||
| 49 | name: string; | ||
| 50 | summary: string; | ||
| 51 | specUrl: string; | ||
| 52 | originUrl: string; | ||
| 53 | providerName: string; | ||
| 54 | logoUrl: string; | ||
| 55 | categories: string; | ||
| 56 | version: string; | ||
| 57 | } | ||
| 58 | |||
| 59 | export interface SyncState { | ||
| 60 | inFlight: Promise<SyncResult> | null; | ||
| 61 | lastSyncedAt: number | null; | ||
| 62 | lastError: string | null; | ||
| 63 | lastCount: number; | ||
| 64 | } | ||
| 65 | |||
| 66 | export interface SyncResult { | ||
| 67 | count: number; | ||
| 68 | syncedAt: number; | ||
| 69 | trigger: string; | ||
| 70 | } | ||
| 71 | |||
| 72 | export interface SyncConfig { | ||
| 73 | listUrl: string; | ||
| 74 | } | ||
| 75 | |||
| 76 | // --------------------------------------------------------------------------- | ||
| 77 | // Constants | ||
| 78 | // --------------------------------------------------------------------------- | ||
| 79 | |||
| 80 | const DEFAULT_LIST_URL = "https://api.apis.guru/v2/list.json"; | ||
| 81 | const SUPPLEMENTS_PATH = fileURLToPath(new URL("./supplements.json", import.meta.url)); | ||
| 82 | |||
| 83 | // --------------------------------------------------------------------------- | ||
| 84 | // Text helpers | ||
| 85 | // --------------------------------------------------------------------------- | ||
| 86 | |||
| 87 | function normalizeText(value: string): string { | ||
| 88 | return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim(); | ||
| 89 | } | ||
| 90 | |||
| 91 | function compactText(value: string): string { | ||
| 92 | return value.toLowerCase().replace(/[^a-z0-9]+/g, ""); | ||
| 93 | } | ||
| 94 | |||
| 95 | function parseTimestamp(value: string | undefined): number { | ||
| 96 | if (!value) return 0; | ||
| 97 | const parsed = Date.parse(value.trim()); | ||
| 98 | return Number.isFinite(parsed) ? parsed : 0; | ||
| 99 | } | ||
| 100 | |||
| 101 | // --------------------------------------------------------------------------- | ||
| 102 | // Mapping – list.json entry → DB row | ||
| 103 | // --------------------------------------------------------------------------- | ||
| 104 | |||
| 105 | interface ApiRow { | ||
| 106 | id: string; | ||
| 107 | name: string; | ||
| 108 | summary: string; | ||
| 109 | spec_url: string; | ||
| 110 | origin_url: string; | ||
| 111 | provider_name: string; | ||
| 112 | logo_url: string; | ||
| 113 | categories: string; | ||
| 114 | version: string; | ||
| 115 | updated_at: number; | ||
| 116 | search_text: string; | ||
| 117 | search_compact: string; | ||
| 118 | } | ||
| 119 | |||
| 120 | function mapEntry(providerKey: string, entry: ListEntry): ApiRow | null { | ||
| 121 | const ver = entry.versions[entry.preferred]; | ||
| 122 | if (!ver) return null; | ||
| 123 | |||
| 124 | const info = ver.info; | ||
| 125 | const name = (info.title ?? providerKey).trim(); | ||
| 126 | if (!name) return null; | ||
| 127 | |||
| 128 | const summary = (info.description ?? "").trim(); | ||
| 129 | const specUrl = ver.swaggerUrl; | ||
| 130 | if (!specUrl) return null; | ||
| 131 | |||
| 132 | const originUrl = info["x-origin"]?.[0]?.url ?? ""; | ||
| 133 | const providerName = (info["x-providerName"] ?? providerKey).trim(); | ||
| 134 | const logoUrl = info["x-logo"]?.url ?? ""; | ||
| 135 | const categories = (info["x-apisguru-categories"] ?? []).join(","); | ||
| 136 | const version = (info.version ?? entry.preferred).trim(); | ||
| 137 | const updatedAt = parseTimestamp(ver.updated) || parseTimestamp(entry.added) || 0; | ||
| 138 | |||
| 139 | const searchText = normalizeText( | ||
| 140 | `${name} ${providerName} ${providerKey} ${summary.slice(0, 300)} ${categories.replace(/,/g, " ")}`, | ||
| 141 | ); | ||
| 142 | |||
| 143 | return { | ||
| 144 | id: providerKey, | ||
| 145 | name, | ||
| 146 | summary, | ||
| 147 | spec_url: specUrl, | ||
| 148 | origin_url: originUrl, | ||
| 149 | provider_name: providerName, | ||
| 150 | logo_url: logoUrl, | ||
| 151 | categories, | ||
| 152 | version, | ||
| 153 | updated_at: updatedAt, | ||
| 154 | search_text: searchText, | ||
| 155 | search_compact: compactText(searchText), | ||
| 156 | }; | ||
| 157 | } | ||
| 158 | |||
| 159 | // --------------------------------------------------------------------------- | ||
| 160 | // SQLite | ||
| 161 | // --------------------------------------------------------------------------- | ||
| 162 | |||
| 163 | export function createDb(dbPath: string): Database { | ||
| 164 | const db = new Database(dbPath); | ||
| 165 | db.exec(` | ||
| 166 | PRAGMA journal_mode = WAL; | ||
| 167 | PRAGMA synchronous = NORMAL; | ||
| 168 | |||
| 169 | CREATE TABLE IF NOT EXISTS api_catalog ( | ||
| 170 | id TEXT PRIMARY KEY, | ||
| 171 | name TEXT NOT NULL, | ||
| 172 | summary TEXT NOT NULL, | ||
| 173 | spec_url TEXT NOT NULL, | ||
| 174 | origin_url TEXT NOT NULL DEFAULT '', | ||
| 175 | provider_name TEXT NOT NULL, | ||
| 176 | logo_url TEXT NOT NULL DEFAULT '', | ||
| 177 | categories TEXT NOT NULL DEFAULT '', | ||
| 178 | version TEXT NOT NULL DEFAULT '', | ||
| 179 | updated_at INTEGER NOT NULL DEFAULT 0, | ||
| 180 | search_text TEXT NOT NULL, | ||
| 181 | search_compact TEXT NOT NULL | ||
| 182 | ); | ||
| 183 | |||
| 184 | CREATE INDEX IF NOT EXISTS idx_catalog_name ON api_catalog(name); | ||
| 185 | CREATE INDEX IF NOT EXISTS idx_catalog_updated ON api_catalog(updated_at DESC); | ||
| 186 | CREATE INDEX IF NOT EXISTS idx_catalog_provider ON api_catalog(provider_name); | ||
| 187 | CREATE INDEX IF NOT EXISTS idx_catalog_search ON api_catalog(search_compact); | ||
| 188 | `); | ||
| 189 | return db; | ||
| 190 | } | ||
| 191 | |||
| 192 | export function rowCount(db: Database): number { | ||
| 193 | const row = db.query("SELECT COUNT(*) AS count FROM api_catalog").get() as { count: number } | null; | ||
| 194 | return row?.count ?? 0; | ||
| 195 | } | ||
| 196 | |||
| 197 | function upsertRows(db: Database, rows: ApiRow[]): void { | ||
| 198 | if (rows.length === 0) return; | ||
| 199 | |||
| 200 | const upsert = db.query(` | ||
| 201 | INSERT INTO api_catalog ( | ||
| 202 | id, name, summary, spec_url, origin_url, | ||
| 203 | provider_name, logo_url, categories, version, | ||
| 204 | updated_at, search_text, search_compact | ||
| 205 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | ||
| 206 | ON CONFLICT(id) DO UPDATE SET | ||
| 207 | name = excluded.name, | ||
| 208 | summary = excluded.summary, | ||
| 209 | spec_url = excluded.spec_url, | ||
| 210 | origin_url = excluded.origin_url, | ||
| 211 | provider_name = excluded.provider_name, | ||
| 212 | logo_url = excluded.logo_url, | ||
| 213 | categories = excluded.categories, | ||
| 214 | version = excluded.version, | ||
| 215 | updated_at = excluded.updated_at, | ||
| 216 | search_text = excluded.search_text, | ||
| 217 | search_compact = excluded.search_compact | ||
| 218 | `); | ||
| 219 | |||
| 220 | db.exec("BEGIN IMMEDIATE"); | ||
| 221 | try { | ||
| 222 | for (const r of rows) { | ||
| 223 | upsert.run( | ||
| 224 | r.id, r.name, r.summary, r.spec_url, r.origin_url, | ||
| 225 | r.provider_name, r.logo_url, r.categories, r.version, | ||
| 226 | r.updated_at, r.search_text, r.search_compact, | ||
| 227 | ); | ||
| 228 | } | ||
| 229 | db.exec("COMMIT"); | ||
| 230 | } catch (error) { | ||
| 231 | db.exec("ROLLBACK"); | ||
| 232 | throw error; | ||
| 233 | } | ||
| 234 | } | ||
| 235 | |||
| 236 | // --------------------------------------------------------------------------- | ||
| 237 | // Sync | ||
| 238 | // --------------------------------------------------------------------------- | ||
| 239 | |||
| 240 | async function loadSupplements(): Promise<ListJson> { | ||
| 241 | try { | ||
| 242 | const file = Bun.file(SUPPLEMENTS_PATH); | ||
| 243 | if (!(await file.exists())) return {}; | ||
| 244 | const data = (await file.json()) as ListJson & { $comment?: string }; | ||
| 245 | delete data.$comment; | ||
| 246 | return data; | ||
| 247 | } catch { | ||
| 248 | return {}; | ||
| 249 | } | ||
| 250 | } | ||
| 251 | |||
| 252 | async function fetchAndSync(db: Database, config: SyncConfig): Promise<number> { | ||
| 253 | const url = config.listUrl || DEFAULT_LIST_URL; | ||
| 254 | console.log(`[scraper] fetching ${url}`); | ||
| 255 | |||
| 256 | const response = await fetch(url); | ||
| 257 | if (!response.ok) { | ||
| 258 | throw new Error(`Failed to fetch list.json: HTTP ${response.status}`); | ||
| 259 | } | ||
| 260 | |||
| 261 | const data = (await response.json()) as ListJson; | ||
| 262 | |||
| 263 | // Merge local supplements (overrides upstream if same key) | ||
| 264 | const supplements = await loadSupplements(); | ||
| 265 | const supplementCount = Object.keys(supplements).length; | ||
| 266 | if (supplementCount > 0) { | ||
| 267 | Object.assign(data, supplements); | ||
| 268 | console.log(`[scraper] merged ${supplementCount} supplemental entries`); | ||
| 269 | } | ||
| 270 | |||
| 271 | const entries = Object.entries(data); | ||
| 272 | console.log(`[scraper] parsing ${entries.length} entries`); | ||
| 273 | |||
| 274 | const rows: ApiRow[] = []; | ||
| 275 | for (const [key, entry] of entries) { | ||
| 276 | const row = mapEntry(key, entry); | ||
| 277 | if (row) rows.push(row); | ||
| 278 | } | ||
| 279 | |||
| 280 | // Batch upsert in chunks of 500 | ||
| 281 | const CHUNK = 500; | ||
| 282 | for (let i = 0; i < rows.length; i += CHUNK) { | ||
| 283 | upsertRows(db, rows.slice(i, i + CHUNK)); | ||
| 284 | } | ||
| 285 | |||
| 286 | const count = rowCount(db); | ||
| 287 | console.log(`[scraper] synced ${rows.length} rows (${count} total in db)`); | ||
| 288 | return count; | ||
| 289 | } | ||
| 290 | |||
| 291 | export function triggerSync( | ||
| 292 | db: Database, | ||
| 293 | state: SyncState, | ||
| 294 | config: SyncConfig, | ||
| 295 | trigger: string, | ||
| 296 | ): Promise<SyncResult> { | ||
| 297 | if (state.inFlight) return state.inFlight; | ||
| 298 | |||
| 299 | const promise = fetchAndSync(db, config) | ||
| 300 | .then((count) => { | ||
| 301 | const syncedAt = Date.now(); | ||
| 302 | state.lastSyncedAt = syncedAt; | ||
| 303 | state.lastError = null; | ||
| 304 | state.lastCount = count; | ||
| 305 | return { count, syncedAt, trigger } satisfies SyncResult; | ||
| 306 | }) | ||
| 307 | .catch((error) => { | ||
| 308 | state.lastError = error instanceof Error ? error.message : String(error); | ||
| 309 | throw error; | ||
| 310 | }) | ||
| 311 | .finally(() => { | ||
| 312 | state.inFlight = null; | ||
| 313 | }); | ||
| 314 | |||
| 315 | state.inFlight = promise; | ||
| 316 | return promise; | ||
| 317 | } | ||
| 318 | |||
| 319 | // --------------------------------------------------------------------------- | ||
| 320 | // Query | ||
| 321 | // --------------------------------------------------------------------------- | ||
| 322 | |||
| 323 | function buildSearchFilter(q: string): { sql: string; params: string[] } { | ||
| 324 | const trimmed = q.trim(); | ||
| 325 | if (!trimmed) return { sql: "", params: [] }; | ||
| 326 | |||
| 327 | const normalized = normalizeText(trimmed); | ||
| 328 | const compact = compactText(normalized); | ||
| 329 | const tokens = normalized.split(/\s+/).filter(Boolean); | ||
| 330 | |||
| 331 | const clauses: string[] = []; | ||
| 332 | const params: string[] = []; | ||
| 333 | |||
| 334 | if (compact.length > 0) { | ||
| 335 | clauses.push("search_compact LIKE ?"); | ||
| 336 | params.push(`%${compact}%`); | ||
| 337 | } | ||
| 338 | |||
| 339 | for (const token of tokens) { | ||
| 340 | clauses.push("search_text LIKE ?"); | ||
| 341 | params.push(`%${token}%`); | ||
| 342 | } | ||
| 343 | |||
| 344 | return { | ||
| 345 | sql: `WHERE (${clauses.join(" OR ")})`, | ||
| 346 | params, | ||
| 347 | }; | ||
| 348 | } | ||
| 349 | |||
| 350 | export function queryCollections(db: Database, input: { | ||
| 351 | q: string; | ||
| 352 | sort: "popular" | "recent"; | ||
| 353 | limit: number; | ||
| 354 | offset: number; | ||
| 355 | }): { items: CatalogItem[]; totalCount: number; hasMore: boolean } { | ||
| 356 | const { sql, params } = buildSearchFilter(input.q); | ||
| 357 | |||
| 358 | const countRow = db.query( | ||
| 359 | `SELECT COUNT(*) AS count FROM api_catalog ${sql}`, | ||
| 360 | ).get(...params) as { count: number } | null; | ||
| 361 | const totalCount = countRow?.count ?? 0; | ||
| 362 | |||
| 363 | // "popular" → alphabetical by name (no popularity metric from APIs.guru) | ||
| 364 | // "recent" → by updated_at desc | ||
| 365 | const orderBy = input.sort === "recent" | ||
| 366 | ? "updated_at DESC, name COLLATE NOCASE ASC" | ||
| 367 | : "name COLLATE NOCASE ASC"; | ||
| 368 | |||
| 369 | const rows = db.query(` | ||
| 370 | SELECT | ||
| 371 | id, name, summary, spec_url AS specUrl, | ||
| 372 | origin_url AS originUrl, provider_name AS providerName, | ||
| 373 | logo_url AS logoUrl, categories, version | ||
| 374 | FROM api_catalog ${sql} | ||
| 375 | ORDER BY ${orderBy} | ||
| 376 | LIMIT ? OFFSET ? | ||
| 377 | `).all(...params, input.limit, input.offset) as CatalogItem[]; | ||
| 378 | |||
| 379 | return { | ||
| 380 | items: rows, | ||
| 381 | totalCount, | ||
| 382 | hasMore: input.offset + rows.length < totalCount, | ||
| 383 | }; | ||
| 384 | } | ||
| @@ -0,0 +1,192 @@ | |||
| 1 | #!/usr/bin/env bun | ||
| 2 | |||
| 3 | import { mkdir } from "node:fs/promises"; | ||
| 4 | import { dirname } from "node:path"; | ||
| 5 | import { fileURLToPath } from "node:url"; | ||
| 6 | import { | ||
| 7 | createDb, | ||
| 8 | queryCollections, | ||
| 9 | rowCount, | ||
| 10 | triggerSync, | ||
| 11 | type SyncState, | ||
| 12 | type SyncConfig, | ||
| 13 | } from "./scraper"; | ||
| 14 | |||
| 15 | // --------------------------------------------------------------------------- | ||
| 16 | // Config (env with defaults) | ||
| 17 | // --------------------------------------------------------------------------- | ||
| 18 | |||
| 19 | const PORT = Math.max(1, Math.min(65535, Number(process.env.SOURCES_PORT) || 4343)); | ||
| 20 | const DB_PATH = process.env.SOURCES_DB_PATH ?? fileURLToPath(new URL("./data/catalog.sqlite", import.meta.url)); | ||
| 21 | const SYNC_INTERVAL_MS = Math.max(60_000, Number(process.env.SOURCES_SYNC_INTERVAL_MS) || 6 * 60 * 60 * 1000); | ||
| 22 | const LIST_URL = process.env.SOURCES_LIST_URL ?? "https://api.apis.guru/v2/list.json"; | ||
| 23 | const ALLOWED_ORIGINS = process.env.SOURCES_ALLOWED_ORIGINS?.split(",").map((s) => s.trim()).filter(Boolean) ?? []; | ||
| 24 | const DEFAULT_LIMIT = 25; | ||
| 25 | const MAX_LIMIT = 100; | ||
| 26 | |||
| 27 | // --------------------------------------------------------------------------- | ||
| 28 | // Helpers | ||
| 29 | // --------------------------------------------------------------------------- | ||
| 30 | |||
| 31 | function parseInteger(input: string | null, fallback: number): number { | ||
| 32 | if (!input) return fallback; | ||
| 33 | const parsed = Number(input); | ||
| 34 | if (!Number.isFinite(parsed)) return fallback; | ||
| 35 | return Math.floor(parsed); | ||
| 36 | } | ||
| 37 | |||
| 38 | function parseSort(value: string | null): "popular" | "recent" { | ||
| 39 | return value === "recent" ? "recent" : "popular"; | ||
| 40 | } | ||
| 41 | |||
| 42 | function json(data: unknown, status = 200, corsHeaders?: Record<string, string>): Response { | ||
| 43 | return Response.json(data, { | ||
| 44 | status, | ||
| 45 | headers: { "cache-control": "no-store", ...corsHeaders }, | ||
| 46 | }); | ||
| 47 | } | ||
| 48 | |||
| 49 | function corsHeaders(request: Request): Record<string, string> { | ||
| 50 | const origin = request.headers.get("origin") ?? ""; | ||
| 51 | // In dev, allow any localhost origin. In prod, check allowlist. | ||
| 52 | const allowed = | ||
| 53 | origin.match(/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/) || | ||
| 54 | ALLOWED_ORIGINS.includes(origin); | ||
| 55 | |||
| 56 | if (!allowed && origin) return {}; | ||
| 57 | |||
| 58 | return { | ||
| 59 | "access-control-allow-origin": origin || "*", | ||
| 60 | "access-control-allow-methods": "GET, POST, OPTIONS", | ||
| 61 | "access-control-allow-headers": "content-type", | ||
| 62 | "access-control-max-age": "86400", | ||
| 63 | }; | ||
| 64 | } | ||
| 65 | |||
| 66 | // --------------------------------------------------------------------------- | ||
| 67 | // Init | ||
| 68 | // --------------------------------------------------------------------------- | ||
| 69 | |||
| 70 | await mkdir(dirname(DB_PATH), { recursive: true }); | ||
| 71 | const db = createDb(DB_PATH); | ||
| 72 | const syncConfig: SyncConfig = { listUrl: LIST_URL }; | ||
| 73 | const state: SyncState = { | ||
| 74 | inFlight: null, | ||
| 75 | lastSyncedAt: null, | ||
| 76 | lastError: null, | ||
| 77 | lastCount: rowCount(db), | ||
| 78 | }; | ||
| 79 | |||
| 80 | // --------------------------------------------------------------------------- | ||
| 81 | // CLI: --sync-only | ||
| 82 | // --------------------------------------------------------------------------- | ||
| 83 | |||
| 84 | if (Bun.argv.includes("--sync-only")) { | ||
| 85 | try { | ||
| 86 | const result = await triggerSync(db, state, syncConfig, "cli"); | ||
| 87 | console.log(`synced ${result.count} sources into ${DB_PATH}`); | ||
| 88 | } finally { | ||
| 89 | db.close(); | ||
| 90 | } | ||
| 91 | process.exit(0); | ||
| 92 | } | ||
| 93 | |||
| 94 | // --------------------------------------------------------------------------- | ||
| 95 | // Server | ||
| 96 | // --------------------------------------------------------------------------- | ||
| 97 | |||
| 98 | const server = Bun.serve({ | ||
| 99 | port: PORT, | ||
| 100 | fetch: async (request) => { | ||
| 101 | const cors = corsHeaders(request); | ||
| 102 | |||
| 103 | // Preflight | ||
| 104 | if (request.method === "OPTIONS") { | ||
| 105 | return new Response(null, { status: 204, headers: cors }); | ||
| 106 | } | ||
| 107 | |||
| 108 | const url = new URL(request.url); | ||
| 109 | |||
| 110 | // GET /health | ||
| 111 | if (request.method === "GET" && url.pathname === "/health") { | ||
| 112 | return json({ | ||
| 113 | ok: true, | ||
| 114 | count: rowCount(db), | ||
| 115 | lastSyncedAt: state.lastSyncedAt, | ||
| 116 | lastError: state.lastError, | ||
| 117 | lastCount: state.lastCount, | ||
| 118 | }, 200, cors); | ||
| 119 | } | ||
| 120 | |||
| 121 | // POST|GET /admin/sync | ||
| 122 | if ((request.method === "POST" || request.method === "GET") && url.pathname === "/admin/sync") { | ||
| 123 | try { | ||
| 124 | const result = await triggerSync(db, state, syncConfig, "manual"); | ||
| 125 | return json({ ok: true, ...result }, 200, cors); | ||
| 126 | } catch (error) { | ||
| 127 | return json({ ok: false, error: error instanceof Error ? error.message : String(error) }, 500, cors); | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | // GET /collections | ||
| 132 | if (request.method === "GET" && url.pathname === "/collections") { | ||
| 133 | try { | ||
| 134 | // Auto-seed on first request if DB is empty | ||
| 135 | if (rowCount(db) === 0) { | ||
| 136 | await triggerSync(db, state, syncConfig, "seed"); | ||
| 137 | } | ||
| 138 | |||
| 139 | const q = url.searchParams.get("q")?.trim() ?? ""; | ||
| 140 | const sort = parseSort(url.searchParams.get("sort")); | ||
| 141 | const limit = Math.max(1, Math.min(MAX_LIMIT, parseInteger(url.searchParams.get("limit"), DEFAULT_LIMIT))); | ||
| 142 | const offset = Math.max(0, parseInteger(url.searchParams.get("offset"), 0)); | ||
| 143 | |||
| 144 | const result = queryCollections(db, { q, sort, limit, offset }); | ||
| 145 | return json({ | ||
| 146 | items: result.items, | ||
| 147 | totalCount: result.totalCount, | ||
| 148 | hasMore: result.hasMore, | ||
| 149 | limit, | ||
| 150 | offset, | ||
| 151 | }, 200, cors); | ||
| 152 | } catch (error) { | ||
| 153 | return json({ | ||
| 154 | error: "Failed to load API catalog", | ||
| 155 | detail: error instanceof Error ? error.message : String(error), | ||
| 156 | }, 500, cors); | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | return json({ error: "Not found" }, 404, cors); | ||
| 161 | }, | ||
| 162 | }); | ||
| 163 | |||
| 164 | console.log(`[sources] running on http://127.0.0.1:${server.port}`); | ||
| 165 | console.log(`[sources] db: ${DB_PATH}`); | ||
| 166 | console.log(`[sources] endpoints: GET /collections, GET /health, POST /admin/sync`); | ||
| 167 | |||
| 168 | // Non-blocking startup sync | ||
| 169 | void triggerSync(db, state, syncConfig, "startup").catch((error) => { | ||
| 170 | console.error(`[sources] initial sync failed: ${error instanceof Error ? error.message : String(error)}`); | ||
| 171 | }); | ||
| 172 | |||
| 173 | // Periodic sync | ||
| 174 | const syncInterval = setInterval(() => { | ||
| 175 | void triggerSync(db, state, syncConfig, "interval").catch((error) => { | ||
| 176 | console.error(`[sources] scheduled sync failed: ${error instanceof Error ? error.message : String(error)}`); | ||
| 177 | }); | ||
| 178 | }, SYNC_INTERVAL_MS); | ||
| 179 | |||
| 180 | // Graceful shutdown | ||
| 181 | let shuttingDown = false; | ||
| 182 | const shutdown = () => { | ||
| 183 | if (shuttingDown) return; | ||
| 184 | shuttingDown = true; | ||
| 185 | clearInterval(syncInterval); | ||
| 186 | server.stop(); | ||
| 187 | db.close(); | ||
| 188 | process.exit(0); | ||
| 189 | }; | ||
| 190 | |||
| 191 | process.on("SIGINT", shutdown); | ||
| 192 | process.on("SIGTERM", shutdown); | ||
| @@ -0,0 +1,171 @@ | |||
| 1 | { | ||
| 2 | "$comment": "Additional OpenAPI specs not in APIs.guru. Same shape as list.json entries.", | ||
| 3 | "cloudflare.com": { | ||
| 4 | "added": "2026-02-11T00:00:00.000Z", | ||
| 5 | "preferred": "4", | ||
| 6 | "versions": { | ||
| 7 | "4": { | ||
| 8 | "added": "2026-02-11T00:00:00.000Z", | ||
| 9 | "updated": "2026-02-11T00:00:00.000Z", | ||
| 10 | "info": { | ||
| 11 | "title": "Cloudflare API", | ||
| 12 | "description": "Interact with Cloudflare's products and services via the Cloudflare API.", | ||
| 13 | "version": "4", | ||
| 14 | "x-apisguru-categories": ["cloud", "hosting"], | ||
| 15 | "x-logo": { "url": "https://www.cloudflare.com/favicon.ico" }, | ||
| 16 | "x-origin": [{ "format": "openapi", "url": "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json", "version": "3.0" }], | ||
| 17 | "x-providerName": "cloudflare.com" | ||
| 18 | }, | ||
| 19 | "swaggerUrl": "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json", | ||
| 20 | "openapiVer": "3.0.3" | ||
| 21 | } | ||
| 22 | } | ||
| 23 | }, | ||
| 24 | "discord.com": { | ||
| 25 | "added": "2026-02-11T00:00:00.000Z", | ||
| 26 | "preferred": "10", | ||
| 27 | "versions": { | ||
| 28 | "10": { | ||
| 29 | "added": "2026-02-11T00:00:00.000Z", | ||
| 30 | "updated": "2026-02-11T00:00:00.000Z", | ||
| 31 | "info": { | ||
| 32 | "title": "Discord HTTP API", | ||
| 33 | "description": "HTTP API for Discord. Build bots, integrations, and applications that interact with Discord.", | ||
| 34 | "version": "10", | ||
| 35 | "x-apisguru-categories": ["messaging", "social"], | ||
| 36 | "x-logo": { "url": "https://discord.com/assets/favicon.ico" }, | ||
| 37 | "x-origin": [{ "format": "openapi", "url": "https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json", "version": "3.1" }], | ||
| 38 | "x-providerName": "discord.com" | ||
| 39 | }, | ||
| 40 | "swaggerUrl": "https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json", | ||
| 41 | "openapiVer": "3.1.0" | ||
| 42 | } | ||
| 43 | } | ||
| 44 | }, | ||
| 45 | "paypal.com": { | ||
| 46 | "added": "2026-02-11T00:00:00.000Z", | ||
| 47 | "preferred": "2", | ||
| 48 | "versions": { | ||
| 49 | "2": { | ||
| 50 | "added": "2026-02-11T00:00:00.000Z", | ||
| 51 | "updated": "2026-02-11T00:00:00.000Z", | ||
| 52 | "info": { | ||
| 53 | "title": "PayPal Checkout Orders API", | ||
| 54 | "description": "Use the Orders API to create, update, retrieve, authorize, and capture orders (payments).", | ||
| 55 | "version": "2", | ||
| 56 | "x-apisguru-categories": ["financial", "payment"], | ||
| 57 | "x-logo": { "url": "https://www.paypal.com/favicon.ico" }, | ||
| 58 | "x-origin": [{ "format": "openapi", "url": "https://raw.githubusercontent.com/paypal/paypal-rest-api-specifications/main/openapi/checkout_orders_v2.json", "version": "3.0" }], | ||
| 59 | "x-providerName": "paypal.com" | ||
| 60 | }, | ||
| 61 | "swaggerUrl": "https://raw.githubusercontent.com/paypal/paypal-rest-api-specifications/main/openapi/checkout_orders_v2.json", | ||
| 62 | "openapiVer": "3.0.0" | ||
| 63 | } | ||
| 64 | } | ||
| 65 | }, | ||
| 66 | "resend.com": { | ||
| 67 | "added": "2026-02-11T00:00:00.000Z", | ||
| 68 | "preferred": "1.0.0", | ||
| 69 | "versions": { | ||
| 70 | "1.0.0": { | ||
| 71 | "added": "2026-02-11T00:00:00.000Z", | ||
| 72 | "updated": "2026-02-11T00:00:00.000Z", | ||
| 73 | "info": { | ||
| 74 | "title": "Resend API", | ||
| 75 | "description": "Email for developers. Send transactional and marketing emails with a simple API.", | ||
| 76 | "version": "1.0.0", | ||
| 77 | "x-apisguru-categories": ["email", "messaging"], | ||
| 78 | "x-logo": { "url": "https://resend.com/favicon.ico" }, | ||
| 79 | "x-origin": [{ "format": "openapi", "url": "https://raw.githubusercontent.com/resend/resend-openapi/main/resend.yaml", "version": "3.0" }], | ||
| 80 | "x-providerName": "resend.com" | ||
| 81 | }, | ||
| 82 | "swaggerUrl": "https://raw.githubusercontent.com/resend/resend-openapi/main/resend.yaml", | ||
| 83 | "openapiVer": "3.0.3" | ||
| 84 | } | ||
| 85 | } | ||
| 86 | }, | ||
| 87 | "figma.com": { | ||
| 88 | "added": "2026-02-11T00:00:00.000Z", | ||
| 89 | "preferred": "0.36.0", | ||
| 90 | "versions": { | ||
| 91 | "0.36.0": { | ||
| 92 | "added": "2026-02-11T00:00:00.000Z", | ||
| 93 | "updated": "2026-02-11T00:00:00.000Z", | ||
| 94 | "info": { | ||
| 95 | "title": "Figma REST API", | ||
| 96 | "description": "The Figma REST API. Access files, components, styles, comments, users, projects, and more.", | ||
| 97 | "version": "0.36.0", | ||
| 98 | "x-apisguru-categories": ["design", "collaboration"], | ||
| 99 | "x-logo": { "url": "https://static.figma.com/app/icon/1/favicon.ico" }, | ||
| 100 | "x-origin": [{ "format": "openapi", "url": "https://raw.githubusercontent.com/figma/rest-api-spec/main/openapi/openapi.yaml", "version": "3.1" }], | ||
| 101 | "x-providerName": "figma.com" | ||
| 102 | }, | ||
| 103 | "swaggerUrl": "https://raw.githubusercontent.com/figma/rest-api-spec/main/openapi/openapi.yaml", | ||
| 104 | "openapiVer": "3.1.0" | ||
| 105 | } | ||
| 106 | } | ||
| 107 | }, | ||
| 108 | "sentry.io": { | ||
| 109 | "added": "2026-02-11T00:00:00.000Z", | ||
| 110 | "preferred": "v0", | ||
| 111 | "versions": { | ||
| 112 | "v0": { | ||
| 113 | "added": "2026-02-11T00:00:00.000Z", | ||
| 114 | "updated": "2026-02-11T00:00:00.000Z", | ||
| 115 | "info": { | ||
| 116 | "title": "Sentry API", | ||
| 117 | "description": "Sentry's API for error tracking, performance monitoring, and debugging.", | ||
| 118 | "version": "v0", | ||
| 119 | "x-apisguru-categories": ["monitoring", "developer_tools"], | ||
| 120 | "x-logo": { "url": "https://sentry.io/favicon.ico" }, | ||
| 121 | "x-origin": [{ "format": "openapi", "url": "https://raw.githubusercontent.com/getsentry/sentry-api-schema/main/openapi-derefed.json", "version": "3.0" }], | ||
| 122 | "x-providerName": "sentry.io" | ||
| 123 | }, | ||
| 124 | "swaggerUrl": "https://raw.githubusercontent.com/getsentry/sentry-api-schema/main/openapi-derefed.json", | ||
| 125 | "openapiVer": "3.0.3" | ||
| 126 | } | ||
| 127 | } | ||
| 128 | }, | ||
| 129 | "pagerduty.com": { | ||
| 130 | "added": "2026-02-11T00:00:00.000Z", | ||
| 131 | "preferred": "v2", | ||
| 132 | "versions": { | ||
| 133 | "v2": { | ||
| 134 | "added": "2026-02-11T00:00:00.000Z", | ||
| 135 | "updated": "2026-02-11T00:00:00.000Z", | ||
| 136 | "info": { | ||
| 137 | "title": "PagerDuty API", | ||
| 138 | "description": "PagerDuty's REST API for incident management, on-call scheduling, and alerting.", | ||
| 139 | "version": "v2", | ||
| 140 | "x-apisguru-categories": ["monitoring", "developer_tools"], | ||
| 141 | "x-logo": { "url": "https://www.pagerduty.com/favicon.ico" }, | ||
| 142 | "x-origin": [{ "format": "openapi", "url": "https://raw.githubusercontent.com/PagerDuty/api-schema/main/reference/REST/openapiv3.json", "version": "3.0" }], | ||
| 143 | "x-providerName": "pagerduty.com" | ||
| 144 | }, | ||
| 145 | "swaggerUrl": "https://raw.githubusercontent.com/PagerDuty/api-schema/main/reference/REST/openapiv3.json", | ||
| 146 | "openapiVer": "3.0.1" | ||
| 147 | } | ||
| 148 | } | ||
| 149 | }, | ||
| 150 | "intercom.io": { | ||
| 151 | "added": "2026-02-11T00:00:00.000Z", | ||
| 152 | "preferred": "2.13", | ||
| 153 | "versions": { | ||
| 154 | "2.13": { | ||
| 155 | "added": "2026-02-11T00:00:00.000Z", | ||
| 156 | "updated": "2026-02-11T00:00:00.000Z", | ||
| 157 | "info": { | ||
| 158 | "title": "Intercom API", | ||
| 159 | "description": "The Intercom API. Build customer messaging integrations with contacts, conversations, and more.", | ||
| 160 | "version": "2.13", | ||
| 161 | "x-apisguru-categories": ["messaging", "customer_support"], | ||
| 162 | "x-logo": { "url": "https://www.intercom.com/favicon.ico" }, | ||
| 163 | "x-origin": [{ "format": "openapi", "url": "https://raw.githubusercontent.com/intercom/Intercom-OpenAPI/main/descriptions/2.13/api.intercom.io.yaml", "version": "3.0" }], | ||
| 164 | "x-providerName": "intercom.io" | ||
| 165 | }, | ||
| 166 | "swaggerUrl": "https://raw.githubusercontent.com/intercom/Intercom-OpenAPI/main/descriptions/2.13/api.intercom.io.yaml", | ||
| 167 | "openapiVer": "3.0.1" | ||
| 168 | } | ||
| 169 | } | ||
| 170 | } | ||
| 171 | } | ||
| @@ -0,0 +1,20 @@ | |||
| 1 | { | ||
| 2 | "compilerOptions": { | ||
| 3 | "target": "ESNext", | ||
| 4 | "module": "ESNext", | ||
| 5 | "moduleResolution": "Bundler", | ||
| 6 | "strict": true, | ||
| 7 | "skipLibCheck": true, | ||
| 8 | "noEmit": true, | ||
| 9 | "types": [ | ||
| 10 | "bun" | ||
| 11 | ], | ||
| 12 | "lib": [ | ||
| 13 | "ESNext", | ||
| 14 | "DOM" | ||
| 15 | ] | ||
| 16 | }, | ||
| 17 | "include": [ | ||
| 18 | "server.ts" | ||
| 19 | ] | ||
| 20 | } | ||