History

Merge pull request #3 from RhysSullivan/feat/cloudflare-sandbox-runtime

Add Cloudflare Workers sandbox runtime for isolated code execution

Rhys Sullivan committed 9 months ago

Commit ae1b0332 parents 9f5774e 0b5549c42 files · +4059 −731Browse files

Unified · Split

42 files
  1. .gitignore +9 −0
  2. bun.lock +151 −2
  3. dev.ts +24 −6
  4. executor/apps/web/src/components/code-editor.tsx +53 −11
  5. executor/apps/web/src/components/tools-view.tsx +485 −348
  6. executor/bun.lock +11 −0
  7. executor/convex/_generated/api.d.ts +97 −0
  8. executor/convex/database.ts +168 −0
  9. executor/convex/executor.ts +61 −1
  10. executor/convex/executorNode.ts +218 −61
  11. executor/convex/http.ts +0 −85
  12. executor/convex/runtimeCallbacks.ts +88 −0
  13. executor/convex/schema.ts +26 −0
  14. executor/lib/adapters/in-process-execution-adapter.test.ts +1 −1
  15. executor/lib/adapters/in_process_execution_adapter.ts +14 −2
  16. executor/lib/execution_constants.ts +1 −0
  17. executor/lib/openapi-cache.test.ts +2 −1
  18. executor/lib/runtimes/cloudflare-worker-loader-runtime.test.ts +182 −0
  19. executor/lib/runtimes/cloudflare_worker_loader_runtime.ts +130 −0
  20. executor/lib/runtimes/runtime-core.test.ts +3 −1
  21. executor/lib/runtimes/runtime_catalog.ts +80 −0
  22. executor/lib/runtimes/runtime_core.ts +101 −103
  23. executor/lib/runtimes/transpile.ts +78 −0
  24. executor/lib/tool-sources.test.ts +84 −0
  25. executor/lib/tool_sources.ts +418 −9
  26. executor/lib/typechecker.test.ts +41 −1
  27. executor/lib/typechecker.ts +139 −98
  28. executor/lib/types.ts +25 −1
  29. executor/package.json +1 −0
  30. executor/packages/sandbox-host/.dev.vars.example +2 −0
  31. executor/packages/sandbox-host/package.json +20 −0
  32. executor/packages/sandbox-host/src/index.ts +405 −0
  33. executor/packages/sandbox-host/src/isolate/globals.isolate.js +2 −0
  34. executor/packages/sandbox-host/src/isolate/harness.isolate.js +108 −0
  35. executor/packages/sandbox-host/tsconfig.json +17 −0
  36. executor/packages/sandbox-host/wrangler.jsonc +28 −0
  37. package.json +3 −0
  38. sources/package.json +16 −0
  39. sources/scraper.ts +384 −0
  40. sources/server.ts +192 −0
  41. sources/supplements.json +171 −0
  42. sources/tsconfig.json +20 −0
.gitignore+9−0
@@ -39,6 +39,15 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json · 38 unmodified lines
39convex_local_backend.sqlite339convex_local_backend.sqlite3
40convex_local_storage/40convex_local_storage/
41 41
42# Local OpenAPI sources service state
43sources/data/*.sqlite
44sources/data/*.sqlite-shm
45sources/data/*.sqlite-wal
46
47# Cloudflare Workers local state
48.wrangler/
49.dev.vars
50
42# Generated push env file51# Generated push env file
43executor/.env.executor-push52executor/.env.executor-push
44 53
bun.lock+151−2
@@ -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=="],
dev.ts+24−6
@@ -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 concurrently10 * 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
21const PID_FILE = join(import.meta.dir, ".dev.pids");23const PID_FILE = join(import.meta.dir, ".dev.pids");
22 24
23const colors = {25const colors = {
26 sources: "\x1b[33m", // yellow
24 convex: "\x1b[36m", // cyan27 convex: "\x1b[36m", // cyan
25 web: "\x1b[34m", // blue28 web: "\x1b[34m", // blue
29 sandbox: "\x1b[91m", // bright red
26 assistant: "\x1b[32m", // green30 assistant: "\x1b[32m", // green
27 bot: "\x1b[35m", // magenta31 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
119const EXECUTOR_WEB_PORT = Number(Bun.env.EXECUTOR_WEB_PORT ?? 4312);123const EXECUTOR_WEB_PORT = Number(Bun.env.EXECUTOR_WEB_PORT ?? 4312);
120const DEV_PORTS = [3000, EXECUTOR_WEB_PORT];124const SANDBOX_PORT = Number(Bun.env.SANDBOX_PORT ?? 8787);
125const DEV_PORTS = [3000, EXECUTOR_WEB_PORT, SANDBOX_PORT];
121 126
122async function killStaleProcesses() {127async function killStaleProcesses() {
123 let killed = 0;128 let killed = 0;
@@ -164,6 +169,11 @@ const urls = resolveExecutorUrls(); · 40 unmodified lines
164console.log(prefix("convex", `Using Convex URL: ${urls.convexUrl}`));169console.log(prefix("convex", `Using Convex URL: ${urls.convexUrl}`));
165console.log(prefix("convex", `Using executor HTTP URL: ${urls.executorUrl}`));170console.log(prefix("convex", `Using executor HTTP URL: ${urls.executorUrl}`));
166 171
172// 1. Sources catalog API
173spawnService("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)
168spawnService("convex", [178spawnService("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 parallel185// 3. CF sandbox host worker (local wrangler dev for iterating on the worker)
186spawnService("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
176spawnService("web", ["bun", "run", "dev"], {194spawnService("web", ["bun", "run", "dev"], {
177 cwd: "./executor/apps/web",195 cwd: "./executor/apps/web",
178});196});
executor/apps/web/src/components/code-editor.tsx+53−11
@@ -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) */
118const OPENAPI_HELPER_TYPES = `118const OPENAPI_HELPER_TYPES = `
119type _OrEmpty<T> = [T] extends [never] ? {} : T;119type _Normalize<T> = Exclude<T, undefined | null>;
120type _OrEmpty<T> = [_Normalize<T>] extends [never] ? {} : _Normalize<T>;
120type _Simplify<T> = { [K in keyof T]: T[K] } & {};121type _Simplify<T> = { [K in keyof T]: T[K] } & {};
122type _ParamsOf<Op> =
123 Op extends { parameters: infer P } ? P :
124 Op extends { parameters?: infer P } ? P :
125 never;
126type _ParamAt<Op, K extends "query" | "path" | "header" | "cookie"> =
127 _ParamsOf<Op> extends { [P in K]?: infer V } ? V : never;
128type _BodyOf<Op> =
129 Op extends { requestBody?: infer B } ? B :
130 Op extends { requestBody: infer B } ? B :
131 never;
132type _BodyContent<B> =
133 B extends { content: infer C }
134 ? C extends Record<string, infer V> ? V : never
135 : never;
121type ToolInput<Op> = _Simplify<136type 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>;
126type ToolOutput<Op> =143type _ResponsesOf<Op> = Op extends { responses: infer R } ? R : never;
127 Op extends { responses: { 200: { content: { "application/json": infer R } } } } ? R :144type _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 :146type _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;
151type _HasStatus<Op, Code extends PropertyKey> =
152 [_RespAt<Op, Code>] extends [never] ? false : true;
153type _PayloadAt<Op, Code extends PropertyKey> =
154 Code extends 204 | 205
155 ? (_HasStatus<Op, Code> extends true ? void : never)
156 : _ResponsePayload<_RespAt<Op, Code>>;
157type _FirstKnown<T extends readonly unknown[]> =
158 T extends readonly [infer H, ...infer Rest]
159 ? [H] extends [never] ? _FirstKnown<Rest> : H
160 : unknown;
161type 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
135function generateToolsDts(tools: ToolDescriptor[], dtsSources: Set<string>): string {177function generateToolsDts(tools: ToolDescriptor[], dtsSources: Set<string>): string {
executor/apps/web/src/components/tools-view.tsx+485−348
@@ -1,6 +1,6 @@
1"use client";1"use client";
2 2
3import { useState } from "react";3import { useRef, useState } from "react";
4import {4import {
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";
15import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";16import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
16import { Button } from "@/components/ui/button";17import { Button } from "@/components/ui/button";
@@ -56,158 +57,27 @@ import type { · 39 unmodified lines
56import { parse as parseDomain } from "tldts";57import { parse as parseDomain } from "tldts";
57import { toast } from "sonner";58import { toast } from "sonner";
58import { cn } from "@/lib/utils";59import { cn } from "@/lib/utils";
60import { Streamdown } from "streamdown";
59 61
60// ── API Presets ──62interface CatalogCollectionItem {
61 63 id: string;
62interface 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
74const API_PRESETS: ApiPreset[] = [74interface 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. */
213function faviconForUrl(url: string | undefined | null): string | null {83function 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
223function 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
233function getSourceFavicon(source: ToolSourceRecord): string | null {93function 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
110function 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
243function sourceKeyForSource(source: ToolSourceRecord): string | null {126function sourceKeyForSource(source: ToolSourceRecord): string | null {
@@ -366,6 +249,39 @@ function inferNameFromUrl(url: string): string { · 122 unmodified lines
366 }249 }
367}250}
368 251
252function 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
262function 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
279function catalogSourceName(item: CatalogCollectionItem): string {
280 const owner = sanitizeSourceName(item.providerName || "catalog");
281 const title = sanitizeSourceName(item.name);
282 return sanitizeSourceName(`${owner}-${title}`);
283}
284
369function AddSourceDialog({285function 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 <Select562 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 Server623 <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 Spec626 </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 GraphQL629 <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 <Input638 >
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 <Input653 {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 <Input736 <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 <Select754 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 <Input763 <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 <ChevronRight776 <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 <img784 <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">
executor/bun.lock+11−0
@@ -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}
executor/convex/_generated/api.d.ts+97−0
@@ -276,6 +276,47 @@ export declare const api: { · 275 unmodified lines
276 any276 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 any562 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 any605 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 any658 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 any717 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 any762 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 any800 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",
executor/convex/database.ts+168−0
@@ -16,6 +16,11 @@ const completedTaskStatusValidator = v.union( · 15 unmodified lines
16 v.literal("denied"),16 v.literal("denied"),
17);17);
18const approvalStatusValidator = v.union(v.literal("pending"), v.literal("approved"), v.literal("denied"));18const approvalStatusValidator = v.union(v.literal("pending"), v.literal("approved"), v.literal("denied"));
19const terminalToolCallStatusValidator = v.union(
20 v.literal("completed"),
21 v.literal("failed"),
22 v.literal("denied"),
23);
19const policyDecisionValidator = v.union(v.literal("allow"), v.literal("require_approval"), v.literal("deny"));24const policyDecisionValidator = v.union(v.literal("allow"), v.literal("require_approval"), v.literal("deny"));
20const credentialScopeValidator = v.union(v.literal("workspace"), v.literal("actor"));25const credentialScopeValidator = v.union(v.literal("workspace"), v.literal("actor"));
21const credentialProviderValidator = v.union(26const credentialProviderValidator = v.union(
@@ -127,6 +132,23 @@ function mapApproval(doc: Doc<"approvals">) { · 105 unmodified lines
127 };132 };
128}133}
129 134
135function 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
130function mapPolicy(doc: Doc<"accessPolicies">) {152function 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
334async 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
312export const createTask = internalMutation({345export 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
652export 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
686export 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
697export 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
728export 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
763export 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
610export const bootstrapAnonymousSession = internalMutation({778export 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) => {
executor/convex/executor.ts+61−1
@@ -5,6 +5,7 @@ import type { MutationCtx } from "./_generated/server"; · 4 unmodified lines
5import { internalMutation } from "./_generated/server";5import { internalMutation } from "./_generated/server";
6import { workspaceMutation } from "../lib/functionBuilders";6import { workspaceMutation } from "../lib/functionBuilders";
7import { actorIdForAccount } from "../lib/identity";7import { actorIdForAccount } from "../lib/identity";
8import { isKnownRuntimeId } from "../lib/runtimes/runtime_catalog";
8import type { ApprovalRecord, TaskRecord } from "../lib/types";9import type { ApprovalRecord, TaskRecord } from "../lib/types";
9 10
10const DEFAULT_TIMEOUT_MS = 300_000;11const 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
25function 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
24async function createTaskRecord(33async 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
248export 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});
executor/convex/executorNode.ts+218−61
@@ -7,8 +7,15 @@ import { action, internalAction } from "./_generated/server"; · 6 unmodified lines
7import type { ActionCtx } from "./_generated/server";7import type { ActionCtx } from "./_generated/server";
8import { InProcessExecutionAdapter } from "../lib/adapters/in_process_execution_adapter";8import { InProcessExecutionAdapter } from "../lib/adapters/in_process_execution_adapter";
9import { resolveCredentialPayload } from "../lib/credential_providers";9import { resolveCredentialPayload } from "../lib/credential_providers";
10import { APPROVAL_DENIED_PREFIX } from "../lib/execution_constants";10import { APPROVAL_DENIED_PREFIX, APPROVAL_PENDING_PREFIX } from "../lib/execution_constants";
11import { actorIdForAccount } from "../lib/identity";11import { actorIdForAccount } from "../lib/identity";
12import { dispatchCodeWithCloudflareWorkerLoader } from "../lib/runtimes/cloudflare_worker_loader_runtime";
13import {
14 CLOUDFLARE_WORKER_LOADER_RUNTIME_ID,
15 isCloudflareWorkerLoaderConfigured,
16 isKnownRuntimeId,
17 LOCAL_BUN_RUNTIME_ID,
18} from "../lib/runtimes/runtime_catalog";
12import { runCodeWithAdapter } from "../lib/runtimes/runtime_core";19import { runCodeWithAdapter } from "../lib/runtimes/runtime_core";
13import { createDiscoverTool } from "../lib/tool_discovery";20import { createDiscoverTool } from "../lib/tool_discovery";
14import {21import {
@@ -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
266async function sleep(ms: number): Promise<void> {
267 await new Promise((resolve) => setTimeout(resolve, ms));
268}
269
270const baseTools = new Map<string, ToolDefinition>(DEFAULT_TOOLS.map((tool) => [tool.path, tool]));274const baseTools = new Map<string, ToolDefinition>(DEFAULT_TOOLS.map((tool) => [tool.path, tool]));
271interface DtsStorageEntry {275interface DtsStorageEntry {
272 sourceKey: string;276 sourceKey: string;
@@ -293,21 +297,6 @@ async function publish( · 20 unmodified lines
293 });297 });
294}298}
295 299
296async 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
841async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCallRequest): Promise<unknown> {830async 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,
executor/convex/http.ts+0−85
@@ -9,7 +9,6 @@ import type { AnonymousContext, PendingApprovalRecord, TaskRecord, ToolDescripto · 8 unmodified lines
9import type { Id } from "./_generated/dataModel";9import type { Id } from "./_generated/dataModel";
10 10
11const http = httpRouter();11const http = httpRouter();
12const internalToken = process.env.EXECUTOR_INTERNAL_TOKEN ?? null;
13 12
14function getMcpAuthorizationServer(): string | null {13function getMcpAuthorizationServer(): string | null {
15 return process.env.MCP_AUTHORIZATION_SERVER14 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
124function 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
131function 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
146const mcpHandler = httpAction(async (ctx, request) => {123const 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
316const 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
372authKit.registerRoutes(http);293authKit.registerRoutes(http);
373registerStripeRoutes(http, components.stripe, {294registerStripeRoutes(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
380http.route({ path: "/.well-known/oauth-protected-resource", method: "GET", handler: oauthProtectedResourceHandler });301http.route({ path: "/.well-known/oauth-protected-resource", method: "GET", handler: oauthProtectedResourceHandler });
381http.route({ path: "/.well-known/oauth-authorization-server", method: "GET", handler: oauthAuthorizationServerProxyHandler });302http.route({ path: "/.well-known/oauth-authorization-server", method: "GET", handler: oauthAuthorizationServerProxyHandler });
382 303
383http.route({
384 pathPrefix: "/internal/runs/",
385 method: "POST",
386 handler: internalRunsHandler,
387});
388
389export default http;304export default http;
executor/convex/runtimeCallbacks.ts+88−0
@@ -0,0 +1,88 @@
1import { v } from "convex/values";
2import { internal } from "./_generated/api";
3import { action, mutation } from "./_generated/server";
4import type { ToolCallResult } from "../lib/types";
5
6function 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
16export 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
35export 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
64export 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});
executor/convex/schema.ts+26−0
@@ -34,6 +34,13 @@ const taskStatus = v.union( · 33 unmodified lines
34 v.literal("denied"),34 v.literal("denied"),
35);35);
36const approvalStatus = v.union(v.literal("pending"), v.literal("approved"), v.literal("denied"));36const approvalStatus = v.union(v.literal("pending"), v.literal("approved"), v.literal("denied"));
37const 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);
37const policyDecision = v.union(v.literal("allow"), v.literal("require_approval"), v.literal("deny"));44const policyDecision = v.union(v.literal("allow"), v.literal("require_approval"), v.literal("deny"));
38const credentialScope = v.union(v.literal("workspace"), v.literal("actor"));45const credentialScope = v.union(v.literal("workspace"), v.literal("actor"));
39const credentialProvider = v.union(46const 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)
executor/lib/adapters/in-process-execution-adapter.test.ts+1−1
@@ -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});
executor/lib/adapters/in_process_execution_adapter.ts+14−2
@@ -1,4 +1,4 @@
1import { APPROVAL_DENIED_PREFIX } from "../execution_constants";1import { APPROVAL_DENIED_PREFIX, APPROVAL_PENDING_PREFIX } from "../execution_constants";
2import type {2import 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 }
executor/lib/execution_constants.ts+1−0
@@ -1,2 +1,3 @@
1export const APPROVAL_DENIED_PREFIX = "APPROVAL_DENIED:";1export const APPROVAL_DENIED_PREFIX = "APPROVAL_DENIED:";
2export const APPROVAL_PENDING_PREFIX = "APPROVAL_PENDING:";
2export const TASK_TIMEOUT_MARKER = "TASK_TIMEOUT";3export const TASK_TIMEOUT_MARKER = "TASK_TIMEOUT";
executor/lib/openapi-cache.test.ts+2−1
@@ -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
executor/lib/runtimes/cloudflare-worker-loader-runtime.test.ts+182−0
@@ -0,0 +1,182 @@
1import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2import { dispatchCodeWithCloudflareWorkerLoader } from "./cloudflare_worker_loader_runtime";
3
4let fakeHostServer: ReturnType<typeof Bun.serve>;
5let fakeCallbackServer: ReturnType<typeof Bun.serve>;
6
7const AUTH_TOKEN = "test-sandbox-token";
8const CALLBACK_TOKEN = "test-callback-token";
9
10let hostResponseStatus = 202;
11let hostResponseBody: Record<string, unknown> = { accepted: true, dispatchId: "dispatch_test" };
12type HostRequestBody = {
13 taskId: string;
14 code: string;
15 timeoutMs: number;
16 callback: { convexUrl: string; internalSecret: string };
17};
18
19let lastHostRequestBody: HostRequestBody | null = null;
20
21beforeAll(() => {
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
52afterAll(() => {
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
62describe("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
138describe("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});
executor/lib/runtimes/cloudflare_worker_loader_runtime.ts+130−0
@@ -0,0 +1,130 @@
1"use node";
2
3import { Result } from "better-result";
4import type { SandboxExecutionRequest } from "../types";
5import { getCloudflareWorkerLoaderConfig } from "./runtime_catalog";
6import { 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 */
34export interface CloudflareDispatchResult {
35 ok: true;
36 accepted: true;
37 dispatchId: string;
38 durationMs: number;
39}
40
41export interface CloudflareDispatchError {
42 ok: false;
43 error: string;
44 durationMs: number;
45}
46
47export 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}
executor/lib/runtimes/runtime-core.test.ts+3−1
@@ -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 },
executor/lib/runtimes/runtime_catalog.ts+80−0
@@ -0,0 +1,80 @@
1// ── Runtime ID constants ──────────────────────────────────────────────────────
2
3export const LOCAL_BUN_RUNTIME_ID = "local-bun";
4export const CLOUDFLARE_WORKER_LOADER_RUNTIME_ID = "cloudflare-worker-loader";
5
6const KNOWN_RUNTIME_IDS = new Set([
7 LOCAL_BUN_RUNTIME_ID,
8 CLOUDFLARE_WORKER_LOADER_RUNTIME_ID,
9]);
10
11export function isKnownRuntimeId(runtimeId: string): boolean {
12 return KNOWN_RUNTIME_IDS.has(runtimeId);
13}
14
15// ── Cloudflare Worker Loader config ──────────────────────────────────────────
16
17export 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 */
34export 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 */
45export 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}
executor/lib/runtimes/runtime_core.ts+101−103
@@ -5,50 +5,20 @@ · 4 unmodified lines
5// the core helpers here (formatArgs, createToolsProxy, console proxy, execution5// 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.
7import { APPROVAL_DENIED_PREFIX, TASK_TIMEOUT_MARKER } from "../execution_constants";7import { APPROVAL_DENIED_PREFIX, TASK_TIMEOUT_MARKER } from "../execution_constants";
8import { Result } from "better-result";
8import { Script, createContext } from "node:vm";9import { Script, createContext } from "node:vm";
9import type {10import type {
10 ExecutionAdapter,11 ExecutionAdapter,
11 SandboxExecutionRequest,12 SandboxExecutionRequest,
12 SandboxExecutionResult,13 SandboxExecutionResult,
13} from "../types";14} from "../types";
14 15import { transpileForRuntime } from "./transpile";
15async 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
43function formatArgs(args: unknown[]): string {17function formatArgs(args: unknown[]): string {
44 return args18 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
32async function sleep(ms: number): Promise<void> {
33 await new Promise((resolve) => setTimeout(resolve, ms));
34}
35
62function createToolsProxy(36function 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. */
85function 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
101export async function runCodeWithAdapter(108export 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}
executor/lib/runtimes/transpile.ts+78−0
@@ -0,0 +1,78 @@
1"use node";
2
3import { Result, TaggedError } from "better-result";
4
5// ---------------------------------------------------------------------------
6// Errors
7// ---------------------------------------------------------------------------
8
9export class TranspileError extends TaggedError("TranspileError")<{
10 message: string;
11}>() {}
12
13// ---------------------------------------------------------------------------
14// TypeScript module loader
15// ---------------------------------------------------------------------------
16
17let cachedTypeScript: typeof import("typescript") | null | undefined;
18
19function 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 */
41export 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}
executor/lib/tool-sources.test.ts+84−0
@@ -321,6 +321,90 @@ test("compiled source artifacts can be materialized and executed", async () => { · 320 unmodified lines
321 }321 }
322});322});
323 323
324test("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
324test("parseGraphqlOperationPaths handles aliases and fragments", async () => {408test("parseGraphqlOperationPaths handles aliases and fragments", async () => {
325 const parsed = parseGraphqlOperationPaths(409 const parsed = parseGraphqlOperationPaths(
326 "linear",410 "linear",
executor/lib/tool_sources.ts+418−9
@@ -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 | OpenApiToolSourceConfig66 | OpenApiToolSourceConfig
63 | GraphqlToolSourceConfig;67 | GraphqlToolSourceConfig;
64 68
69interface 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
82const POSTMAN_SPEC_PREFIX = "postman:";
83const DEFAULT_POSTMAN_PROXY_URL = "https://www.postman.com/_api/ws/proxy";
84const POSTMAN_TEMPLATE_PATTERN = /\{\{([^{}]+)\}\}/g;
85
65function sanitizeSegment(value: string): string {86function sanitizeSegment(value: string): string {
66 const cleanedBase = value87 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
689export 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
702function 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
709function 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
718function 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
729function 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
739function 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
744async 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
668function buildOpenApiUrl(844function 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 > 01283 const displayArgsType = argPreviewKeys.length > 0
1111 ? compactArgKeysHint(argPreviewKeys)1284 ? compactArgKeysHint(argPreviewKeys)
1112 : usesGeneratedTypes1285 : 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]?.approval1288 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
1357function 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
1381function 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
1398function 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
1412function 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
1425function 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
1438function 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
1459async 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
1188async function loadOpenApiTools(config: OpenApiToolSourceConfig): Promise<ToolDefinition[]> {1577async 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_TOOLS2297 * 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 {
executor/lib/typechecker.test.ts+41−1
@@ -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 = `
322export 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
executor/lib/typechecker.ts+139−98
@@ -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
16import { Result } from "better-result";
16import type { ToolDescriptor } from "./types";17import type { ToolDescriptor } from "./types";
17 18
18// ---------------------------------------------------------------------------19// ---------------------------------------------------------------------------
@@ -23,11 +24,8 @@ let cachedTypeScript: typeof import("typescript") | null | undefined; · 4 unmodified lines
23 24
24function getTypeScriptModule(): typeof import("typescript") | null {25function 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
59function safeTypeExpression(raw: string | undefined, fallback: string): string {55function 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
65const OPENAPI_HELPER_TYPES = `61const OPENAPI_HELPER_TYPES = `
66type _OrEmpty<T> = [T] extends [never] ? {} : T;62type _Normalize<T> = Exclude<T, undefined | null>;
63type _OrEmpty<T> = [_Normalize<T>] extends [never] ? {} : _Normalize<T>;
67type _Simplify<T> = { [K in keyof T]: T[K] } & {};64type _Simplify<T> = { [K in keyof T]: T[K] } & {};
65type _ParamsOf<Op> =
66 Op extends { parameters: infer P } ? P :
67 Op extends { parameters?: infer P } ? P :
68 never;
69type _ParamAt<Op, K extends "query" | "path" | "header" | "cookie"> =
70 _ParamsOf<Op> extends { [P in K]?: infer V } ? V : never;
71type _BodyOf<Op> =
72 Op extends { requestBody?: infer B } ? B :
73 Op extends { requestBody: infer B } ? B :
74 never;
75type _BodyContent<B> =
76 B extends { content: infer C }
77 ? C extends Record<string, infer V> ? V : never
78 : never;
68type ToolInput<Op> = _Simplify<79type 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>;
73type ToolOutput<Op> =86type _ResponsesOf<Op> = Op extends { responses: infer R } ? R : never;
74 Op extends { responses: { 200: { content: { "application/json": infer R } } } } ? R :87type _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 :89type _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;
94type _HasStatus<Op, Code extends PropertyKey> =
95 [_RespAt<Op, Code>] extends [never] ? false : true;
96type _PayloadAt<Op, Code extends PropertyKey> =
97 Code extends 204 | 205
98 ? (_HasStatus<Op, Code> extends true ? void : never)
99 : _ResponsePayload<_RespAt<Op, Code>>;
100type _FirstKnown<T extends readonly unknown[]> =
101 T extends readonly [infer H, ...infer Rest]
102 ? [H] extends [never] ? _FirstKnown<Rest> : H
103 : unknown;
104type 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
82function stripExportKeywordsForTypechecker(dts: string): string {120function 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
274const TYPECHECK_OK: TypecheckResult = { ok: true, errors: [] };
275
236let warnedMissingCompilerHostSupport = false;276let warnedMissingCompilerHostSupport = false;
237let warnedSemanticFallback = false;277let 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
307function 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 since381 // 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}
executor/lib/types.ts+25−1
@@ -4,6 +4,7 @@ import type { Id } from "../convex/_generated/dataModel"; · 3 unmodified lines
4 4
5export type TaskStatus = "queued" | "running" | "completed" | "failed" | "timed_out" | "denied";5export type TaskStatus = "queued" | "running" | "completed" | "failed" | "timed_out" | "denied";
6export type ApprovalStatus = "pending" | "approved" | "denied";6export type ApprovalStatus = "pending" | "approved" | "denied";
7export type ToolCallStatus = "requested" | "pending_approval" | "completed" | "failed" | "denied";
7export type PolicyDecision = "allow" | "require_approval" | "deny";8export type PolicyDecision = "allow" | "require_approval" | "deny";
8export type CredentialScope = "workspace" | "actor";9export type CredentialScope = "workspace" | "actor";
9export type CredentialProvider = "managed" | "workos-vault";10export 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
59export 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
58export interface AccessPolicyRecord {74export 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
168export type ToolCallResult =184export 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
172export type RuntimeOutputStream = "stdout" | "stderr";196export type RuntimeOutputStream = "stdout" | "stderr";
173 197
executor/package.json+1−0
@@ -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 }
executor/packages/sandbox-host/.dev.vars.example+2−0
@@ -0,0 +1,2 @@
1# Shared-secret bearer token — must match CLOUDFLARE_SANDBOX_AUTH_TOKEN on the executor side.
2AUTH_TOKEN=dev-sandbox-token
executor/packages/sandbox-host/package.json+20−0
@@ -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}
executor/packages/sandbox-host/src/index.ts+405−0
@@ -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
37import { Result } from "better-result";
38import { WorkerEntrypoint } from "cloudflare:workers";
39import { ConvexHttpClient } from "convex/browser";
40
41const 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)
52import GLOBALS_MODULE from "./isolate/globals.isolate.js";
53// @ts-expect-error — wrangler Text module import (no TS declarations)
54import HARNESS_CODE from "./isolate/harness.isolate.js";
55
56// ── Types ────────────────────────────────────────────────────────────────────
57
58interface Env {
59 LOADER: WorkerLoader;
60 AUTH_TOKEN: string;
61}
62
63/** Dynamic Worker Loader binding — provided by the `worker_loaders` config. */
64interface WorkerLoader {
65 get(id: string, getCode: () => Promise<WorkerCode>): WorkerStub;
66}
67
68interface 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
77interface WorkerStub {
78 getEntrypoint(name?: string, options?: { props?: unknown }): EntrypointStub;
79}
80
81interface EntrypointStub {
82 fetch(input: string | Request, init?: RequestInit): Promise<Response>;
83}
84
85interface RunRequest {
86 taskId: string;
87 code: string;
88 timeoutMs: number;
89 callback: {
90 convexUrl: string;
91 internalSecret: string;
92 };
93}
94
95interface RunResult {
96 status: "completed" | "failed" | "timed_out" | "denied";
97 stdout: string;
98 stderr: string;
99 error?: string;
100 exitCode?: number;
101}
102
103interface RunDispatchResponse {
104 accepted: true;
105 dispatchId: string;
106}
107
108interface ToolCallResult {
109 ok: true | false;
110 value?: unknown;
111 error?: string;
112 kind?: "pending" | "denied" | "failed";
113 approvalId?: string;
114 retryAfterMs?: number;
115}
116
117interface 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. */
126function 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
149const failedResult = (error: string): RunResult => ({
150 status: "failed",
151 stdout: "",
152 stderr: "",
153 error,
154});
155
156async 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
169export 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 */
240function buildUserModule(userCode: string): string {
241 return `export async function run(tools, console) {\n"use strict";\n${userCode}\n}\n`;
242}
243
244async 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
308async 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
346export 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};
executor/packages/sandbox-host/src/isolate/globals.isolate.js+2−0
@@ -0,0 +1,2 @@
1// Captured before user code module is evaluated (this module is imported first).
2export const ResponseJson = Response.json.bind(Response);
executor/packages/sandbox-host/src/isolate/harness.isolate.js+108−0
@@ -0,0 +1,108 @@
1import { ResponseJson as _ResponseJson } from "./globals.js";
2import { run } from "./user-code.js";
3
4const APPROVAL_DENIED_PREFIX = "APPROVAL_DENIED:";
5
6function 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
19async function sleep(ms) {
20 await new Promise((resolve) => setTimeout(resolve, ms));
21}
22
23function 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
51export 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};
executor/packages/sandbox-host/tsconfig.json+17−0
@@ -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}
executor/packages/sandbox-host/wrangler.jsonc+28−0
@@ -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}
package.json+3−0
@@ -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",
sources/package.json+16−0
@@ -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}
sources/scraper.ts+384−0
@@ -0,0 +1,384 @@
1import { Database } from "bun:sqlite";
2import { fileURLToPath } from "node:url";
3
4// ---------------------------------------------------------------------------
5// List.json shape (from APIs.guru / our fork)
6// ---------------------------------------------------------------------------
7
8interface ListOrigin {
9 format: string;
10 url: string;
11 version: string;
12}
13
14interface 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
25interface ListVersion {
26 added?: string;
27 updated?: string;
28 info: ListVersionInfo;
29 swaggerUrl: string;
30 swaggerYamlUrl?: string;
31 openapiVer?: string;
32 link?: string;
33}
34
35interface ListEntry {
36 added?: string;
37 preferred: string;
38 versions: Record<string, ListVersion>;
39}
40
41type ListJson = Record<string, ListEntry>;
42
43// ---------------------------------------------------------------------------
44// Exported types
45// ---------------------------------------------------------------------------
46
47export 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
59export interface SyncState {
60 inFlight: Promise<SyncResult> | null;
61 lastSyncedAt: number | null;
62 lastError: string | null;
63 lastCount: number;
64}
65
66export interface SyncResult {
67 count: number;
68 syncedAt: number;
69 trigger: string;
70}
71
72export interface SyncConfig {
73 listUrl: string;
74}
75
76// ---------------------------------------------------------------------------
77// Constants
78// ---------------------------------------------------------------------------
79
80const DEFAULT_LIST_URL = "https://api.apis.guru/v2/list.json";
81const SUPPLEMENTS_PATH = fileURLToPath(new URL("./supplements.json", import.meta.url));
82
83// ---------------------------------------------------------------------------
84// Text helpers
85// ---------------------------------------------------------------------------
86
87function normalizeText(value: string): string {
88 return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
89}
90
91function compactText(value: string): string {
92 return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
93}
94
95function 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
105interface 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
120function 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
163export 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
192export 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
197function 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
240async 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
252async 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
291export 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
323function 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
350export 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}
sources/server.ts+192−0
@@ -0,0 +1,192 @@
1#!/usr/bin/env bun
2
3import { mkdir } from "node:fs/promises";
4import { dirname } from "node:path";
5import { fileURLToPath } from "node:url";
6import {
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
19const PORT = Math.max(1, Math.min(65535, Number(process.env.SOURCES_PORT) || 4343));
20const DB_PATH = process.env.SOURCES_DB_PATH ?? fileURLToPath(new URL("./data/catalog.sqlite", import.meta.url));
21const SYNC_INTERVAL_MS = Math.max(60_000, Number(process.env.SOURCES_SYNC_INTERVAL_MS) || 6 * 60 * 60 * 1000);
22const LIST_URL = process.env.SOURCES_LIST_URL ?? "https://api.apis.guru/v2/list.json";
23const ALLOWED_ORIGINS = process.env.SOURCES_ALLOWED_ORIGINS?.split(",").map((s) => s.trim()).filter(Boolean) ?? [];
24const DEFAULT_LIMIT = 25;
25const MAX_LIMIT = 100;
26
27// ---------------------------------------------------------------------------
28// Helpers
29// ---------------------------------------------------------------------------
30
31function 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
38function parseSort(value: string | null): "popular" | "recent" {
39 return value === "recent" ? "recent" : "popular";
40}
41
42function 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
49function 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
70await mkdir(dirname(DB_PATH), { recursive: true });
71const db = createDb(DB_PATH);
72const syncConfig: SyncConfig = { listUrl: LIST_URL };
73const state: SyncState = {
74 inFlight: null,
75 lastSyncedAt: null,
76 lastError: null,
77 lastCount: rowCount(db),
78};
79
80// ---------------------------------------------------------------------------
81// CLI: --sync-only
82// ---------------------------------------------------------------------------
83
84if (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
98const 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
164console.log(`[sources] running on http://127.0.0.1:${server.port}`);
165console.log(`[sources] db: ${DB_PATH}`);
166console.log(`[sources] endpoints: GET /collections, GET /health, POST /admin/sync`);
167
168// Non-blocking startup sync
169void 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
174const 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
181let shuttingDown = false;
182const shutdown = () => {
183 if (shuttingDown) return;
184 shuttingDown = true;
185 clearInterval(syncInterval);
186 server.stop();
187 db.close();
188 process.exit(0);
189};
190
191process.on("SIGINT", shutdown);
192process.on("SIGTERM", shutdown);
sources/supplements.json+171−0
@@ -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}
sources/tsconfig.json+20−0
@@ -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}