From b7b9b1aab4242d50b6cddc08a5efa49f8af71728 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Wed, 10 Dec 2025 12:32:13 +0100 Subject: [PATCH 1/5] server package --- bun.lock | 59 ++- packages/server/.gitignore | 5 + packages/server/README.md | 1 + packages/server/package.json | 43 +++ packages/server/scripts/build.ts | 91 +++++ packages/server/src/agent-deployment.ts | 236 ++++++++++++ packages/server/src/chat.ts | 278 ++++++++++++++ packages/server/src/cli.ts | 78 ++++ packages/server/src/logger.ts | 29 ++ packages/server/src/postgres.ts | 225 +++++++++++ packages/server/src/server.ts | 474 ++++++++++++++++++++++++ packages/server/tsconfig.json | 10 + packages/server/tsdown.config.ts | 103 +++++ 13 files changed, 1617 insertions(+), 15 deletions(-) create mode 100644 packages/server/.gitignore create mode 100644 packages/server/README.md create mode 100644 packages/server/package.json create mode 100644 packages/server/scripts/build.ts create mode 100644 packages/server/src/agent-deployment.ts create mode 100644 packages/server/src/chat.ts create mode 100644 packages/server/src/cli.ts create mode 100644 packages/server/src/logger.ts create mode 100644 packages/server/src/postgres.ts create mode 100644 packages/server/src/server.ts create mode 100644 packages/server/tsconfig.json create mode 100644 packages/server/tsdown.config.ts diff --git a/bun.lock b/bun.lock index 51f8e99..96b4b40 100644 --- a/bun.lock +++ b/bun.lock @@ -286,6 +286,25 @@ "blink": ">= 1", }, }, + "packages/server": { + "name": "blink-server", + "version": "0.0.7", + "bin": { + "blink-server": "dist/cli.js", + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@types/pg": "^8.11.10", + "@types/ws": "^8.5.13", + "boxen": "^8.0.1", + "chalk": "^5.4.1", + "commander": "^12.1.0", + "drizzle-orm": "^0.44.5", + "fetch-to-node": "^2.1.0", + "pg": "^8.16.0", + "ws": "^8.18.0", + }, + }, "packages/site": { "name": "@blink.so/site", "dependencies": { @@ -1896,6 +1915,8 @@ "blink": ["blink@workspace:packages/blink"], + "blink-server": ["blink-server@workspace:packages/server"], + "bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="], "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], @@ -1904,7 +1925,7 @@ "bowser": ["bowser@2.13.0", "", {}, "sha512-yHAbSRuT6LTeKi6k2aS40csueHqgAsFEgmrOsfRyFpJnFv5O2hl9FYmWEUZ97gZ/dG17U4IQQcTx4YAFYPuWRQ=="], - "boxen": ["boxen@7.1.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^7.0.1", "chalk": "^5.2.0", "cli-boxes": "^3.0.0", "string-width": "^5.1.2", "type-fest": "^2.13.0", "widest-line": "^4.0.1", "wrap-ansi": "^8.1.0" } }, "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog=="], + "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -1968,7 +1989,7 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "camelcase": ["camelcase@7.0.1", "", {}, "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw=="], + "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], "caniuse-lite": ["caniuse-lite@1.0.30001748", "", {}, "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w=="], @@ -2432,6 +2453,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fetch-to-node": ["fetch-to-node@2.1.0", "", {}, "sha512-Wq05j6LE1GrWpT2t1YbCkyFY6xKRJq3hx/oRJdWEJpZlik3g25MmdJS6RFm49iiMJw6zpZuBOrgihOgy2jGyAA=="], + "fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], @@ -4422,6 +4445,8 @@ "@types/tedious/@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], + "@types/update-notifier/boxen": ["boxen@7.1.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^7.0.1", "chalk": "^5.2.0", "cli-boxes": "^3.0.0", "string-width": "^5.1.2", "type-fest": "^2.13.0", "widest-line": "^4.0.1", "wrap-ansi": "^8.1.0" } }, "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog=="], + "@types/ws/@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], "@types/yauzl/@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], @@ -4452,15 +4477,11 @@ "blink/tsdown": ["tsdown@0.14.2", "", { "dependencies": { "ansis": "^4.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "debug": "^4.4.1", "diff": "^8.0.2", "empathic": "^2.0.0", "hookable": "^5.5.3", "rolldown": "latest", "rolldown-plugin-dts": "^0.15.8", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "tree-kill": "^1.2.2", "unconfig": "^7.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-6ThtxVZoTlR5YJov5rYvH8N1+/S/rD/pGfehdCLGznGgbxz+73EASV1tsIIZkLw2n+SXcERqHhcB/OkyxdKv3A=="], - "body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "boxen/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + "blink-server/@types/node": ["@types/node@22.18.8", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw=="], - "boxen/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + "blink-server/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "boxen/widest-line": ["widest-line@4.0.1", "", { "dependencies": { "string-width": "^5.0.1" } }, "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig=="], - - "boxen/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=="], + "body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "browserify-aes/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -4830,8 +4851,6 @@ "type-is/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], - "update-notifier/boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], - "update-notifier/is-in-ci": ["is-in-ci@1.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg=="], "update-notifier/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -5382,6 +5401,16 @@ "@types/tedious/@types/node/undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="], + "@types/update-notifier/boxen/camelcase": ["camelcase@7.0.1", "", {}, "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw=="], + + "@types/update-notifier/boxen/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@types/update-notifier/boxen/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + + "@types/update-notifier/boxen/widest-line": ["widest-line@4.0.1", "", { "dependencies": { "string-width": "^5.0.1" } }, "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig=="], + + "@types/update-notifier/boxen/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=="], + "@types/ws/@types/node/undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="], "@types/yauzl/@types/node/undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="], @@ -5404,6 +5433,8 @@ "bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "blink-server/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "blink/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="], "blink/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="], @@ -5462,8 +5493,6 @@ "blink/tsdown/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "boxen/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "builder-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "builder-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -5816,8 +5845,6 @@ "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "update-notifier/boxen/camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="], "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="], @@ -5958,6 +5985,8 @@ "@npmcli/move-file/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@types/update-notifier/boxen/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "bl/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], diff --git a/packages/server/.gitignore b/packages/server/.gitignore new file mode 100644 index 0000000..5d625fe --- /dev/null +++ b/packages/server/.gitignore @@ -0,0 +1,5 @@ +dist/ +node_modules/ +.env +.env.local + diff --git a/packages/server/README.md b/packages/server/README.md new file mode 100644 index 0000000..7d8569d --- /dev/null +++ b/packages/server/README.md @@ -0,0 +1 @@ +🚧🚧🚧 diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000..1a39000 --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,43 @@ +{ + "name": "blink-server", + "description": "Agents as a Service", + "version": "0.0.7", + "author": { + "name": "Coder", + "email": "support@coder.com", + "url": "https://coder.com" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/coder/blink" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "bin": { + "blink-server": "dist/cli.js" + }, + "type": "module", + "scripts": { + "build": "bun scripts/build.ts", + "typecheck": "tsgo --noEmit" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@types/pg": "^8.11.10", + "@types/ws": "^8.5.13", + "chalk": "^5.4.1", + "boxen": "^8.0.1", + "commander": "^12.1.0", + "drizzle-orm": "^0.44.5", + "fetch-to-node": "^2.1.0", + "pg": "^8.16.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/packages/server/scripts/build.ts b/packages/server/scripts/build.ts new file mode 100644 index 0000000..8e2fe13 --- /dev/null +++ b/packages/server/scripts/build.ts @@ -0,0 +1,91 @@ +import { build } from "bun"; +import { execSync } from "child_process"; +import { cpSync, mkdirSync, rmSync, writeFileSync } from "fs"; +import { join } from "path"; + +const distDir = join(import.meta.dirname, "..", "dist"); +const repoRoot = join(import.meta.dirname, "..", "..", ".."); + +/** + * buildServer builds the CLI for the server. + */ +async function buildServer() { + await build({ + entrypoints: [join(__dirname, "..", "src", "cli.ts")], + outdir: "dist", + target: "node", + format: "esm", + minify: true, + }); +} + +/** + * buildNextSite builds the NextJS site and copies the necessary files to the dist directory. + */ +function buildNextSite() { + const sitePackage = join(repoRoot, "packages", "site"); + + execSync("bun run build", { + cwd: sitePackage, + stdio: "inherit", + env: { + ...process.env, + NODE_ENV: "production", + // This ensures the site is bundled alone. + NEXT_OUTPUT: "standalone", + }, + }); + + rmSync(join(distDir, "site"), { recursive: true, force: true }); + mkdirSync(join(distDir, "site"), { recursive: true }); + // This moves all of the compiled site and sources to run the server-side. + cpSync( + join(sitePackage, ".next", "standalone", "packages", "site", ".next"), + join(distDir, "site", ".next"), + { recursive: true } + ); + // This copies all of the static assets. + cpSync( + join(sitePackage, ".next", "static"), + join(distDir, "site", ".next", "static"), + { recursive: true } + ); + // This copies all public assets. + cpSync(join(sitePackage, "public"), join(distDir, "site", "public"), { + recursive: true, + }); + // This copies the required server node_modules. + cpSync( + join(sitePackage, ".next", "standalone", "node_modules"), + join(distDir, "site", "node_modules"), + { recursive: true } + ); + // Write minimal package.json for module.createRequire() to work. + writeFileSync( + join(distDir, "site", "package.json"), + JSON.stringify({ type: "module" }) + ); +} + +function copyMigrations() { + const databasePackage = join(repoRoot, "packages", "database"); + + rmSync(join(distDir, "migrations"), { recursive: true, force: true }); + cpSync(join(databasePackage, "migrations"), join(distDir, "migrations"), { + recursive: true, + }); +} + +console.time("buildServer"); +await buildServer(); +console.timeEnd("buildServer"); + +if (process.env.BUILD_SITE) { + console.time("buildNextSite"); + buildNextSite(); + console.timeEnd("buildNextSite"); +} + +console.time("copyMigrations"); +copyMigrations(); +console.timeEnd("copyMigrations"); diff --git a/packages/server/src/agent-deployment.ts b/packages/server/src/agent-deployment.ts new file mode 100644 index 0000000..a10d4bf --- /dev/null +++ b/packages/server/src/agent-deployment.ts @@ -0,0 +1,236 @@ +import type Querier from "@blink.so/database/querier"; +import type { AgentDeployment } from "@blink.so/database/schema"; +import { + InternalAPIServerListenPortEnvironmentVariable, + InternalAPIServerURLEnvironmentVariable, +} from "@blink.so/runtime/types"; +import { spawn } from "child_process"; +import { mkdir, writeFile } from "fs/promises"; +import { createServer } from "net"; +import { tmpdir } from "os"; +import { join } from "path"; + +interface DockerDeployOptions { + deployment: AgentDeployment; + querier: Querier; + baseUrl: string; + downloadFile: (id: string) => Promise<{ + stream: ReadableStream; + type: string; + name: string; + size: number; + }>; +} + +/** + * Janky Docker-based agent deployment for self-hosted + * This will download files, write them to a temp directory, + * and run them in a Docker container. + */ +export async function deployAgentWithDocker(opts: DockerDeployOptions) { + const { deployment, querier, baseUrl, downloadFile } = opts; + console.log(`Deploying agent ${deployment.agent_id} (${deployment.id})`); + + try { + await querier.updateAgentDeployment({ + id: deployment.id, + status: "deploying", + }); + + if (!deployment.output_files || deployment.output_files.length === 0) { + throw new Error("No output files provided"); + } + + // Create a temp directory for this deployment + const deploymentDir = join(tmpdir(), `blink-agent-${deployment.id}`); + await mkdir(deploymentDir, { recursive: true }); + + console.log(`Writing files to ${deploymentDir}`); + + // Download and write all files + for (const file of deployment.output_files) { + const fileData = await downloadFile(file.id); + const filePath = join(deploymentDir, file.path); + + // Create parent directories if needed + const parentDir = join(filePath, ".."); + await mkdir(parentDir, { recursive: true }); + + // Convert ReadableStream to Buffer + const reader = fileData.stream.getReader(); + const chunks: Uint8Array[] = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + const buffer = Buffer.concat(chunks); + + await writeFile(filePath, buffer); + console.log(`Wrote ${file.path} (${buffer.length} bytes)`); + } + + // Add the node wrapper runtime + const runtime = await import("@blink.so/runtime/node/wrapper"); + const wrapperPath = join(deploymentDir, "__wrapper.js"); + await writeFile(wrapperPath, runtime.default); + console.log(`Wrote __wrapper.js (runtime wrapper)`); + + // The original entrypoint becomes an env var for the wrapper + const originalEntrypoint = deployment.entrypoint; + const wrapperEntrypoint = "__wrapper.js"; + + // Get environment variables for the agent + const envs = await querier.selectAgentEnvironmentVariablesByAgentID({ + agentID: deployment.agent_id, + }); + const target = await querier.selectAgentDeploymentTargetByID( + deployment.target_id + ); + + // Find free ports for this agent (one for external access, one for internal API) + const externalPort = await findFreePort(); + const internalAPIPort = await findFreePort(); + + // Build Docker env args + const dockerEnvArgs: string[] = []; + // Wrapper runtime configuration + dockerEnvArgs.push("-e", `ENTRYPOINT=./${originalEntrypoint}`); + dockerEnvArgs.push( + "-e", + `${InternalAPIServerListenPortEnvironmentVariable}=${internalAPIPort}` + ); + dockerEnvArgs.push( + "-e", + `${InternalAPIServerURLEnvironmentVariable}=${baseUrl}` + ); + // Agent configuration + dockerEnvArgs.push("-e", `BLINK_REQUEST_URL=${baseUrl}`); + dockerEnvArgs.push("-e", `BLINK_REQUEST_ID=${target?.request_id}`); + dockerEnvArgs.push("-e", `PORT=${externalPort}`); + // User-defined environment variables + for (const envVar of envs) { + if (envVar.value !== null) { + dockerEnvArgs.push("-e", `${envVar.key}=${envVar.value}`); + } + } + + // Run docker container + // Mount the deployment directory as /app + // Expose the port so we can access the agent + const containerName = `blink-agent-${deployment.agent_id}`; + + // Stop and remove existing container if it exists + try { + await runCommand("docker", ["stop", containerName]); + await runCommand("docker", ["rm", containerName]); + } catch { + // Ignore errors if container doesn't exist + } + + const dockerArgs = [ + "run", + "-d", + "--name", + containerName, + "--restart", + "unless-stopped", + "--network", + "host", + "-v", + `${deploymentDir}:/app`, + "-w", + "/app", + ...dockerEnvArgs, + "node:22", + "node", + wrapperEntrypoint, + ]; + + console.log(`Running: docker ${dockerArgs.join(" ")}`); + const containerId = await runCommand("docker", dockerArgs); + + console.log(`Container started: ${containerId}`); + + // Update deployment status and set as active if target is production + await querier.tx(async (tx) => { + await tx.updateAgentDeployment({ + id: deployment.id, + status: "success", + direct_access_url: `http://localhost:${externalPort}`, + platform_metadata: { + type: "lambda", + arn: `container:${containerId.trim()}`, + }, + }); + + const deploymentTarget = await tx.selectAgentDeploymentTargetByID( + deployment.target_id + ); + // TODO: We should probably not have this hardcoded. + if (deploymentTarget && deploymentTarget.target === "production") { + await tx.updateAgent({ + id: deployment.agent_id, + active_deployment_id: deployment.id, + }); + } + }); + + console.log(`Deployment ${deployment.id} successful`); + } catch (error) { + console.error(`Deployment ${deployment.id} failed:`, error); + await querier.updateAgentDeployment({ + id: deployment.id, + status: "failed", + error_message: error instanceof Error ? error.message : String(error), + }); + throw error; + } +} + +function runCommand(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const proc = spawn(command, args); + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + proc.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + proc.on("close", (code) => { + if (code === 0) { + resolve(stdout); + } else { + reject(new Error(`Command failed with code ${code}: ${stderr}`)); + } + }); + + proc.on("error", (error) => { + reject(error); + }); + }); +} + +function findFreePort(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(); + server.listen(0, () => { + const address = server.address(); + if (address && typeof address !== "string") { + const port = address.port; + server.close(() => { + resolve(port); + }); + } else { + server.close(); + reject(new Error("Failed to get port")); + } + }); + server.on("error", reject); + }); +} diff --git a/packages/server/src/chat.ts b/packages/server/src/chat.ts new file mode 100644 index 0000000..18a3e14 --- /dev/null +++ b/packages/server/src/chat.ts @@ -0,0 +1,278 @@ +import type { StreamChatEvent } from "@blink.so/api"; +import { runChat } from "@blink.so/api/util/chat"; +import type Querier from "@blink.so/database/querier"; +import type { DBMessage } from "@blink.so/database/schema"; +import type { WebSocketServer } from "ws"; +import { WebSocket } from "ws"; + +class ChatSession { + private sseStreams: Set> = new Set(); + private streamingBuffer: string[] = []; + private streamAbortController?: AbortController; + private running = false; + + constructor(private id: string) {} + + addSSEStream(writer: WritableStreamDefaultWriter) { + this.sseStreams.add(writer); + writer.closed.then(() => { + this.sseStreams.delete(writer); + }); + + // Send buffered events to new connection + (async () => { + for (const encoded of this.streamingBuffer) { + await writer.write(encoded); + } + })(); + } + + broadcast( + event: StreamChatEvent, + wss: WebSocketServer, + wsDataMap: WeakMap< + WebSocket, + { type: "token"; id: string } | { type: "chat"; chatID: string } + > + ) { + const encoded = encodeStreamChatEvent(event); + + // Store message chunks for reconnecting clients + if (event.event === "message.chunk.added") { + this.streamingBuffer.push(encoded); + } + + // Broadcast to WebSockets + wss.clients.forEach((client) => { + const data = wsDataMap.get(client); + if ( + client.readyState === WebSocket.OPEN && + data?.type === "chat" && + data.chatID === this.id + ) { + client.send(encoded); + } + }); + + // Broadcast to SSE streams + for (const writer of this.sseStreams) { + writer.write(encoded).catch(() => { + // Client disconnected, ignore + }); + } + } + + async start(opts: { + interrupt: boolean; + db: Querier; + env: Record; + wss: WebSocketServer; + wsDataMap: WeakMap< + WebSocket, + { type: "token"; id: string } | { type: "chat"; chatID: string } + >; + }) { + if (opts.interrupt) { + this.streamAbortController?.abort(); + } + + if (this.running && !opts.interrupt) { + return; + } + + this.running = true; + this.executeChat(opts); + } + + stop() { + this.streamAbortController?.abort(); + this.running = false; + } + + private async executeChat(opts: { + db: Querier; + env: Record; + wss: WebSocketServer; + wsDataMap: WeakMap< + WebSocket, + { type: "token"; id: string } | { type: "chat"; chatID: string } + >; + }) { + this.streamAbortController?.abort(); + const controller = new AbortController(); + this.streamAbortController = controller; + + try { + this.streamingBuffer = []; + const result = await runChat({ + id: this.id, + signal: controller.signal, + db: opts.db, + broadcast: async (event) => { + this.broadcast(event, opts.wss, opts.wsDataMap); + }, + waitUntil: async (promise) => { + // In Node/Bun we can just let it run + promise.catch(console.error); + }, + env: opts.env as any, + writePlatformLog: async () => { + // No-op for now + }, + }); + + this.streamingBuffer = []; + + if (result.continue) { + // Continue executing + await this.executeChat(opts); + } else { + this.running = false; + } + } catch (error) { + if (error instanceof Error && error.name === "AbortError") { + // Expected when stopping + return; + } + console.error("Chat execution error:", error); + this.running = false; + } finally { + if (this.streamAbortController === controller) { + this.streamAbortController = undefined; + } + } + } + + async broadcastMessagesChanged( + event: "message.created" | "message.updated", + messages: DBMessage[], + wss: WebSocketServer, + wsDataMap: WeakMap< + WebSocket, + { type: "token"; id: string } | { type: "chat"; chatID: string } + > + ) { + for (const message of messages) { + this.broadcast( + { + event, + data: { + id: message.id, + chat_id: message.chat_id, + role: message.role, + parts: message.parts, + format: "ai-sdk", + created_at: message.created_at.toISOString(), + metadata: message.metadata, + }, + }, + wss, + wsDataMap + ); + } + } + + getBufferedEvents() { + return this.streamingBuffer; + } + + sendBufferedEvents(ws: any) { + for (const encoded of this.streamingBuffer) { + ws.send(encoded); + } + } +} + +export class ChatManager { + private sessions = new Map(); + + constructor( + private wss: WebSocketServer, + private wsDataMap: WeakMap< + WebSocket, + { type: "token"; id: string } | { type: "chat"; chatID: string } + >, + private getDB: () => Promise, + private env: Record + ) {} + + private getSession(id: string): ChatSession { + let session = this.sessions.get(id); + if (!session) { + session = new ChatSession(id); + this.sessions.set(id, session); + } + return session; + } + + async handleStream(id: string, request: Request): Promise { + const session = this.getSession(id); + + // Handle SSE + if (request.headers.get("Accept") === "text/event-stream") { + const transform = new TextEncoderStream(); + const writer = transform.writable.getWriter(); + session.addSSEStream(writer); + + return new Response(transform.readable, { + status: 200, + headers: { + "content-type": "text/event-stream", + "cache-control": "no-cache, no-transform", + "transfer-encoding": "chunked", + connection: "keep-alive", + }, + }); + } + + return new Response("Bad Request", { status: 400 }); + } + + async handleStart(opts: { id: string; interrupt: boolean }) { + const session = this.getSession(opts.id); + const db = await this.getDB(); + await session.start({ + interrupt: opts.interrupt, + db, + env: this.env, + wss: this.wss, + wsDataMap: this.wsDataMap, + }); + } + + async handleStop(id: string) { + const session = this.sessions.get(id); + if (session) { + session.stop(); + } + } + + async handleMessagesChanged( + event: "message.created" | "message.updated", + id: string, + messages: DBMessage[] + ) { + const session = this.getSession(id); + await session.broadcastMessagesChanged( + event, + messages, + this.wss, + this.wsDataMap + ); + } + + sendBufferedEventsToWebSocket(chatID: string, ws: any) { + const session = this.sessions.get(chatID); + if (session) { + session.sendBufferedEvents(ws); + } + } +} + +function encodeStreamChatEvent(event: StreamChatEvent): string { + return [ + `event: ${event.event}`, + `data: ${JSON.stringify(event.data)}`, + "\n", + ].join("\n"); +} diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts new file mode 100644 index 0000000..7e60841 --- /dev/null +++ b/packages/server/src/cli.ts @@ -0,0 +1,78 @@ +#!/usr/bin/env node + +import boxen from "boxen"; +import chalk from "chalk"; +import { Command } from "commander"; +import { version } from "../package.json"; +import * as logger from "./logger"; +import { ensurePostgres } from "./postgres"; +import { startServer } from "./server"; + +const program = new Command(); + +program + .name("blink-server") + .description("Self-hosted Blink server") + .version(version) + .option("-p, --port ", "Port to run the server on", "3005") + .action(async (options) => { + try { + await runServer(options); + } catch (error) { + console.error(error, error instanceof Error ? error.stack : undefined); + logger.error( + error instanceof Error ? error.message : "An unknown error occurred" + ); + process.exit(1); + } + }); + +async function runServer(options: { port: string }) { + const port = parseInt(options.port, 10); + if (isNaN(port) || port < 1 || port > 65535) { + throw new Error(`Invalid port: ${options.port}`); + } + + console.log(chalk.bold("blinkβ– "), version, chalk.gray("agents as a service")); + + // Check and setup environment variables + let postgresUrl = process.env.POSTGRES_URL || process.env.DATABASE_URL; + + if (!postgresUrl) { + postgresUrl = await ensurePostgres(); + } + + // Generate or use existing AUTH_SECRET + const authSecret = + process.env.AUTH_SECRET || "fake-random-string-should-be-in-db"; + + const baseUrl = process.env.BASE_URL || `http://localhost:${port}`; + + // Start the server + const srv = await startServer({ + port, + postgresUrl, + authSecret, + baseUrl, + }); + + const box = boxen( + [ + "View the Web UI:", + chalk.magenta.underline(baseUrl), + "", + `Set ${chalk.bold("BLINK_API_URL=" + baseUrl)} when using the Blink CLI.`, + ].join("\n"), + { + borderColor: "cyan", + padding: { + left: 4, + right: 4, + }, + textAlignment: "center", + } + ); + console.log(box); +} + +program.parse(); diff --git a/packages/server/src/logger.ts b/packages/server/src/logger.ts new file mode 100644 index 0000000..7e847a0 --- /dev/null +++ b/packages/server/src/logger.ts @@ -0,0 +1,29 @@ +import chalk from "chalk"; + +function formatTimestamp(): string { + const now = new Date(); + const date = now.toISOString().split("T")[0]!; + const time = now.toTimeString().split(" ")[0]!; + const ms = now.getMilliseconds().toString().padStart(3, "0"); + return chalk.gray(`${date} ${time}.${ms}`); +} + +export function info(message: string) { + console.log(`${formatTimestamp()} ${chalk.cyan("[info]")} ${message}`); +} + +export function warn(message: string) { + console.log(`${formatTimestamp()} ${chalk.yellow("[warn]")} ${message}`); +} + +export function error(message: string) { + console.log(`${formatTimestamp()} ${chalk.red("[error]")} ${message}`); +} + +export function success(message: string) { + console.log(`${formatTimestamp()} ${chalk.green("[info]")} ${message}`); +} + +export function plain(message: string) { + console.log(message); +} diff --git a/packages/server/src/postgres.ts b/packages/server/src/postgres.ts new file mode 100644 index 0000000..a3055b4 --- /dev/null +++ b/packages/server/src/postgres.ts @@ -0,0 +1,225 @@ +import { spawn } from "child_process"; +import { createServer } from "net"; +import * as logger from "./logger"; + +const CONTAINER_NAME = "blink-server-postgres"; +const POSTGRES_PASSWORD = "blink-server-dev-password"; +const POSTGRES_USER = "postgres"; +const POSTGRES_DB = "blink"; +const POSTGRES_PORT = 54321; + +function runCommand(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const proc = spawn(command, args); + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + proc.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + proc.on("close", (code) => { + if (code === 0) { + resolve(stdout.trim()); + } else { + reject(new Error(`Command failed with code ${code}: ${stderr}`)); + } + }); + + proc.on("error", (error) => { + reject(error); + }); + }); +} + +async function isPortAvailable(port: number): Promise { + return new Promise((resolve) => { + const server = createServer(); + + server.once("error", () => { + resolve(false); + }); + + server.once("listening", () => { + server.close(); + resolve(true); + }); + + server.listen(port); + }); +} + +async function isDockerRunning(): Promise { + try { + await runCommand("docker", ["info"]); + return true; + } catch { + return false; + } +} + +async function getContainerStatus(): Promise< + "running" | "stopped" | "not-found" +> { + try { + const output = await runCommand("docker", [ + "ps", + "-a", + "--filter", + `name=^${CONTAINER_NAME}$`, + "--format", + "{{.State}}", + ]); + + if (!output) { + return "not-found"; + } + + return output === "running" ? "running" : "stopped"; + } catch { + return "not-found"; + } +} + +async function startExistingContainer(): Promise { + logger.plain(`Starting existing PostgreSQL container: ${CONTAINER_NAME}`); + await runCommand("docker", ["start", CONTAINER_NAME]); + + // Wait for PostgreSQL to be ready + await waitForPostgres(); +} + +async function createAndStartContainer(): Promise { + logger.plain(`Creating PostgreSQL container: ${CONTAINER_NAME}`); + + const portAvailable = await isPortAvailable(POSTGRES_PORT); + if (!portAvailable) { + throw new Error( + `Port ${POSTGRES_PORT} is already in use. Please free the port or set POSTGRES_URL manually.` + ); + } + + await runCommand("docker", [ + "run", + "-d", + "--name", + CONTAINER_NAME, + "--restart", + "unless-stopped", + "-e", + `POSTGRES_PASSWORD=${POSTGRES_PASSWORD}`, + "-e", + `POSTGRES_DB=${POSTGRES_DB}`, + "-p", + `${POSTGRES_PORT}:5432`, + "pgvector/pgvector:pg17", + ]); + + logger.plain("PostgreSQL container created"); + + // Wait for PostgreSQL to be ready + await waitForPostgres(); +} + +async function waitForPostgres(): Promise { + logger.plain("Waiting for PostgreSQL to be ready..."); + + const maxAttempts = 30; + for (let i = 0; i < maxAttempts; i++) { + try { + await runCommand("docker", [ + "exec", + CONTAINER_NAME, + "pg_isready", + "-U", + POSTGRES_USER, + ]); + logger.plain("PostgreSQL is ready"); + return; + } catch { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + + throw new Error("PostgreSQL failed to become ready in time"); +} + +async function promptUser(question: string): Promise { + logger.plain(question); + process.stdout.write("(y/n): "); + + return new Promise((resolve) => { + const stdin = process.stdin; + stdin.setRawMode(true); + stdin.resume(); + stdin.setEncoding("utf8"); + + const onData = (key: string) => { + stdin.setRawMode(false); + stdin.pause(); + stdin.removeListener("data", onData); + + console.log(key); + + if (key === "y" || key === "Y") { + resolve(true); + } else { + resolve(false); + } + }; + + stdin.on("data", onData); + }); +} + +export async function ensurePostgres(): Promise { + // Check if Docker is running + const dockerRunning = await isDockerRunning(); + if (!dockerRunning) { + throw new Error( + "Docker is not running. Please start Docker or set POSTGRES_URL manually." + ); + } + + const status = await getContainerStatus(); + + if (status === "running") { + logger.info( + `Using Docker PostgreSQL '${CONTAINER_NAME}' because POSTGRES_URL is not set` + ); + return getConnectionString(); + } + + if (status === "stopped") { + await startExistingContainer(); + logger.info( + `Using Docker PostgreSQL '${CONTAINER_NAME}' because POSTGRES_URL is not set` + ); + return getConnectionString(); + } + + // Container doesn't exist, ask user if they want to create it + const shouldCreate = await promptUser( + "No PostgreSQL container found. Create one with Docker?" + ); + + if (!shouldCreate) { + throw new Error( + "PostgreSQL is required. Please set POSTGRES_URL manually." + ); + } + + await createAndStartContainer(); + logger.info( + `Using Docker PostgreSQL '${CONTAINER_NAME}' because POSTGRES_URL is not set` + ); + return getConnectionString(); +} + +export function getConnectionString(): string { + return `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${POSTGRES_PORT}/${POSTGRES_DB}`; +} diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts new file mode 100644 index 0000000..c1a7d10 --- /dev/null +++ b/packages/server/src/server.ts @@ -0,0 +1,474 @@ +import api from "@blink.so/api/server"; +import connectToPostgres from "@blink.so/database/postgres"; +import Querier from "@blink.so/database/querier"; +import { migrate } from "drizzle-orm/node-postgres/migrator"; +import { existsSync } from "fs"; +import { readFile } from "fs/promises"; +import { createServer, IncomingMessage } from "http"; +import module from "module"; +import path, { join } from "path"; +import { parse } from "url"; +import { WebSocket, WebSocketServer } from "ws"; +import { deployAgentWithDocker } from "./agent-deployment"; +import { ChatManager } from "./chat"; + +type WSData = { type: "token"; id: string } | { type: "chat"; chatID: string }; + +interface ServerOptions { + port: number; + postgresUrl: string; + authSecret: string; + baseUrl: string; +} + +// Files are now stored in the database instead of in-memory + +export async function startServer(options: ServerOptions) { + const { port, postgresUrl, authSecret, baseUrl } = options; + + const db = await connectToPostgres(postgresUrl); + const querier = new Querier(db); + + // Here we find the correct directories for the site and migrations. + let siteDir = join(import.meta.dirname, "site"); + let migrationsDir = join(import.meta.dirname, "migrations"); + if (import.meta.filename.endsWith("server.ts")) { + // We're running in development mode, so we need to point to the dist directory. + const distDir = join(import.meta.dirname, "..", "dist"); + if (!existsSync(distDir)) { + throw new Error( + `Dist directory not found: ${distDir}. Run 'bun run build' to build the server.` + ); + } + siteDir = join(distDir, "site"); + migrationsDir = join(distDir, "migrations"); + } + + // Run database migrations... + await migrate(db, { migrationsFolder: migrationsDir }); + + const app = await startNextServer({ + siteDir, + postgresUrl, + authSecret, + baseUrl, + }); + await app.prepare(); + const nextHandler = app.getRequestHandler(); + + const chatManagerRef: { current?: ChatManager } = {}; + + // Store WebSocket metadata without monkey-patching + const wsDataMap = new WeakMap(); + + // Create WebSocket server first (needed in api.fetch below) + const wss = new WebSocketServer({ noServer: true }); + + // Helper to convert Node.js request to Fetch Request + const toFetchRequest = (nodeReq: IncomingMessage): Request => { + const protocol = "http"; + const host = nodeReq.headers.host || `localhost:${port}`; + const fullUrl = `${protocol}://${host}${nodeReq.url}`; + + const headers = new Headers(); + for (const [key, value] of Object.entries(nodeReq.headers)) { + if (value) { + if (Array.isArray(value)) { + for (const v of value) headers.append(key, v); + } else { + headers.set(key, value); + } + } + } + + // Node.js IncomingMessage is a ReadableStream but needs type assertion + // for the Fetch API Request constructor + const body = + nodeReq.method !== "GET" && nodeReq.method !== "HEAD" + ? (nodeReq as any) + : undefined; + + return new Request(fullUrl, { + method: nodeReq.method, + headers, + body, + // @ts-ignore - this is a NodeJS thing. + duplex: "half", + }); + }; + + // Create HTTP server + const server = createServer(async (nodeReq, nodeRes) => { + try { + const url = new URL( + nodeReq.url || "/", + `http://${nodeReq.headers.host || `localhost:${port}`}` + ); + + if (url.pathname.startsWith("/api")) { + const req = toFetchRequest(nodeReq); + const response = await api.fetch( + req, + { + AUTH_SECRET: authSecret, + NODE_ENV: "development", + agentStore: (deploymentTargetID) => { + return { + delete: async (key) => { + await querier.deleteAgentStorageKV({ + deployment_target_id: deploymentTargetID, + key, + }); + }, + get: async (key) => { + const value = await querier.selectAgentStorageKV({ + deployment_target_id: deploymentTargetID, + key, + }); + if (!value) { + return undefined; + } + return value.value; + }, + set: async (key, value) => { + const target = + await querier.selectAgentDeploymentTargetByID( + deploymentTargetID + ); + if (!target) { + throw new Error("Deployment target not found"); + } + await querier.upsertAgentStorageKV({ + agent_deployment_target_id: target.id, + agent_id: target.agent_id, + key: key, + value: value, + }); + }, + list: async (prefix, options) => { + const values = await querier.selectAgentStorageKVByPrefix({ + deployment_target_id: deploymentTargetID, + prefix: prefix ?? "", + limit: options?.limit ?? 100, + cursor: options?.cursor, + }); + return { + entries: values.items.map((value) => ({ + key: value.key, + value: value.value, + })), + cursor: values.next_cursor ? values.next_cursor : undefined, + }; + }, + }; + }, + database: async () => { + const conn = await connectToPostgres(postgresUrl); + return new Querier(conn); + }, + apiBaseURL: url, + auth: { + handleWebSocketTokenRequest: async (id, request) => { + // WebSocket upgrades are handled in the 'upgrade' event + return new Response(null, { status: 101 }); + }, + sendTokenToWebSocket: async (id, token) => { + wss.clients.forEach((client) => { + const data = wsDataMap.get(client); + if ( + client.readyState === WebSocket.OPEN && + data?.type === "token" && + data.id === id + ) { + client.send(token); + } + }); + }, + }, + chat: { + async handleMessagesChanged(event, id, messages) { + await chatManagerRef.current?.handleMessagesChanged( + event, + id, + messages + ); + }, + handleStart: async (opts) => { + await chatManagerRef.current?.handleStart(opts); + }, + handleStop: async (id) => { + await chatManagerRef.current?.handleStop(id); + }, + handleStream: async (id, req) => { + if (!chatManagerRef.current) { + return new Response("Server not ready", { status: 503 }); + } + // WebSocket upgrades are handled in the 'upgrade' event + if (req.headers.get("upgrade")?.toLowerCase() === "websocket") { + return new Response(null, { status: 101 }); + } + return await chatManagerRef.current.handleStream(id, req); + }, + generateTitle: async (opts) => { + // noop + }, + }, + deployAgent: async (deployment) => { + await deployAgentWithDocker({ + deployment, + querier, + baseUrl, + downloadFile: async (id: string) => { + const file = await querier.selectFileByID(id); + if (!file || !file.content) { + throw new Error("File not found"); + } + + // Convert buffer back to ReadableStream + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(file.content); + controller.close(); + }, + }); + + return { + stream, + type: file.content_type, + name: file.name, + size: file.byte_length, + }; + }, + }); + }, + files: { + upload: async (opts) => { + const id = crypto.randomUUID(); + + // Read file content into buffer + const arrayBuffer = await opts.file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // Store file in database + await querier.insertFile({ + id, + name: opts.file.name, + message_id: null, + user_id: null, + organization_id: null, + content_type: opts.file.type, + byte_length: opts.file.size, + pdf_page_count: null, + content: buffer, + }); + + return { + id, + url: `${baseUrl}/api/files/${id}`, + }; + }, + download: async (id) => { + const file = await querier.selectFileByID(id); + if (!file || !file.content) { + throw new Error("File not found"); + } + + // Convert buffer back to ReadableStream + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(file.content); + controller.close(); + }, + }); + + return { + stream, + type: file.content_type, + name: file.name, + size: file.byte_length, + }; + }, + }, + logs: { + get: async (opts) => { + return querier.getAgentLogs(opts); + }, + write: async (opts) => { + await querier.writeAgentLog(opts); + }, + }, + traces: { + write: async (spans) => { + await querier.writeAgentTraces(spans); + }, + read: async (opts) => { + return querier.readAgentTraces(opts); + }, + }, + runtime: { + usage: async (opts) => { + // noop + throw new Error("Not implemented"); + }, + }, + }, + { + waitUntil: async (promise) => { + // noop + }, + passThroughOnException: () => { + // noop + }, + props: {}, + } + ); + + // Write Fetch Response to Node.js response + const headersObj: Record = {}; + response.headers.forEach((value, key) => { + const existing = headersObj[key]; + if (existing) { + if (Array.isArray(existing)) { + existing.push(value); + } else { + headersObj[key] = [existing, value]; + } + } else { + headersObj[key] = value; + } + }); + nodeRes.writeHead(response.status, response.statusText, headersObj); + + if (response.body) { + const reader = response.body.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + nodeRes.write(value); + } + } + nodeRes.end(); + return; + } + + // Handle Next.js routes + await nextHandler(nodeReq, nodeRes); + } catch (error) { + console.error("Request error:", error); + if (!nodeRes.headersSent) { + nodeRes.writeHead(500, { "Content-Type": "text/plain" }); + nodeRes.end("Internal Server Error"); + } + } + }); + + // Handle WebSocket upgrades + server.on("upgrade", (request, socket, head) => { + const { pathname, query } = parse(request.url || "", true); + + // Check if this is a token auth WebSocket + if (pathname?.startsWith("/api/auth/token")) { + const id = query.id as string; + wss.handleUpgrade(request, socket, head, (ws) => { + wsDataMap.set(ws, { type: "token", id }); + wss.emit("connection", ws, request); + }); + return; + } + + // Check if this is a chat WebSocket + const chatMatch = pathname?.match(/\/api\/chats\/([^/]+)\/stream/); + if (chatMatch?.[1]) { + const chatID = chatMatch[1]; + wss.handleUpgrade(request, socket, head, (ws) => { + wsDataMap.set(ws, { type: "chat", chatID }); + wss.emit("connection", ws, request); + }); + return; + } + + socket.destroy(); + }); + + wss.on("connection", (ws) => { + const data = wsDataMap.get(ws); + + if (data?.type === "chat") { + // Send buffered chunk events to reconnecting client + chatManagerRef.current?.sendBufferedEventsToWebSocket(data.chatID, ws); + } + + ws.on("close", () => { + wsDataMap.delete(ws); + }); + }); + + chatManagerRef.current = new ChatManager( + wss, + wsDataMap, + async () => { + const conn = await connectToPostgres(postgresUrl); + return new Querier(conn); + }, + process.env as Record + ); + + server.listen(port); + + return server; +} + +export interface StartNextServerOptions { + siteDir: string; + + postgresUrl: string; + authSecret: string; + baseUrl: string; +} + +/** + * startNextServer starts the Next.js server. + * It does this in a kinda convoluted way because we use the standalone + * mode but want to handle all the routes ourselves, not having it listen + * on it's own port and such. + */ +const startNextServer = async (opts: StartNextServerOptions) => { + // createRequire needs a filename (not directory) to establish module resolution context. + // We create a minimal package.json in the site dir during build for this purpose. + const packageJsonPath = path.join(opts.siteDir, "package.json"); + if (!existsSync(packageJsonPath)) { + throw new Error( + `package.json not found at ${packageJsonPath}. Make sure you built with BUILD_SITE=1.` + ); + } + const customRequire = module.createRequire(packageJsonPath); + + // These are env vars that the server needs to run. + // We could technically make these use the same DB instance somehow. + process.env.POSTGRES_URL = opts.postgresUrl; + process.env.AUTH_SECRET = opts.authSecret; + process.env.NEXT_PUBLIC_BASE_URL = opts.baseUrl; + + let nextConfig: any = {}; + try { + const content = await readFile( + path.join(opts.siteDir, ".next", "required-server-files.json"), + "utf-8" + ); + nextConfig = JSON.parse(content).config; + } catch (err) { + throw new Error( + `dev error: required next config file not found at ${path.join(opts.siteDir, ".next", "required-server-files.json")}: ${err}` + ); + } + // This is required for Next to not freak out about not having a config. + // Their standalone generated file does exactly this. + process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig); + + const next = customRequire("next") as typeof import("next").default; + const app = next({ + dev: false, + dir: opts.siteDir, + }); + return app; +}; diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 0000000..99559bc --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["bun-types"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/server/tsdown.config.ts b/packages/server/tsdown.config.ts new file mode 100644 index 0000000..f1f3628 --- /dev/null +++ b/packages/server/tsdown.config.ts @@ -0,0 +1,103 @@ +import { execSync } from "child_process"; +import { cpSync, existsSync, mkdirSync } from "fs"; +import { join } from "path"; +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: "src/main.ts", + format: ["esm"], + target: "node20", + outDir: "dist", + clean: true, + dts: false, + // Only externalize Next.js + external: [/^next/], + // Use shims to ensure circular deps work + shims: true, + onSuccess: async () => { + console.log("\nπŸ“¦ Building production assets..."); + + const rootDir = join(import.meta.dirname, "..", ".."); + const siteDir = join(rootDir, "packages", "site"); + const dbDir = join(rootDir, "packages", "database"); + const distDir = join(import.meta.dirname, "dist"); + const siteBuildDir = join(distDir, "site"); + + // Build Next.js site in its source directory + console.log("πŸ”¨ Building Next.js site..."); + execSync("bun run build", { + cwd: siteDir, + stdio: "inherit", + }); + + // Copy only essential parts of .next folder (exclude cache) + const nextBuildSource = join(siteDir, ".next"); + const nextBuildTarget = join(siteBuildDir, ".next"); + + if (existsSync(nextBuildSource)) { + console.log("πŸ“„ Copying Next.js build output (excluding cache)..."); + mkdirSync(siteBuildDir, { recursive: true }); + + // Copy only the essential directories + const essentialDirs = [ + "server", + "static", + "types", + "app-paths-manifest.json", + "build-manifest.json", + "package.json", + "prerender-manifest.json", + "react-loadable-manifest.json", + "required-server-files.json", + "routes-manifest.json", + ]; + + for (const item of essentialDirs) { + const src = join(nextBuildSource, item); + const dest = join(nextBuildTarget, item); + if (existsSync(src)) { + cpSync(src, dest, { recursive: true }); + } + } + } else { + throw new Error("Next.js build not found at " + nextBuildSource); + } + + // Copy public folder if exists + const publicSource = join(siteDir, "public"); + if (existsSync(publicSource)) { + console.log("πŸ“„ Copying public assets..."); + cpSync(publicSource, join(siteBuildDir, "public"), { recursive: true }); + } + + // Copy migrations + const migrationsSource = join(dbDir, "migrations"); + const migrationsTarget = join(distDir, "migrations"); + + if (existsSync(migrationsSource)) { + console.log("πŸ“„ Copying migrations..."); + cpSync(migrationsSource, migrationsTarget, { recursive: true }); + } + + // Create minimal package.json for external dependencies + const packageJsonPath = join(distDir, "package.json"); + const packageJson = { + type: "module", + dependencies: { + next: "*", + pg: "*", + "drizzle-orm": "*", + }, + }; + console.log("πŸ“„ Creating package.json..."); + cpSync(join(import.meta.dirname, "package.json"), packageJsonPath); + + console.log("βœ… Build complete!"); + console.log(` Server: ${distDir}/main.js + chunks`); + console.log(` Site: ${siteBuildDir}/`); + console.log(` Migrations: ${migrationsTarget}/`); + console.log(`\nπŸ“ Next steps:`); + console.log(` cd dist && bun install (for Next.js)`); + console.log(` bun run start:prod`); + }, +}); From c72967105121fde19acb11908f8f5b2f79dd0811 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Tue, 9 Dec 2025 12:50:43 +0100 Subject: [PATCH 2/5] changes --- .gitignore | 1 + packages/server/package.json | 3 +- packages/server/scripts/build.ts | 53 ++++++++++++- packages/server/src/cli.ts | 13 +++- packages/server/src/postgres.ts | 4 + packages/server/src/server.ts | 127 +++++++++++++++++++++++++++---- packages/site/lib/database.ts | 20 +++-- packages/site/next-env.d.ts | 1 + 8 files changed, 197 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 717961f..495da71 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ .sonda .next **/tsconfig.tsbuildinfo +.blink diff --git a/packages/server/package.json b/packages/server/package.json index 1a39000..cd77e1c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -23,7 +23,8 @@ "type": "module", "scripts": { "build": "bun scripts/build.ts", - "typecheck": "tsgo --noEmit" + "typecheck": "tsgo --noEmit", + "dev": "bun src/cli.ts --dev" }, "devDependencies": { "@types/node": "^22.10.2", diff --git a/packages/server/scripts/build.ts b/packages/server/scripts/build.ts index 8e2fe13..dcfc756 100644 --- a/packages/server/scripts/build.ts +++ b/packages/server/scripts/build.ts @@ -1,6 +1,13 @@ import { build } from "bun"; import { execSync } from "child_process"; -import { cpSync, mkdirSync, rmSync, writeFileSync } from "fs"; +import { + cpSync, + mkdirSync, + readdirSync, + rmSync, + symlinkSync, + writeFileSync, +} from "fs"; import { join } from "path"; const distDir = join(import.meta.dirname, "..", "dist"); @@ -65,6 +72,50 @@ function buildNextSite() { join(distDir, "site", "package.json"), JSON.stringify({ type: "module" }) ); + + // Create symlinks for packages in .bun directory so Node.js can resolve them. + // Bun uses a .bun directory structure instead of flat node_modules, so we need + // to create symlinks at the top level pointing to the actual packages. + const bunDir = join(distDir, "site", "node_modules", ".bun"); + const nodeModulesDir = join(distDir, "site", "node_modules"); + for (const entry of readdirSync(bunDir)) { + // Skip non-package entries + if (entry === "node_modules" || entry.startsWith(".")) continue; + + // Parse package name from entry (e.g., "next@15.5.6+..." -> "next") + // or ("@img+sharp-linux-arm64@0.34.5" -> "@img/sharp-linux-arm64") + const atIndex = entry.lastIndexOf("@"); + if (atIndex <= 0) continue; // Skip if no version found + + let packageName = entry.slice(0, atIndex); + // Handle scoped packages (bun uses + instead of /) + if (packageName.startsWith("@") && packageName.includes("+")) { + packageName = packageName.replace("+", "/"); + } + + const targetPath = packageName.includes("/") + ? join(nodeModulesDir, ...packageName.split("/")) + : join(nodeModulesDir, packageName); + + // Create parent directory for scoped packages + if (packageName.includes("/")) { + const scope = packageName.split("/")[0]!; + mkdirSync(join(nodeModulesDir, scope), { recursive: true }); + } + + // Create relative symlink + const relativePath = join( + ".bun", + entry, + "node_modules", + ...packageName.split("/") + ); + try { + symlinkSync(relativePath, targetPath); + } catch { + // Symlink may already exist + } + } } function copyMigrations() { diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index 7e60841..12d057e 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -15,6 +15,10 @@ program .description("Self-hosted Blink server") .version(version) .option("-p, --port ", "Port to run the server on", "3005") + .option( + "-d, --dev [host]", + "Proxy frontend requests to Next.js dev server (default: localhost:3000)" + ) .action(async (options) => { try { await runServer(options); @@ -27,7 +31,7 @@ program } }); -async function runServer(options: { port: string }) { +async function runServer(options: { port: string; dev?: boolean | string }) { const port = parseInt(options.port, 10); if (isNaN(port) || port < 1 || port > 65535) { throw new Error(`Invalid port: ${options.port}`); @@ -48,12 +52,19 @@ async function runServer(options: { port: string }) { const baseUrl = process.env.BASE_URL || `http://localhost:${port}`; + // Determine devProxy value + const devProxy = options.dev + ? options.dev === true + ? "localhost:3000" + : options.dev + : undefined; // Start the server const srv = await startServer({ port, postgresUrl, authSecret, baseUrl, + devProxy, }); const box = boxen( diff --git a/packages/server/src/postgres.ts b/packages/server/src/postgres.ts index a3055b4..b59cae0 100644 --- a/packages/server/src/postgres.ts +++ b/packages/server/src/postgres.ts @@ -116,7 +116,11 @@ async function createAndStartContainer(): Promise { `POSTGRES_DB=${POSTGRES_DB}`, "-p", `${POSTGRES_PORT}:5432`, + "-v", + "blink-server-postgres-data:/var/lib/postgresql/data", "pgvector/pgvector:pg17", + "-c", + "max_connections=1000", ]); logger.plain("PostgreSQL container created"); diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index c1a7d10..6397ea0 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -5,6 +5,7 @@ import { migrate } from "drizzle-orm/node-postgres/migrator"; import { existsSync } from "fs"; import { readFile } from "fs/promises"; import { createServer, IncomingMessage } from "http"; +import net from "net"; import module from "module"; import path, { join } from "path"; import { parse } from "url"; @@ -19,12 +20,13 @@ interface ServerOptions { postgresUrl: string; authSecret: string; baseUrl: string; + devProxy?: string; // e.g. "localhost:3000" } // Files are now stored in the database instead of in-memory export async function startServer(options: ServerOptions) { - const { port, postgresUrl, authSecret, baseUrl } = options; + const { port, postgresUrl, authSecret, baseUrl, devProxy } = options; const db = await connectToPostgres(postgresUrl); const querier = new Querier(db); @@ -47,14 +49,20 @@ export async function startServer(options: ServerOptions) { // Run database migrations... await migrate(db, { migrationsFolder: migrationsDir }); - const app = await startNextServer({ - siteDir, - postgresUrl, - authSecret, - baseUrl, - }); - await app.prepare(); - const nextHandler = app.getRequestHandler(); + // Only start Next.js in-memory if not in dev proxy mode + let nextHandler: ((req: IncomingMessage, res: any) => Promise) | null = + null; + + if (!devProxy) { + const app = await startNextServer({ + siteDir, + postgresUrl, + authSecret, + baseUrl, + }); + await app.prepare(); + nextHandler = app.getRequestHandler(); + } const chatManagerRef: { current?: ChatManager } = {}; @@ -163,8 +171,7 @@ export async function startServer(options: ServerOptions) { }; }, database: async () => { - const conn = await connectToPostgres(postgresUrl); - return new Querier(conn); + return querier; }, apiBaseURL: url, auth: { @@ -352,7 +359,11 @@ export async function startServer(options: ServerOptions) { } // Handle Next.js routes - await nextHandler(nodeReq, nodeRes); + if (nextHandler) { + await nextHandler(nodeReq, nodeRes); + } else { + await proxyToNextDev(nodeReq, nodeRes, devProxy!); + } } catch (error) { console.error("Request error:", error); if (!nodeRes.headersSent) { @@ -366,6 +377,25 @@ export async function startServer(options: ServerOptions) { server.on("upgrade", (request, socket, head) => { const { pathname, query } = parse(request.url || "", true); + // In dev mode, proxy Next.js HMR WebSocket + if (devProxy && pathname === "/_next/webpack-hmr") { + const [host, portStr] = devProxy.split(":"); + const port = parseInt(portStr || "3000", 10); + const proxySocket = net.connect(port, host, () => { + proxySocket.write( + `GET ${request.url} HTTP/1.1\r\n` + + Object.entries(request.headers) + .map(([k, v]) => `${k}: ${v}`) + .join("\r\n") + + "\r\n\r\n" + ); + socket.pipe(proxySocket); + proxySocket.pipe(socket); + }); + proxySocket.on("error", () => socket.destroy()); + return; + } + // Check if this is a token auth WebSocket if (pathname?.startsWith("/api/auth/token")) { const id = query.id as string; @@ -407,8 +437,7 @@ export async function startServer(options: ServerOptions) { wss, wsDataMap, async () => { - const conn = await connectToPostgres(postgresUrl); - return new Querier(conn); + return querier; }, process.env as Record ); @@ -418,6 +447,76 @@ export async function startServer(options: ServerOptions) { return server; } +/** + * Proxy HTTP requests to a Next.js dev server + */ +async function proxyToNextDev( + nodeReq: IncomingMessage, + nodeRes: import("http").ServerResponse, + proxyTarget: string +) { + try { + const url = `http://${proxyTarget}${nodeReq.url}`; + + const headers = new Headers(); + for (const [key, value] of Object.entries(nodeReq.headers)) { + if (value) { + if (Array.isArray(value)) { + for (const v of value) headers.append(key, v); + } else { + headers.set(key, value); + } + } + } + + const body = + nodeReq.method !== "GET" && nodeReq.method !== "HEAD" + ? (nodeReq as unknown as BodyInit) + : undefined; + + const response = await fetch(url, { + method: nodeReq.method, + headers, + body, + // @ts-ignore - Node.js specific option for streaming request body + duplex: "half", + }); + + // Write response headers, excluding encoding headers since fetch auto-decompresses + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + // Skip headers that are invalid after fetch auto-decompresses the response + const lowerKey = key.toLowerCase(); + if ( + lowerKey === "content-encoding" || + lowerKey === "content-length" || + lowerKey === "transfer-encoding" + ) { + return; + } + responseHeaders[key] = value; + }); + nodeRes.writeHead(response.status, responseHeaders); + + // Stream response body + if (response.body) { + const reader = response.body.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + nodeRes.write(value); + } + } + nodeRes.end(); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + nodeRes.writeHead(502, { "Content-Type": "text/plain" }); + nodeRes.end( + `Proxy error: ${message}. Is 'next dev' running on ${proxyTarget}?` + ); + } +} + export interface StartNextServerOptions { siteDir: string; diff --git a/packages/site/lib/database.ts b/packages/site/lib/database.ts index ed6d018..5738c3b 100644 --- a/packages/site/lib/database.ts +++ b/packages/site/lib/database.ts @@ -1,15 +1,19 @@ import connectToPostgres from "@blink.so/database/postgres"; import Querier from "@blink.so/database/querier"; +const querierCache = new Map(); + // getQuerier is a helper function for all functions in the site // that need to connect to the database. -// -// They do not need to be concerned about ending connections. -// This all runs serverless, and we have max idle time -// which will close the connection. +// TODO: it's janky that we're caching the querier globally like this. +// We should make it cleaner. export const getQuerier = async (): Promise => { - const conn = await connectToPostgres( - process.env.DATABASE_URL ?? process.env.POSTGRES_URL ?? "" - ); - return new Querier(conn); + const url = process.env.DATABASE_URL ?? process.env.POSTGRES_URL ?? ""; + let querier = querierCache.get(url); + if (!querier) { + const conn = await connectToPostgres(url); + querier = new Querier(conn); + querierCache.set(url, querier); + } + return querier; }; diff --git a/packages/site/next-env.d.ts b/packages/site/next-env.d.ts index 1b3be08..830fb59 100644 --- a/packages/site/next-env.d.ts +++ b/packages/site/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. From ae7959eb23f9b4090b339e0c6225753c278362d0 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Tue, 9 Dec 2025 13:29:13 +0100 Subject: [PATCH 3/5] docker networking --- packages/server/src/agent-deployment.ts | 40 ++- .../server/src/check-docker-networking.ts | 264 ++++++++++++++++++ 2 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 packages/server/src/check-docker-networking.ts diff --git a/packages/server/src/agent-deployment.ts b/packages/server/src/agent-deployment.ts index a10d4bf..6955801 100644 --- a/packages/server/src/agent-deployment.ts +++ b/packages/server/src/agent-deployment.ts @@ -9,6 +9,7 @@ import { mkdir, writeFile } from "fs/promises"; import { createServer } from "net"; import { tmpdir } from "os"; import { join } from "path"; +import { getDockerNetworkingConfig } from "./check-docker-networking"; interface DockerDeployOptions { deployment: AgentDeployment; @@ -88,10 +89,34 @@ export async function deployAgentWithDocker(opts: DockerDeployOptions) { deployment.target_id ); + // Determine the best Docker networking mode for this system + const networkConfig = await getDockerNetworkingConfig(); + console.log(`Docker networking config: ${JSON.stringify(networkConfig)}`); + + if (networkConfig.recommended === "none") { + throw new Error( + "Docker networking check failed: neither host networking nor port binding supports bidirectional communication between host and container. " + + "Please check your Docker configuration." + ); + } + + const useHostNetwork = + networkConfig.recommended === "host" || + networkConfig.recommended === "both"; + // Find free ports for this agent (one for external access, one for internal API) const externalPort = await findFreePort(); const internalAPIPort = await findFreePort(); + // Calculate the URL the container should use to reach the host + let containerBaseUrl = baseUrl; + if (!useHostNetwork && networkConfig.portBind.hostAddress) { + // Replace the host in baseUrl with the address that works from the container + const url = new URL(baseUrl); + url.hostname = networkConfig.portBind.hostAddress; + containerBaseUrl = url.toString().replace(/\/$/, ""); // Remove trailing slash + } + // Build Docker env args const dockerEnvArgs: string[] = []; // Wrapper runtime configuration @@ -102,10 +127,10 @@ export async function deployAgentWithDocker(opts: DockerDeployOptions) { ); dockerEnvArgs.push( "-e", - `${InternalAPIServerURLEnvironmentVariable}=${baseUrl}` + `${InternalAPIServerURLEnvironmentVariable}=${containerBaseUrl}` ); // Agent configuration - dockerEnvArgs.push("-e", `BLINK_REQUEST_URL=${baseUrl}`); + dockerEnvArgs.push("-e", `BLINK_REQUEST_URL=${containerBaseUrl}`); dockerEnvArgs.push("-e", `BLINK_REQUEST_ID=${target?.request_id}`); dockerEnvArgs.push("-e", `PORT=${externalPort}`); // User-defined environment variables @@ -128,6 +153,7 @@ export async function deployAgentWithDocker(opts: DockerDeployOptions) { // Ignore errors if container doesn't exist } + // Build docker args based on networking mode const dockerArgs = [ "run", "-d", @@ -135,8 +161,14 @@ export async function deployAgentWithDocker(opts: DockerDeployOptions) { containerName, "--restart", "unless-stopped", - "--network", - "host", + ...(useHostNetwork + ? ["--network", "host"] + : [ + "-p", + `${externalPort}:${externalPort}`, + "-p", + `${internalAPIPort}:${internalAPIPort}`, + ]), "-v", `${deploymentDir}:/app`, "-w", diff --git a/packages/server/src/check-docker-networking.ts b/packages/server/src/check-docker-networking.ts new file mode 100644 index 0000000..4a3deca --- /dev/null +++ b/packages/server/src/check-docker-networking.ts @@ -0,0 +1,264 @@ +import { spawn } from "node:child_process"; +import http from "node:http"; +import { createServer } from "node:net"; + +export interface NetworkingTestResult { + hostNetwork: { + hostToContainer: boolean; + containerToHost: boolean; + hostAddress: string | null; + }; + portBind: { + hostToContainer: boolean; + containerToHost: boolean; + hostAddress: string | null; + }; + recommended: "host" | "port-bind" | "both" | "none"; +} + +let cachedResult: NetworkingTestResult | null = null; + +/** + * Get the cached networking test result, or run the test if not cached. + */ +export async function getDockerNetworkingConfig(): Promise { + if (cachedResult) { + return cachedResult; + } + cachedResult = await checkDockerNetworking(); + return cachedResult; +} + +/** + * Clear the cached networking test result (useful for testing or if Docker config changes). + */ +export function clearDockerNetworkingCache(): void { + cachedResult = null; +} + +async function getRandomPort(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(); + server.listen(0, () => { + const addr = server.address(); + const port = typeof addr === "object" ? addr?.port : 0; + server.close(() => resolve(port!)); + }); + server.on("error", reject); + }); +} + +function startHostServer(): Promise<{ server: http.Server; port: number }> { + return new Promise((resolve) => { + const server = http.createServer((_req, res) => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ source: "host" })); + }); + server.listen(0, "0.0.0.0", () => { + const addr = server.address(); + const port = typeof addr === "object" ? addr?.port : 0; + resolve({ server, port: port! }); + }); + }); +} + +function execDocker( + args: string[] +): Promise<{ stdout: string; stderr: string; code: number }> { + return new Promise((resolve) => { + const proc = spawn("docker", args, { stdio: "pipe" }); + let stdout = ""; + let stderr = ""; + proc.stdout.on("data", (d) => (stdout += d)); + proc.stderr.on("data", (d) => (stderr += d)); + proc.on("close", (code) => resolve({ stdout, stderr, code: code ?? 1 })); + }); +} + +async function dockerRun( + name: string, + args: string[], + script: string +): Promise { + await execDocker([ + "run", + "--rm", + "-d", + "--name", + name, + ...args, + "node:alpine", + "node", + "-e", + script, + ]); +} + +async function dockerRm(name: string): Promise { + await execDocker(["rm", "-f", name]); +} + +const CONTAINER_SERVER_SCRIPT = ` +const http = require("http"); +const port = process.env.PORT || 3000; +http.createServer((req, res) => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ source: "container" })); +}).listen(port, "0.0.0.0", () => console.log("ready")); +`; + +async function testConnection(url: string, timeoutMs = 2000): Promise { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + const response = await fetch(url, { signal: controller.signal }); + clearTimeout(timeout); + return response.ok; + } catch { + return false; + } +} + +async function waitForServer( + url: string, + maxAttempts = 10, + delayMs = 300 +): Promise { + for (let i = 0; i < maxAttempts; i++) { + if (await testConnection(url)) return true; + await new Promise((r) => setTimeout(r, delayMs)); + } + return false; +} + +async function testContainerToHost( + containerName: string, + hostPort: number, + isHostNetwork: boolean +): Promise<{ success: boolean; address: string | null }> { + // Try multiple host addresses + const hostAddresses = [ + ...(isHostNetwork ? ["127.0.0.1"] : []), // localhost works with host networking + "host.docker.internal", + "172.17.0.1", // Common Docker bridge gateway + ]; + + for (const addr of hostAddresses) { + try { + const script = ` + fetch("http://${addr}:${hostPort}", { signal: AbortSignal.timeout(2000) }) + .then(r => r.text()) + .then(console.log) + .catch(() => process.exit(1)) + `; + const { stdout, code } = await execDocker([ + "exec", + containerName, + "node", + "-e", + script, + ]); + if (code === 0 && stdout.includes('"source":"host"')) { + return { success: true, address: addr }; + } + } catch { + // Continue to next address + } + } + return { success: false, address: null }; +} + +export async function checkDockerNetworking(): Promise { + const results: NetworkingTestResult = { + hostNetwork: { + hostToContainer: false, + containerToHost: false, + hostAddress: null, + }, + portBind: { + hostToContainer: false, + containerToHost: false, + hostAddress: null, + }, + recommended: "none", + }; + + // Start host server + const { server: hostServer, port: hostPort } = await startHostServer(); + + // Get random ports for containers + const hostNetPort = await getRandomPort(); + const bridgePort = await getRandomPort(); + + const HOST_CONTAINER = "blink-net-test-host"; + const BRIDGE_CONTAINER = "blink-net-test-bridge"; + + try { + // Start containers in parallel + await Promise.all([ + dockerRun( + HOST_CONTAINER, + ["--network", "host"], + CONTAINER_SERVER_SCRIPT.replace("3000", String(hostNetPort)) + ), + dockerRun( + BRIDGE_CONTAINER, + ["-p", `${bridgePort}:3000`], + CONTAINER_SERVER_SCRIPT + ), + ]); + + // Wait for containers to be ready + const [hostNetReady, bridgeReady] = await Promise.all([ + waitForServer(`http://localhost:${hostNetPort}`), + waitForServer(`http://localhost:${bridgePort}`), + ]); + + // Test host β†’ container + if (hostNetReady) { + results.hostNetwork.hostToContainer = await testConnection( + `http://localhost:${hostNetPort}` + ); + } + if (bridgeReady) { + results.portBind.hostToContainer = await testConnection( + `http://localhost:${bridgePort}` + ); + } + + // Test container β†’ host + const hostNetResult = await testContainerToHost( + HOST_CONTAINER, + hostPort, + true + ); + results.hostNetwork.containerToHost = hostNetResult.success; + results.hostNetwork.hostAddress = hostNetResult.address; + + const bridgeResult = await testContainerToHost( + BRIDGE_CONTAINER, + hostPort, + false + ); + results.portBind.containerToHost = bridgeResult.success; + results.portBind.hostAddress = bridgeResult.address; + + // Determine recommendation + const hostWorks = + results.hostNetwork.hostToContainer && + results.hostNetwork.containerToHost; + const bridgeWorks = + results.portBind.hostToContainer && results.portBind.containerToHost; + + if (hostWorks && bridgeWorks) results.recommended = "both"; + else if (hostWorks) results.recommended = "host"; + else if (bridgeWorks) results.recommended = "port-bind"; + else results.recommended = "none"; + } finally { + // Cleanup + hostServer.close(); + await Promise.all([dockerRm(HOST_CONTAINER), dockerRm(BRIDGE_CONTAINER)]); + } + + return results; +} From d0cf97115793104e0fc416ca6c6908f7e00e33c6 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Wed, 10 Dec 2025 13:32:04 +0100 Subject: [PATCH 4/5] expose server via devhook --- bun.lock | 1 + packages/api/src/routes/auth/auth.server.ts | 30 ++++++----- packages/server/package.json | 1 + packages/server/src/cli.ts | 30 +++++++++-- packages/server/src/devhook.ts | 56 +++++++++++++++++++++ 5 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 packages/server/src/devhook.ts diff --git a/bun.lock b/bun.lock index 96b4b40..6583fd2 100644 --- a/bun.lock +++ b/bun.lock @@ -293,6 +293,7 @@ "blink-server": "dist/cli.js", }, "devDependencies": { + "@blink.so/api": "workspace:*", "@types/node": "^22.10.2", "@types/pg": "^8.11.10", "@types/ws": "^8.5.13", diff --git a/packages/api/src/routes/auth/auth.server.ts b/packages/api/src/routes/auth/auth.server.ts index ce3f508..be8db4b 100644 --- a/packages/api/src/routes/auth/auth.server.ts +++ b/packages/api/src/routes/auth/auth.server.ts @@ -364,20 +364,22 @@ async function handleOAuthCallback( }); // Set cookies and redirect using Hono's setCookie helper - setCookie(c, SESSION_COOKIE_NAME, token, { + // NOTE: last_login_provider must come BEFORE blink_session_token because + // the devhook compute-protocol only preserves the last Set-Cookie header + setCookie(c, "last_login_provider", provider, { path: "/", httpOnly: true, sameSite: "Lax", secure: SESSION_SECURE, - maxAge: 30 * 24 * 60 * 60, // 30 days + maxAge: 60 * 60 * 24 * 180, // 180 days }); - setCookie(c, "last_login_provider", provider, { + setCookie(c, SESSION_COOKIE_NAME, token, { path: "/", httpOnly: true, sameSite: "Lax", secure: SESSION_SECURE, - maxAge: 60 * 60 * 24 * 180, // 180 days + maxAge: 30 * 24 * 60 * 60, // 30 days }); // If linking an existing account, redirect back with success message @@ -544,20 +546,22 @@ export default function mountAuth(server: APIServer) { }); // Set cookies using Hono's setCookie helper - setCookie(c, SESSION_COOKIE_NAME, token, { + // NOTE: last_login_provider must come BEFORE blink_session_token because + // the devhook compute-protocol only preserves the last Set-Cookie header + setCookie(c, "last_login_provider", "credentials", { path: "/", httpOnly: true, sameSite: "Lax", secure: SESSION_SECURE, - maxAge: 30 * 24 * 60 * 60, // 30 days + maxAge: 60 * 60 * 24 * 180, // 180 days }); - setCookie(c, "last_login_provider", "credentials", { + setCookie(c, SESSION_COOKIE_NAME, token, { path: "/", httpOnly: true, sameSite: "Lax", secure: SESSION_SECURE, - maxAge: 60 * 60 * 24 * 180, // 180 days + maxAge: 30 * 24 * 60 * 60, // 30 days }); return c.json({ ok: true, url: "/chat" }); @@ -628,20 +632,22 @@ export default function mountAuth(server: APIServer) { }); // Set cookies using Hono's setCookie helper - setCookie(c, SESSION_COOKIE_NAME, sessionToken, { + // NOTE: last_login_provider must come BEFORE blink_session_token because + // the devhook compute-protocol only preserves the last Set-Cookie header + setCookie(c, "last_login_provider", "credentials", { path: "/", httpOnly: true, sameSite: "Lax", secure: SESSION_SECURE, - maxAge: 30 * 24 * 60 * 60, // 30 days + maxAge: 60 * 60 * 24 * 180, // 180 days }); - setCookie(c, "last_login_provider", "credentials", { + setCookie(c, SESSION_COOKIE_NAME, sessionToken, { path: "/", httpOnly: true, sameSite: "Lax", secure: SESSION_SECURE, - maxAge: 60 * 60 * 24 * 180, // 180 days + maxAge: 30 * 24 * 60 * 60, // 30 days }); return c.json({ ok: true }); diff --git a/packages/server/package.json b/packages/server/package.json index cd77e1c..1c44449 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -27,6 +27,7 @@ "dev": "bun src/cli.ts --dev" }, "devDependencies": { + "@blink.so/api": "workspace:*", "@types/node": "^22.10.2", "@types/pg": "^8.11.10", "@types/ws": "^8.5.13", diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index 12d057e..895c474 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -4,6 +4,7 @@ import boxen from "boxen"; import chalk from "chalk"; import { Command } from "commander"; import { version } from "../package.json"; +import { startDevhookProxy } from "./devhook"; import * as logger from "./logger"; import { ensurePostgres } from "./postgres"; import { startServer } from "./server"; @@ -33,7 +34,7 @@ program async function runServer(options: { port: string; dev?: boolean | string }) { const port = parseInt(options.port, 10); - if (isNaN(port) || port < 1 || port > 65535) { + if (Number.isNaN(port) || port < 1 || port > 65535) { throw new Error(`Invalid port: ${options.port}`); } @@ -52,14 +53,33 @@ async function runServer(options: { port: string; dev?: boolean | string }) { const baseUrl = process.env.BASE_URL || `http://localhost:${port}`; - // Determine devProxy value const devProxy = options.dev ? options.dev === true ? "localhost:3000" : options.dev : undefined; + + // Determine access URL - use BLINK_ACCESS_URL if set, otherwise create devhook + let accessUrl: string; + let devhookCleanup: (() => void) | undefined; + + if (process.env.BLINK_ACCESS_URL) { + accessUrl = process.env.BLINK_ACCESS_URL; + } else { + const devhook = await startDevhookProxy(port); + accessUrl = devhook.accessUrl; + devhookCleanup = devhook.cleanup; + + const cleanup = () => { + devhookCleanup?.(); + process.exit(0); + }; + process.on("SIGTERM", cleanup); + process.on("SIGINT", cleanup); + } + // Start the server - const srv = await startServer({ + const _srv = await startServer({ port, postgresUrl, authSecret, @@ -70,9 +90,9 @@ async function runServer(options: { port: string; dev?: boolean | string }) { const box = boxen( [ "View the Web UI:", - chalk.magenta.underline(baseUrl), + chalk.magenta.underline(accessUrl), "", - `Set ${chalk.bold("BLINK_API_URL=" + baseUrl)} when using the Blink CLI.`, + `Set ${chalk.bold(`BLINK_API_URL=${accessUrl}`)} when using the Blink CLI.`, ].join("\n"), { borderColor: "cyan", diff --git a/packages/server/src/devhook.ts b/packages/server/src/devhook.ts new file mode 100644 index 0000000..86e1eea --- /dev/null +++ b/packages/server/src/devhook.ts @@ -0,0 +1,56 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import Client from "@blink.so/api"; + +function getXdgDataDir(): string { + return process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"); +} + +function getDevhookPath(): string { + return join(getXdgDataDir(), "blink-server", "devhook.txt"); +} + +function getOrCreateDevhookID(): string { + const devhookPath = getDevhookPath(); + if (existsSync(devhookPath)) { + return readFileSync(devhookPath, "utf-8").trim(); + } + mkdirSync(join(getXdgDataDir(), "blink-server"), { recursive: true }); + const id = crypto.randomUUID(); + writeFileSync(devhookPath, id); + return id; +} + +export interface DevhookProxy { + accessUrl: string; + cleanup: () => void; +} + +export async function startDevhookProxy(port: number): Promise { + const devhookId = getOrCreateDevhookID(); + const accessUrl = `https://${devhookId}.blink.host`; + + const client = new Client({ baseURL: "https://blink.so" }); + + return new Promise((resolve, reject) => { + const listener = client.devhook.listen({ + id: devhookId, + onRequest: async (request) => { + const localUrl = new URL(request.url); + localUrl.protocol = "http:"; + localUrl.host = `localhost:${port}`; + return fetch(new Request(localUrl.toString(), request)); + }, + onConnect: () => { + resolve({ + accessUrl, + cleanup: () => listener.dispose(), + }); + }, + onError: (error) => { + reject(error); + }, + }); + }); +} From 150aa3963b42fd42e3554e5f9741b2f6cd4db85e Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Tue, 9 Dec 2025 16:40:14 +0100 Subject: [PATCH 5/5] onboarding flow --- packages/api/src/client.browser.ts | 3 + .../api/src/routes/agent-request.server.ts | 213 +- packages/api/src/routes/agent-request.test.ts | 74 +- .../api/src/routes/agents/agents.client.ts | 105 + .../api/src/routes/agents/agents.server.ts | 55 + .../src/routes/agents/setup-github.client.ts | 187 + .../routes/agents/setup-github.server.test.ts | 358 ++ .../src/routes/agents/setup-github.server.ts | 553 +++ .../src/routes/agents/setup-slack.client.ts | 161 + .../src/routes/agents/setup-slack.server.ts | 259 ++ .../routes/onboarding/onboarding.client.ts | 119 + .../routes/onboarding/onboarding.server.ts | 244 ++ packages/api/src/server.ts | 8 + .../migrations/0001_colorful_silk_fever.sql | 1 + .../migrations/0002_glossy_hellcat.sql | 1 + .../database/migrations/0003_bent_veda.sql | 1 + .../migrations/meta/0001_snapshot.json | 3263 ++++++++++++++++ .../migrations/meta/0002_snapshot.json | 3269 ++++++++++++++++ .../migrations/meta/0003_snapshot.json | 3275 +++++++++++++++++ .../database/migrations/meta/_journal.json | 21 + packages/database/src/convert.ts | 1 + packages/database/src/schema.ts | 83 + packages/server/scripts/build.ts | 4 +- packages/server/src/cli.ts | 1 + packages/server/src/server.ts | 7 +- packages/site/.storybook/main.ts | 5 +- .../(app)/[organization]/[agent]/layout.tsx | 6 + .../[organization]/[agent]/page.stories.tsx | 3 + .../env-var-confirmation.stories.tsx | 306 ++ .../integrations/env-var-confirmation.tsx | 183 + .../integrations/github-integration.tsx | 229 ++ .../integrations/integration-card.stories.tsx | 179 + .../integrations/integration-card.tsx | 71 + .../integrations-manager.stories.tsx | 536 +++ .../integrations/integrations-manager.tsx | 188 + .../settings/integrations/llm-integration.tsx | 117 + .../[agent]/settings/integrations/page.tsx | 39 + .../integrations/slack-integration.tsx | 136 + .../integrations/use-integration-setup.ts | 149 + .../integrations/web-search-integration.tsx | 108 + .../[agent]/settings/navigation.tsx | 6 + .../site/app/(app)/[organization]/layout.tsx | 14 +- .../app/(app)/[organization]/page.stories.tsx | 6 + .../site/app/(app)/[organization]/page.tsx | 19 + .../~/onboarding/[agent]/page.tsx | 62 + .../~/onboarding/[agent]/wizard.tsx | 80 + .../components/progress-indicator.tsx | 79 + .../onboarding/components/wizard-content.tsx | 244 ++ .../~/onboarding/steps/deploying.tsx | 343 ++ .../~/onboarding/steps/github-setup.tsx | 162 + .../~/onboarding/steps/llm-api-keys.tsx | 256 ++ .../~/onboarding/steps/slack-setup.tsx | 73 + .../~/onboarding/steps/success.tsx | 55 + .../~/onboarding/steps/web-search.tsx | 149 + .../~/onboarding/steps/welcome.tsx | 141 + .../~/onboarding/wizard.stories.tsx | 422 +++ .../[organization]/~/onboarding/wizard.tsx | 77 + .../github-setup-wizard.stories.tsx | 256 ++ .../site/components/github-setup-wizard.tsx | 550 +++ .../site/components/llm-api-keys-setup.tsx | 276 ++ packages/site/components/slack-icon.tsx | 14 + .../components/slack-setup-wizard.stories.tsx | 584 +++ .../site/components/slack-setup-wizard.tsx | 721 ++++ packages/site/components/web-search-setup.tsx | 160 + packages/site/lib/api-client.ts | 3 +- packages/site/lib/slack-manifest.ts | 107 + packages/site/next-env.d.ts | 1 - 67 files changed, 19371 insertions(+), 10 deletions(-) create mode 100644 packages/api/src/routes/agents/setup-github.client.ts create mode 100644 packages/api/src/routes/agents/setup-github.server.test.ts create mode 100644 packages/api/src/routes/agents/setup-github.server.ts create mode 100644 packages/api/src/routes/agents/setup-slack.client.ts create mode 100644 packages/api/src/routes/agents/setup-slack.server.ts create mode 100644 packages/api/src/routes/onboarding/onboarding.client.ts create mode 100644 packages/api/src/routes/onboarding/onboarding.server.ts create mode 100644 packages/database/migrations/0001_colorful_silk_fever.sql create mode 100644 packages/database/migrations/0002_glossy_hellcat.sql create mode 100644 packages/database/migrations/0003_bent_veda.sql create mode 100644 packages/database/migrations/meta/0001_snapshot.json create mode 100644 packages/database/migrations/meta/0002_snapshot.json create mode 100644 packages/database/migrations/meta/0003_snapshot.json create mode 100644 packages/site/app/(app)/[organization]/[agent]/settings/integrations/env-var-confirmation.stories.tsx create mode 100644 packages/site/app/(app)/[organization]/[agent]/settings/integrations/env-var-confirmation.tsx create mode 100644 packages/site/app/(app)/[organization]/[agent]/settings/integrations/github-integration.tsx create mode 100644 packages/site/app/(app)/[organization]/[agent]/settings/integrations/integration-card.stories.tsx create mode 100644 packages/site/app/(app)/[organization]/[agent]/settings/integrations/integration-card.tsx create mode 100644 packages/site/app/(app)/[organization]/[agent]/settings/integrations/integrations-manager.stories.tsx create mode 100644 packages/site/app/(app)/[organization]/[agent]/settings/integrations/integrations-manager.tsx create mode 100644 packages/site/app/(app)/[organization]/[agent]/settings/integrations/llm-integration.tsx create mode 100644 packages/site/app/(app)/[organization]/[agent]/settings/integrations/page.tsx create mode 100644 packages/site/app/(app)/[organization]/[agent]/settings/integrations/slack-integration.tsx create mode 100644 packages/site/app/(app)/[organization]/[agent]/settings/integrations/use-integration-setup.ts create mode 100644 packages/site/app/(app)/[organization]/[agent]/settings/integrations/web-search-integration.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/[agent]/page.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/[agent]/wizard.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/components/progress-indicator.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/components/wizard-content.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/steps/deploying.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/steps/github-setup.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/steps/llm-api-keys.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/steps/slack-setup.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/steps/success.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/steps/web-search.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/steps/welcome.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/wizard.stories.tsx create mode 100644 packages/site/app/(app)/[organization]/~/onboarding/wizard.tsx create mode 100644 packages/site/components/github-setup-wizard.stories.tsx create mode 100644 packages/site/components/github-setup-wizard.tsx create mode 100644 packages/site/components/llm-api-keys-setup.tsx create mode 100644 packages/site/components/slack-icon.tsx create mode 100644 packages/site/components/slack-setup-wizard.stories.tsx create mode 100644 packages/site/components/slack-setup-wizard.tsx create mode 100644 packages/site/components/web-search-setup.tsx create mode 100644 packages/site/lib/slack-manifest.ts diff --git a/packages/api/src/client.browser.ts b/packages/api/src/client.browser.ts index 103e44a..769408c 100644 --- a/packages/api/src/client.browser.ts +++ b/packages/api/src/client.browser.ts @@ -5,6 +5,7 @@ import ChatRuns from "./routes/chats/runs.client"; import Files from "./routes/files.client"; import Invites from "./routes/invites.client"; import Messages from "./routes/messages.client"; +import Onboarding from "./routes/onboarding/onboarding.client"; import Organizations from "./routes/organizations/organizations.client"; import Users from "./routes/users.client"; @@ -34,6 +35,7 @@ export default class Client { public readonly invites = new Invites(this); public readonly users = new Users(this); public readonly messages = new Messages(this); + public readonly onboarding = new Onboarding(this); public constructor(options?: ClientOptions) { this.baseURL = new URL( @@ -101,5 +103,6 @@ export * from "./routes/agents/traces.client"; export * from "./routes/chats/chats.client"; export * from "./routes/invites.client"; export * from "./routes/messages.client"; +export * from "./routes/onboarding/onboarding.client"; export * from "./routes/organizations/organizations.client"; export * from "./routes/users.client"; diff --git a/packages/api/src/routes/agent-request.server.ts b/packages/api/src/routes/agent-request.server.ts index 809ccd6..69bf430 100644 --- a/packages/api/src/routes/agent-request.server.ts +++ b/packages/api/src/routes/agent-request.server.ts @@ -1,5 +1,7 @@ import { BlinkInvocationTokenHeader } from "@blink.so/runtime/types"; +import { createHmac, timingSafeEqual } from "node:crypto"; import type { Context } from "hono"; + import type { Bindings } from "../server"; import { detectRequestLocation } from "../server-helper"; import { generateAgentInvocationToken } from "./agents/me/me.server"; @@ -23,7 +25,48 @@ export default async function handleAgentRequest( } return c.json({ message: "No agent exists for this webook" }, 404); } + + const incomingUrl = new URL(c.req.raw.url); + + // Detect if this is a Slack request (works for both webhook and subdomain routing) + const isSlackPath = + (routing.mode === "webhook" && routing.subpath === "/slack") || + (routing.mode === "subdomain" && incomingUrl.pathname === "/slack"); + + // Handle Slack verification tracking if active + const slackVerification = query.agent?.slack_verification; + let requestBodyText: string | undefined; + + if (isSlackPath && slackVerification) { + // Read the body for verification processing + requestBodyText = await c.req.text(); + + const result = await processSlackVerificationTracking( + db, + { id: query.agent.id, slack_verification: slackVerification }, + requestBodyText, + c.req.header("x-slack-signature"), + c.req.header("x-slack-request-timestamp") + ); + + // URL verification challenge must be responded to immediately + if (result.challengeResponse) { + return c.json({ challenge: result.challengeResponse }); + } + + // Invalid signature - acknowledge but don't process further + if (!result.signatureValid) { + return c.json({ ok: true }); + } + + // Otherwise, continue to forward to agent (if deployed) + } + if (!query.agent_deployment) { + // No deployment - if this was a valid Slack request, we already tracked it + if (isSlackPath && slackVerification) { + return c.json({ ok: true }); // Acknowledge Slack event + } return c.json( { message: `No deployment exists for this agent. Be sure to deploy your agent to receive webhook events`, @@ -38,7 +81,6 @@ export default async function handleAgentRequest( 404 ); } - const incomingUrl = new URL(c.req.raw.url); let url: URL; if (routing.mode === "webhook") { @@ -133,6 +175,17 @@ export default async function handleAgentRequest( c.req.raw.headers.forEach((value, key) => { headers.set(key, value); }); + + // If we read the body as text (for Slack verification), we need to recalculate + // the Content-Length header. When fetch() sends a string body, it encodes it as + // UTF-8, which may have a different byte length than the original Content-Length. + // Note: Some runtimes (like Bun) auto-correct this, but Node.js throws an error + // if Content-Length doesn't match the actual body length. + if (requestBodyText !== undefined) { + const encoder = new TextEncoder(); + const byteLength = encoder.encode(requestBodyText).length; + headers.set("content-length", byteLength.toString()); + } // Strip cookies from webhook requests to prevent session leakage // Subdomain requests are on a different origin, so cookies won't be sent anyway if (routing.mode === "webhook") { @@ -150,8 +203,11 @@ export default async function handleAgentRequest( let response: Response | undefined; let error: string | undefined; try { + // Use the body we already read if it's a Slack request, otherwise use the stream + const bodyToSend = + requestBodyText !== undefined ? requestBodyText : c.req.raw.body; response = await fetch(url, { - body: c.req.raw.body, + body: bodyToSend, method: c.req.raw.method, signal, headers, @@ -295,3 +351,156 @@ export default async function handleAgentRequest( ); } } + +/** + * Verify Slack request signature using HMAC-SHA256. + */ +function verifySlackSignature( + signingSecret: string, + timestamp: string, + body: string, + signature: string +): boolean { + const time = Math.floor(Date.now() / 1000); + const requestTimestamp = Number.parseInt(timestamp, 10); + + // Request is older than 5 minutes - reject to prevent replay attacks + if (Math.abs(time - requestTimestamp) > 60 * 5) { + return false; + } + + const hmac = createHmac("sha256", signingSecret); + const sigBasestring = `v0:${timestamp}:${body}`; + hmac.update(sigBasestring); + const mySignature = `v0=${hmac.digest("hex")}`; + + try { + return timingSafeEqual(Buffer.from(mySignature), Buffer.from(signature)); + } catch { + return false; + } +} + +/** + * Process Slack verification tracking without blocking the request flow. + * Returns tracking results so the caller can decide how to proceed. + */ +async function processSlackVerificationTracking( + db: Awaited>, + agent: { + id: string; + slack_verification: { + signingSecret: string; + botToken: string; + startedAt: string; + lastEventAt?: string; + dmReceivedAt?: string; + dmChannel?: string; + signatureFailedAt?: string; + }; + }, + body: string, + slackSignature: string | undefined, + slackTimestamp: string | undefined +): Promise<{ + signatureValid: boolean; + challengeResponse?: string; +}> { + const verification = agent.slack_verification; + + // Verify Slack signature if headers are present + if (slackSignature && slackTimestamp) { + if ( + !verifySlackSignature( + verification.signingSecret, + slackTimestamp, + body, + slackSignature + ) + ) { + // Signature verification failed - record in database + await db.updateAgent({ + id: agent.id, + slack_verification: { + ...verification, + signatureFailedAt: new Date().toISOString(), + }, + }); + return { signatureValid: false }; + } + } + + // Parse the payload + let payload: { + type?: string; + challenge?: string; + event?: { + type?: string; + channel_type?: string; + channel?: string; + bot_id?: string; + ts?: string; + }; + }; + + try { + payload = JSON.parse(body); + } catch { + // Can't parse - treat as invalid but not a security issue + return { signatureValid: true }; + } + + // Handle Slack URL verification challenge + if (payload.type === "url_verification" && payload.challenge) { + // Update lastEventAt since we received a valid event + await db.updateAgent({ + id: agent.id, + slack_verification: { + ...verification, + lastEventAt: new Date().toISOString(), + }, + }); + return { signatureValid: true, challengeResponse: payload.challenge }; + } + + // Track if we received a DM + const isDM = + payload.event?.type === "message" && + payload.event.channel_type === "im" && + !payload.event.bot_id; // Ignore bot's own messages + + // If this is a DM and we haven't already recorded one, send a response to Slack + if (isDM && !verification.dmReceivedAt && payload.event?.channel) { + await fetch("https://slack.com/api/chat.postMessage", { + method: "POST", + headers: { + Authorization: `Bearer ${verification.botToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + channel: payload.event.channel, + thread_ts: payload.event.ts, + text: "Congrats, your Slack app is set up! You can now go back to the Blink dashboard.", + }), + }).catch(() => { + // Silent fail - user will see status in the UI + }); + } + + const updatedVerification = { + ...verification, + lastEventAt: new Date().toISOString(), + ...(isDM && { + dmReceivedAt: new Date().toISOString(), + dmChannel: payload.event?.channel, + }), + }; + + await db.updateAgent({ + id: agent.id, + slack_verification: updatedVerification, + }); + + // Continue to agent - we've tracked the event + return { signatureValid: true }; +} diff --git a/packages/api/src/routes/agent-request.test.ts b/packages/api/src/routes/agent-request.test.ts index c6352ff..731e6aa 100644 --- a/packages/api/src/routes/agent-request.test.ts +++ b/packages/api/src/routes/agent-request.test.ts @@ -4,10 +4,17 @@ import { serve } from "../test"; interface SetupAgentOptions { name: string; - handler: (req: Request) => Response; + handler: (req: Request) => Response | Promise; + /** If provided, sets up slack_verification on the agent */ + slackVerification?: { + signingSecret: string; + botToken: string; + }; } interface SetupAgentResult extends Disposable { + /** The agent ID */ + agentId: string; /** Subpath webhook URL (/api/webhook/:id) - cookies are stripped */ webhookUrl: string; getWebhookUrl: (subpath?: string) => string; @@ -74,6 +81,19 @@ async function setupAgent( if (!agent.request_url) throw new Error("No webhook route"); const db = await bindings.database(); + + // Set up slack verification if provided + if (options.slackVerification) { + await db.updateAgent({ + id: agent.id, + slack_verification: { + signingSecret: options.slackVerification.signingSecret, + botToken: options.slackVerification.botToken, + startedAt: new Date().toISOString(), + }, + }); + } + const target = await db.selectAgentDeploymentTargetByName( agent.id, "production" @@ -85,6 +105,7 @@ async function setupAgent( const subdomainHost = `${requestId}.${parsedApiUrl.host}`; return { + agentId: agent.id, webhookUrl: `${apiUrl}/api/webhook/${target.request_id}`, getWebhookUrl: (subpath?: string) => `${apiUrl}/api/webhook/${target.request_id}${subpath || ""}`, @@ -378,6 +399,57 @@ describe("webhook requests (/api/webhook/:id)", () => { }); }); +describe("Slack request Content-Length handling", () => { + test("recalculates Content-Length when body is read as text for Slack verification", async () => { + // Body with multi-byte UTF-8 characters + // "Hello δΈ–η•Œ" is 6 ASCII chars (6 bytes) + 1 space (1 byte) + 2 Chinese chars (6 bytes) = 13 bytes + const bodyWithMultiByteChars = JSON.stringify({ + type: "event_callback", + event: { type: "message", text: "Hello δΈ–η•Œ" }, + }); + const expectedByteLength = new TextEncoder().encode( + bodyWithMultiByteChars + ).length; + + let receivedContentLength: string | null | undefined; + let receivedBodyLength: number | undefined; + + using agent = await setupAgent({ + name: "slack-content-length", + handler: async (req) => { + receivedContentLength = req.headers.get("content-length"); + const body = await req.text(); + receivedBodyLength = new TextEncoder().encode(body).length; + return new Response("OK"); + }, + slackVerification: { + signingSecret: "test-secret", + botToken: "xoxb-test-token", + }, + }); + + // Send request with a Content-Length that would be wrong if we used string length + // instead of byte length (string length is different from byte length for multi-byte chars) + const response = await fetch(agent.getWebhookUrl("/slack"), { + method: "POST", + headers: { + "content-type": "application/json", + // Intentionally set a wrong Content-Length to verify it gets corrected + "content-length": String(bodyWithMultiByteChars.length), + }, + body: bodyWithMultiByteChars, + }); + + // The request should succeed (we won't have valid Slack signature, but that's OK for this test) + // What we're testing is that the Content-Length was recalculated correctly + expect(response.status).toBe(200); + + // Verify the Content-Length header received by the agent matches actual byte length + expect(receivedContentLength).toBe(String(expectedByteLength)); + expect(receivedBodyLength).toBe(expectedByteLength); + }); +}); + describe("subdomain requests", () => { test("basic request", async () => { using agent = await setupAgent({ diff --git a/packages/api/src/routes/agents/agents.client.ts b/packages/api/src/routes/agents/agents.client.ts index 2d75ecb..2ccc99c 100644 --- a/packages/api/src/routes/agents/agents.client.ts +++ b/packages/api/src/routes/agents/agents.client.ts @@ -19,6 +19,8 @@ import AgentEnv, { schemaCreateAgentEnv } from "./env.client"; import AgentLogs from "./logs.client"; import AgentMembers from "./members.client"; import AgentRuns from "./runs.client"; +import AgentSetupGitHub from "./setup-github.client"; +import AgentSetupSlack from "./setup-slack.client"; import AgentSteps from "./steps.client"; import AgentTraces from "./traces.client"; @@ -28,6 +30,68 @@ export const schemaAgentVisibility = z.enum([ "organization", ]); +export const schemaOnboardingStep = z.enum([ + "welcome", + "llm-api-keys", + "github-setup", + "slack-setup", + "web-search", + "deploying", + "success", +]); + +export const schemaOnboardingState = z.object({ + currentStep: schemaOnboardingStep, + finished: z.boolean().optional(), + github: z + .object({ + appName: z.string(), + appUrl: z.string(), + installUrl: z.string(), + // Credentials (values) + appId: z.string().optional(), + clientId: z.string().optional(), + clientSecret: z.string().optional(), + webhookSecret: z.string().optional(), + privateKey: z.string().optional(), + // Env var names used + envVars: z + .object({ + appId: z.string(), + clientId: z.string(), + clientSecret: z.string(), + webhookSecret: z.string(), + privateKey: z.string(), + }) + .optional(), + }) + .optional(), + slack: z + .object({ + botToken: z.string(), + signingSecret: z.string(), + envVars: z + .object({ + botToken: z.string(), + signingSecret: z.string(), + }) + .optional(), + }) + .optional(), + apiKeys: z + .object({ + aiProvider: z.enum(["anthropic", "openai", "vercel"]).optional(), + aiApiKey: z.string().optional(), + aiEnvVar: z.string().optional(), + exaApiKey: z.string().optional(), + exaEnvVar: z.string().optional(), + }) + .optional(), +}); + +export type OnboardingState = z.infer; +export type OnboardingStep = z.infer; + export const schemaCreateAgentRequest = z.object({ organization_id: z.uuid(), name: z.string().regex(nameFormat), @@ -48,6 +112,9 @@ export const schemaCreateAgentRequest = z.object({ // Optional: Specify the request_id for the production deployment target. // This is useful for setting up webhooks before the agent is fully deployed. request_id: z.uuid().optional(), + + // Optional: Initialize agent with onboarding state + onboarding_state: schemaOnboardingState.optional(), }); export type CreateAgentRequest = z.infer; @@ -70,6 +137,7 @@ export const schemaAgent = z.object({ .describe("The URL for the agent requests. Only visible to owners."), chat_expire_ttl: z.number().int().positive().nullable(), user_permission: z.enum(["read", "write", "admin"]).optional(), + onboarding_state: schemaOnboardingState.nullable(), }); export const schemaUpdateAgentRequest = z.object({ @@ -165,6 +233,8 @@ export default class Agents { public readonly logs: AgentLogs; public readonly traces: AgentTraces; public readonly members: AgentMembers; + public readonly setupGitHub: AgentSetupGitHub; + public readonly setupSlack: AgentSetupSlack; public constructor(client: Client) { this.client = client; @@ -175,6 +245,8 @@ export default class Agents { this.logs = new AgentLogs(client); this.traces = new AgentTraces(client); this.members = new AgentMembers(client); + this.setupGitHub = new AgentSetupGitHub(client); + this.setupSlack = new AgentSetupSlack(client); } /** @@ -351,6 +423,39 @@ export default class Agents { await assertResponseStatus(resp, 200); return resp.json(); } + + /** + * Update onboarding state for an agent. + * + * @param id - The id of the agent. + * @param state - The partial onboarding state to merge. + * @returns The updated agent. + */ + public async updateOnboarding( + id: string, + state: Partial + ): Promise { + const resp = await this.client.request( + "PATCH", + `/api/agents/${id}/onboarding`, + JSON.stringify(state) + ); + await assertResponseStatus(resp, 200); + return resp.json(); + } + + /** + * Clear onboarding state for an agent (mark onboarding as complete). + * + * @param id - The id of the agent. + */ + public async clearOnboarding(id: string): Promise { + const resp = await this.client.request( + "DELETE", + `/api/agents/${id}/onboarding` + ); + await assertResponseStatus(resp, 204); + } } export * from "./deployments.client"; diff --git a/packages/api/src/routes/agents/agents.server.ts b/packages/api/src/routes/agents/agents.server.ts index 9a5b86d..0722af9 100644 --- a/packages/api/src/routes/agents/agents.server.ts +++ b/packages/api/src/routes/agents/agents.server.ts @@ -28,6 +28,8 @@ import mountLogs from "./logs.server"; import mountAgentsMe from "./me/me.server"; import mountAgentMembers from "./members.server"; import mountRuns from "./runs.server"; +import mountSetupGitHub from "./setup-github.server"; +import mountSetupSlack from "./setup-slack.server"; import mountSteps from "./steps.server"; import mountTraces from "./traces.server"; @@ -52,6 +54,7 @@ export default function mountAgents(app: Hono<{ Bindings: Bindings }>) { description: req.description, visibility: req.visibility ?? "organization", chat_expire_ttl: req.chat_expire_ttl, + onboarding_state: req.onboarding_state, }); // Grant admin permission to the creator @@ -258,6 +261,56 @@ export default function mountAgents(app: Hono<{ Bindings: Bindings }>) { return c.body(null, 204); }); + // Update onboarding state for an agent. + app.patch( + "/:agent_id/onboarding", + withAuth, + withAgentURLParam, + withAgentPermission("write"), + async (c) => { + const agent = c.get("agent"); + const db = await c.env.database(); + const body = await c.req.json(); + + // Merge the new state with existing state + const currentState = agent.onboarding_state ?? { currentStep: "welcome" }; + const newState = { ...currentState, ...body }; + + const updated = await db.updateAgent({ + id: agent.id, + onboarding_state: newState, + }); + return c.json( + convert.agent( + updated, + await createAgentRequestURL(c, updated), + await getAgentUserPermission(c, updated) + ) + ); + } + ); + + // Clear onboarding state for an agent (mark onboarding complete). + app.delete( + "/:agent_id/onboarding", + withAuth, + withAgentURLParam, + withAgentPermission("write"), + async (c) => { + const agent = c.get("agent"); + const db = await c.env.database(); + + // Only update if there's an existing onboarding state + if (agent.onboarding_state) { + await db.updateAgent({ + id: agent.id, + onboarding_state: { ...agent.onboarding_state, finished: true }, + }); + } + return c.body(null, 204); + } + ); + // Delete an agent. app.delete( "/:agent_id", @@ -417,6 +470,8 @@ export default function mountAgents(app: Hono<{ Bindings: Bindings }>) { mountLogs(app.basePath("/:agent_id/logs")); mountTraces(app.basePath("/:agent_id/traces")); mountAgentMembers(app.basePath("/:agent_id/members")); + mountSetupGitHub(app.basePath("/:agent_id/setup/github")); + mountSetupSlack(app.basePath("/:agent_id/setup/slack")); // This is special - just for the agent invocation API. // We don't like to do this, but we do because this API diff --git a/packages/api/src/routes/agents/setup-github.client.ts b/packages/api/src/routes/agents/setup-github.client.ts new file mode 100644 index 0000000..ac98459 --- /dev/null +++ b/packages/api/src/routes/agents/setup-github.client.ts @@ -0,0 +1,187 @@ +import { z } from "zod"; +import type Client from "../../client.browser"; +import { assertResponseStatus } from "../../client-helper"; + +// GitHub App data returned from GitHub after creation +export const schemaGitHubAppData = z.object({ + id: z.number(), + client_id: z.string(), + client_secret: z.string(), + webhook_secret: z.string(), + pem: z.string(), + name: z.string(), + html_url: z.string(), + slug: z.string(), +}); + +export type GitHubAppData = z.infer; + +// Start creation request/response +export const schemaStartGitHubAppCreationRequest = z.object({ + name: z.string().min(1).max(34), + organization: z.string().optional(), +}); + +export type StartGitHubAppCreationRequest = z.infer< + typeof schemaStartGitHubAppCreationRequest +>; + +export const schemaStartGitHubAppCreationResponse = z.object({ + manifest: z.string(), + github_url: z.string(), + session_id: z.string(), +}); + +export type StartGitHubAppCreationResponse = z.infer< + typeof schemaStartGitHubAppCreationResponse +>; + +// GitHub credentials returned when status is completed +// These should be saved as env vars by the client +export const schemaGitHubAppCredentials = z.object({ + app_id: z.number(), + client_id: z.string(), + client_secret: z.string(), + webhook_secret: z.string(), + private_key: z.string(), // base64-encoded PEM +}); + +export type GitHubAppCredentials = z.infer; + +// Creation status response +// Status flow: pending -> app_created -> completed +// - pending: waiting for user to create app on GitHub +// - app_created: app created, waiting for user to install it +// - completed: app created and installed +// - failed/expired: error states +export const schemaGitHubAppCreationStatusResponse = z.object({ + status: z.enum(["pending", "app_created", "completed", "failed", "expired"]), + error: z.string().optional(), + app_data: z + .object({ + id: z.number(), + name: z.string(), + html_url: z.string(), + slug: z.string(), + }) + .optional(), + // Credentials are only included when status is "completed" + credentials: schemaGitHubAppCredentials.optional(), +}); + +export type GitHubAppCreationStatusResponse = z.infer< + typeof schemaGitHubAppCreationStatusResponse +>; + +// Complete creation request +export const schemaCompleteGitHubAppCreationRequest = z.object({ + session_id: z.string(), +}); + +export type CompleteGitHubAppCreationRequest = z.infer< + typeof schemaCompleteGitHubAppCreationRequest +>; + +export const schemaCompleteGitHubAppCreationResponse = z.object({ + success: z.boolean(), + app_name: z.string().optional(), + app_url: z.string().optional(), + install_url: z.string().optional(), +}); + +export type CompleteGitHubAppCreationResponse = z.infer< + typeof schemaCompleteGitHubAppCreationResponse +>; + +// Webhook URL response +export const schemaGitHubWebhookUrlResponse = z.object({ + webhook_url: z.string(), +}); + +export type GitHubWebhookUrlResponse = z.infer< + typeof schemaGitHubWebhookUrlResponse +>; + +export default class AgentSetupGitHub { + private readonly client: Client; + + public constructor(client: Client) { + this.client = client; + } + + /** + * Get the webhook URL for GitHub integration. + * This doesn't require any credentials and can be called before setup. + */ + public async getWebhookUrl( + agentId: string + ): Promise { + const resp = await this.client.request( + "GET", + `/api/agents/${agentId}/setup/github/webhook-url` + ); + await assertResponseStatus(resp, 200); + return resp.json(); + } + + /** + * Start GitHub App creation for an agent. + * Returns a URL to redirect the user to GitHub for app creation. + */ + public async startCreation( + agentId: string, + request: StartGitHubAppCreationRequest + ): Promise { + const resp = await this.client.request( + "POST", + `/api/agents/${agentId}/setup/github/start-creation`, + JSON.stringify(request) + ); + await assertResponseStatus(resp, 200); + return resp.json(); + } + + /** + * Get the current creation status. + * Poll this endpoint to check if the GitHub callback has been received. + */ + public async getCreationStatus( + agentId: string, + sessionId: string + ): Promise { + const resp = await this.client.request( + "GET", + `/api/agents/${agentId}/setup/github/creation-status/${sessionId}` + ); + await assertResponseStatus(resp, 200); + return resp.json(); + } + + /** + * Complete GitHub App creation and save credentials. + * Call this after the status shows "completed" to persist the credentials. + */ + public async completeCreation( + agentId: string, + request: CompleteGitHubAppCreationRequest + ): Promise { + const resp = await this.client.request( + "POST", + `/api/agents/${agentId}/setup/github/complete-creation`, + JSON.stringify(request) + ); + await assertResponseStatus(resp, 200); + return resp.json(); + } + + /** + * Cancel ongoing GitHub App creation. + */ + public async cancelCreation(agentId: string): Promise { + const resp = await this.client.request( + "POST", + `/api/agents/${agentId}/setup/github/cancel-creation` + ); + await assertResponseStatus(resp, 204); + } +} diff --git a/packages/api/src/routes/agents/setup-github.server.test.ts b/packages/api/src/routes/agents/setup-github.server.test.ts new file mode 100644 index 0000000..a1daed1 --- /dev/null +++ b/packages/api/src/routes/agents/setup-github.server.test.ts @@ -0,0 +1,358 @@ +import { describe, expect, test } from "bun:test"; +import { serve } from "../../test"; + +describe("GitHub App Setup", () => { + test("start-creation returns manifest and github URL", async () => { + const { helpers } = await serve({ + bindings: { + accessUrl: new URL("https://test.blink.so"), + }, + }); + const { client } = await helpers.createUser(); + const org = await client.organizations.create({ + name: "test-org", + }); + const agent = await client.agents.create({ + name: "test-agent", + organization_id: org.id, + }); + + const result = await client.agents.setupGitHub.startCreation(agent.id, { + name: "my-github-app", + }); + + expect(result.session_id).toBeDefined(); + expect(result.github_url).toBe("https://github.com/settings/apps/new"); + expect(result.manifest).toBeDefined(); + + const manifest = JSON.parse(result.manifest); + expect(manifest.name).toBe("my-github-app"); + expect(manifest.public).toBe(false); + expect(manifest.default_permissions).toEqual({ + contents: "write", + issues: "write", + pull_requests: "write", + metadata: "read", + }); + }); + + test("start-creation with organization returns organization github URL", async () => { + const { helpers } = await serve({ + bindings: { + accessUrl: new URL("https://test.blink.so"), + }, + }); + const { client } = await helpers.createUser(); + const org = await client.organizations.create({ + name: "test-org", + }); + const agent = await client.agents.create({ + name: "test-agent", + organization_id: org.id, + }); + + const result = await client.agents.setupGitHub.startCreation(agent.id, { + name: "my-github-app", + organization: "my-gh-org", + }); + + expect(result.github_url).toBe( + "https://github.com/organizations/my-gh-org/settings/apps/new" + ); + }); + + test("get-creation-status returns pending for new session", async () => { + const { helpers } = await serve({ + bindings: { + accessUrl: new URL("https://test.blink.so"), + }, + }); + const { client } = await helpers.createUser(); + const org = await client.organizations.create({ + name: "test-org", + }); + const agent = await client.agents.create({ + name: "test-agent", + organization_id: org.id, + }); + + const startResult = await client.agents.setupGitHub.startCreation( + agent.id, + { + name: "my-github-app", + } + ); + + const status = await client.agents.setupGitHub.getCreationStatus( + agent.id, + startResult.session_id + ); + + expect(status.status).toBe("pending"); + expect(status.app_data).toBeUndefined(); + expect(status.credentials).toBeUndefined(); + }); + + test("get-creation-status returns expired for invalid session", async () => { + const { helpers } = await serve({ + bindings: { + accessUrl: new URL("https://test.blink.so"), + }, + }); + const { client } = await helpers.createUser(); + const org = await client.organizations.create({ + name: "test-org", + }); + const agent = await client.agents.create({ + name: "test-agent", + organization_id: org.id, + }); + + const status = await client.agents.setupGitHub.getCreationStatus( + agent.id, + "invalid-session-id" + ); + + expect(status.status).toBe("expired"); + }); + + test("cancel-creation clears setup state", async () => { + const { helpers } = await serve({ + bindings: { + accessUrl: new URL("https://test.blink.so"), + }, + }); + const { client } = await helpers.createUser(); + const org = await client.organizations.create({ + name: "test-org", + }); + const agent = await client.agents.create({ + name: "test-agent", + organization_id: org.id, + }); + + const startResult = await client.agents.setupGitHub.startCreation( + agent.id, + { + name: "my-github-app", + } + ); + + // Verify setup is in progress + let status = await client.agents.setupGitHub.getCreationStatus( + agent.id, + startResult.session_id + ); + expect(status.status).toBe("pending"); + + // Cancel + await client.agents.setupGitHub.cancelCreation(agent.id); + + // Verify setup is cleared + status = await client.agents.setupGitHub.getCreationStatus( + agent.id, + startResult.session_id + ); + expect(status.status).toBe("expired"); + }); + + test("complete-creation fails for pending session", async () => { + const { helpers } = await serve({ + bindings: { + accessUrl: new URL("https://test.blink.so"), + }, + }); + const { client } = await helpers.createUser(); + const org = await client.organizations.create({ + name: "test-org", + }); + const agent = await client.agents.create({ + name: "test-agent", + organization_id: org.id, + }); + + const startResult = await client.agents.setupGitHub.startCreation( + agent.id, + { + name: "my-github-app", + } + ); + + await expect( + client.agents.setupGitHub.completeCreation(agent.id, { + session_id: startResult.session_id, + }) + ).rejects.toThrow("GitHub App creation not completed"); + }); + + test("get-creation-status returns credentials when completed", async () => { + const { helpers, bindings } = await serve({ + bindings: { + accessUrl: new URL("https://test.blink.so"), + }, + }); + const { client } = await helpers.createUser(); + const org = await client.organizations.create({ + name: "test-org", + }); + const agent = await client.agents.create({ + name: "test-agent", + organization_id: org.id, + }); + + const startResult = await client.agents.setupGitHub.startCreation( + agent.id, + { + name: "my-github-app", + } + ); + + // Simulate GitHub callback by directly updating the agent's setup state + const db = await bindings.database(); + const agentData = await db.selectAgentByID(agent.id); + if (!agentData || !agentData.github_app_setup) { + throw new Error("Agent or setup not found"); + } + const setup = agentData.github_app_setup; + + await db.updateAgent({ + id: agent.id, + github_app_setup: { + ...setup, + status: "completed", + appData: { + id: 12345, + clientId: "Iv1.abc123", + clientSecret: "secret123", + webhookSecret: "webhook-secret", + pem: "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----", + name: "my-github-app", + htmlUrl: "https://github.com/apps/my-github-app", + slug: "my-github-app", + }, + }, + }); + + // Now get status should return credentials + const status = await client.agents.setupGitHub.getCreationStatus( + agent.id, + startResult.session_id + ); + + expect(status.status).toBe("completed"); + expect(status.app_data).toEqual({ + id: 12345, + name: "my-github-app", + html_url: "https://github.com/apps/my-github-app", + slug: "my-github-app", + }); + expect(status.credentials).toBeDefined(); + if (!status.credentials) { + throw new Error("Credentials should be defined"); + } + expect(status.credentials.app_id).toBe(12345); + expect(status.credentials.client_id).toBe("Iv1.abc123"); + expect(status.credentials.client_secret).toBe("secret123"); + expect(status.credentials.webhook_secret).toBe("webhook-secret"); + // Private key should be base64 encoded + expect(status.credentials.private_key).toBe( + btoa( + "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----" + ) + ); + }); + + test("complete-creation clears setup state when completed", async () => { + const { helpers, bindings } = await serve({ + bindings: { + accessUrl: new URL("https://test.blink.so"), + }, + }); + const { client } = await helpers.createUser(); + const org = await client.organizations.create({ + name: "test-org", + }); + const agent = await client.agents.create({ + name: "test-agent", + organization_id: org.id, + }); + + const startResult = await client.agents.setupGitHub.startCreation( + agent.id, + { + name: "my-github-app", + } + ); + + // Simulate completed setup + const db = await bindings.database(); + const agentData = await db.selectAgentByID(agent.id); + if (!agentData || !agentData.github_app_setup) { + throw new Error("Agent or setup not found"); + } + const setup = agentData.github_app_setup; + + await db.updateAgent({ + id: agent.id, + github_app_setup: { + ...setup, + status: "completed", + appData: { + id: 12345, + clientId: "Iv1.abc123", + clientSecret: "secret123", + webhookSecret: "webhook-secret", + pem: "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----", + name: "my-github-app", + htmlUrl: "https://github.com/apps/my-github-app", + slug: "my-github-app", + }, + }, + }); + + // Complete the creation + const result = await client.agents.setupGitHub.completeCreation(agent.id, { + session_id: startResult.session_id, + }); + + expect(result.success).toBe(true); + expect(result.app_name).toBe("my-github-app"); + expect(result.app_url).toBe("https://github.com/apps/my-github-app"); + expect(result.install_url).toBe( + "https://github.com/apps/my-github-app/installations/new" + ); + + // Verify setup state is cleared + const status = await client.agents.setupGitHub.getCreationStatus( + agent.id, + startResult.session_id + ); + expect(status.status).toBe("expired"); + + // Verify no env vars were created (that's now the client's responsibility) + const envVars = await client.agents.env.list({ agent_id: agent.id }); + expect(envVars.length).toBe(0); + }); + + test("get-webhook-url returns correct URL", async () => { + const { helpers } = await serve({ + bindings: { + accessUrl: new URL("https://test.blink.so"), + }, + }); + const { client } = await helpers.createUser(); + const org = await client.organizations.create({ + name: "test-org", + }); + const agent = await client.agents.create({ + name: "test-agent", + organization_id: org.id, + }); + + const result = await client.agents.setupGitHub.getWebhookUrl(agent.id); + + expect(result.webhook_url).toBeDefined(); + expect(result.webhook_url).toContain("https://test.blink.so/api/webhook/"); + expect(result.webhook_url).toContain("/github"); + }); +}); diff --git a/packages/api/src/routes/agents/setup-github.server.ts b/packages/api/src/routes/agents/setup-github.server.ts new file mode 100644 index 0000000..676f2e5 --- /dev/null +++ b/packages/api/src/routes/agents/setup-github.server.ts @@ -0,0 +1,553 @@ +import type { Hono } from "hono"; +import { HTTPException } from "hono/http-exception"; +import { validator } from "hono/validator"; + +import { + withAgentPermission, + withAgentURLParam, + withAuth, +} from "../../middleware"; +import type { Bindings } from "../../server"; +import { + type CompleteGitHubAppCreationResponse, + type GitHubAppCreationStatusResponse, + type StartGitHubAppCreationResponse, + schemaCompleteGitHubAppCreationRequest, + schemaGitHubAppData, + schemaStartGitHubAppCreationRequest, +} from "./setup-github.client"; + +// 10 minute expiry for GitHub App creation sessions +const SESSION_EXPIRY_MS = 10 * 60 * 1000; + +/** + * Create the GitHub App manifest for the manifest flow. + */ +function createGitHubAppManifest( + name: string, + webhookUrl: string, + callbackUrl: string, + setupUrl: string +) { + return { + name, + url: "https://blink.so", + description: "A Blink agent for GitHub", + public: false, + redirect_url: callbackUrl, + setup_url: setupUrl, + setup_on_update: true, + hook_attributes: { + url: webhookUrl, + active: true, + }, + default_events: [ + "issues", + "issue_comment", + "pull_request", + "pull_request_review", + "pull_request_review_comment", + "push", + ], + default_permissions: { + contents: "write", + issues: "write", + pull_requests: "write", + metadata: "read", + }, + }; +} + +export default function mountSetupGitHub( + app: Hono<{ + Bindings: Bindings; + }> +) { + // Get webhook URL (no credentials required) + app.get( + "/webhook-url", + withAuth, + withAgentURLParam, + withAgentPermission("read"), + async (c) => { + const agent = c.get("agent"); + const db = await c.env.database(); + + // Get the agent's production deployment target for webhook URL + const target = await db.selectAgentDeploymentTargetByName( + agent.id, + "production" + ); + if (!target) { + return c.json({ error: "No deployment target found" }, 400); + } + + if (!c.env.accessUrl) { + return c.json( + { error: "Access URL not configured on this deployment" }, + 500 + ); + } + const webhookUrl = `${c.env.accessUrl.origin}/api/webhook/${target.request_id}/github`; + + return c.json({ webhook_url: webhookUrl }); + } + ); + + // Start GitHub App creation + app.post( + "/start-creation", + withAuth, + withAgentURLParam, + withAgentPermission("write"), + validator("json", (value) => { + return schemaStartGitHubAppCreationRequest.parse(value); + }), + async (c) => { + const agent = c.get("agent"); + const req = c.req.valid("json"); + const db = await c.env.database(); + + // Get the agent's production deployment target for webhook URL + const target = await db.selectAgentDeploymentTargetByName( + agent.id, + "production" + ); + if (!target) { + return c.json({ error: "No deployment target found" }, 400); + } + + if (!c.env.accessUrl) { + return c.json( + { error: "Access URL not configured on this deployment" }, + 500 + ); + } + + const webhookUrl = `${c.env.accessUrl.origin}/api/webhook/${target.request_id}/github`; + const sessionId = crypto.randomUUID(); + const now = new Date(); + const expiresAt = new Date(now.getTime() + SESSION_EXPIRY_MS); + + // Build the callback URL - this is where GitHub will redirect after app creation + const callbackUrl = `${c.env.accessUrl.origin}/api/agents/${agent.id}/setup/github/callback?session_id=${sessionId}`; + // Build the setup URL - this is where GitHub will redirect after app installation + const setupUrl = `${c.env.accessUrl.origin}/api/agents/${agent.id}/setup/github/setup-complete?session_id=${sessionId}`; + + // Create the manifest + const manifest = createGitHubAppManifest( + req.name, + webhookUrl, + callbackUrl, + setupUrl + ); + + // Store setup state + await db.updateAgent({ + id: agent.id, + github_app_setup: { + sessionId, + manifestName: req.name, + organization: req.organization, + startedAt: now.toISOString(), + expiresAt: expiresAt.toISOString(), + status: "pending", + }, + }); + + // Return the manifest and GitHub URL for the frontend to submit + const githubUrl = req.organization + ? `https://github.com/organizations/${req.organization}/settings/apps/new` + : `https://github.com/settings/apps/new`; + + const response: StartGitHubAppCreationResponse = { + manifest: JSON.stringify(manifest), + github_url: githubUrl, + session_id: sessionId, + }; + return c.json(response); + } + ); + + // GitHub callback - receives the code after app creation + // This endpoint is PUBLIC (no auth) because GitHub redirects the user's browser here + // Security is provided by validating the session_id + app.get("/callback", async (c) => { + const agentId = c.req.param("agent_id"); + if (!agentId) { + return c.html(createCallbackHtml("error", "Agent ID is required")); + } + + const db = await c.env.database(); + const agent = await db.selectAgentByID(agentId); + if (!agent) { + return c.html(createCallbackHtml("error", "Agent not found")); + } + + const sessionId = c.req.query("session_id"); + const code = c.req.query("code"); + + // Validate session - this provides security for this public endpoint + const setup = agent.github_app_setup; + if (!setup || setup.sessionId !== sessionId) { + return c.html( + createCallbackHtml( + "error", + "Invalid or expired session. Please restart the GitHub App setup." + ) + ); + } + + // Check expiry + if (new Date() > new Date(setup.expiresAt)) { + await db.updateAgent({ + id: agent.id, + github_app_setup: { + ...setup, + status: "failed", + error: "Session expired", + }, + }); + return c.html( + createCallbackHtml( + "error", + "Session expired. Please restart the GitHub App setup." + ) + ); + } + + if (!code) { + await db.updateAgent({ + id: agent.id, + github_app_setup: { + ...setup, + status: "failed", + error: "No code received from GitHub", + }, + }); + return c.html( + createCallbackHtml( + "error", + "No authorization code received from GitHub." + ) + ); + } + + try { + // Exchange the code for credentials + const res = await fetch( + `https://api.github.com/app-manifests/${code}/conversions`, + { + method: "POST", + headers: { + Accept: "application/vnd.github+json", + "User-Agent": "blink.so", + "X-GitHub-Api-Version": "2022-11-28", + }, + } + ); + + if (!res.ok) { + const errorText = await res.text(); + throw new Error( + `GitHub API error: ${res.status} ${res.statusText}${errorText ? ` - ${errorText}` : ""}` + ); + } + + const rawData = await res.json(); + const data = schemaGitHubAppData.parse(rawData); + + // Store the app data in the session (status stays "pending" until installation) + await db.updateAgent({ + id: agent.id, + github_app_setup: { + ...setup, + status: "app_created", + appData: { + id: data.id, + clientId: data.client_id, + clientSecret: data.client_secret, + webhookSecret: data.webhook_secret, + pem: data.pem, + name: data.name, + htmlUrl: data.html_url, + slug: data.slug, + }, + }, + }); + + // Redirect to the app's installation page + const installUrl = `${data.html_url}/installations/new`; + return c.redirect(installUrl); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + + await db.updateAgent({ + id: agent.id, + github_app_setup: { + ...setup, + status: "failed", + error: errorMessage, + }, + }); + + return c.html( + createCallbackHtml( + "error", + `Failed to create GitHub App: ${errorMessage}` + ) + ); + } + }); + + // Setup complete - this is the Setup URL that GitHub redirects to after app installation + // This endpoint is PUBLIC (no auth) because GitHub redirects the user's browser here + app.get("/setup-complete", async (c) => { + const agentId = c.req.param("agent_id"); + if (!agentId) { + return c.html( + createCallbackHtml("error", "Agent ID is required", undefined) + ); + } + + const db = await c.env.database(); + const agent = await db.selectAgentByID(agentId); + if (!agent) { + return c.html(createCallbackHtml("error", "Agent not found", undefined)); + } + + const sessionId = c.req.query("session_id"); + const installationId = c.req.query("installation_id"); + const setup = agent.github_app_setup; + + // Check if there's an active setup session + const hasActiveSession = setup && setup.sessionId === sessionId; + + if (hasActiveSession && setup.appData) { + // Update the setup with installation info if provided + if (installationId) { + await db.updateAgent({ + id: agent.id, + github_app_setup: { + ...setup, + status: "completed", + installationId: Number.parseInt(installationId, 10), + }, + }); + } else if (setup.status === "app_created") { + // Mark as completed even without installation_id (user might have installed) + await db.updateAgent({ + id: agent.id, + github_app_setup: { + ...setup, + status: "completed", + }, + }); + } + + return c.html( + createCallbackHtml( + "success", + `GitHub App "${setup.appData.name}" has been installed! Return to the setup wizard to continue.`, + true + ) + ); + } + + // No active session - just show a generic success message + return c.html( + createCallbackHtml("success", "GitHub App installation complete!", false) + ); + }); + + // Get creation status + app.get( + "/creation-status/:session_id", + withAuth, + withAgentURLParam, + withAgentPermission("read"), + async (c) => { + const agent = c.get("agent"); + const sessionId = c.req.param("session_id"); + const setup = agent.github_app_setup; + + if (!setup || setup.sessionId !== sessionId) { + return c.json({ status: "expired" as const }); + } + + // Check expiry for pending states + if ( + (setup.status === "pending" || setup.status === "app_created") && + new Date() > new Date(setup.expiresAt) + ) { + return c.json({ status: "expired" as const }); + } + + const response: GitHubAppCreationStatusResponse = { + status: setup.status, + error: setup.error, + app_data: setup.appData + ? { + id: setup.appData.id, + name: setup.appData.name, + html_url: setup.appData.htmlUrl, + slug: setup.appData.slug, + } + : undefined, + // Include full credentials only when status is completed + // so the client can save them as env vars + credentials: + setup.status === "completed" && setup.appData + ? { + app_id: setup.appData.id, + client_id: setup.appData.clientId, + client_secret: setup.appData.clientSecret, + webhook_secret: setup.appData.webhookSecret, + private_key: btoa(setup.appData.pem), + } + : undefined, + }; + return c.json(response); + } + ); + + // Complete creation - clear setup state (env vars are now saved client-side) + app.post( + "/complete-creation", + withAuth, + withAgentURLParam, + withAgentPermission("write"), + validator("json", (value) => { + return schemaCompleteGitHubAppCreationRequest.parse(value); + }), + async (c) => { + const agent = c.get("agent"); + const req = c.req.valid("json"); + const db = await c.env.database(); + + const setup = agent.github_app_setup; + if (!setup || setup.sessionId !== req.session_id) { + throw new HTTPException(400, { + message: "Invalid or expired session", + }); + } + + if (setup.status !== "completed" || !setup.appData) { + throw new HTTPException(400, { + message: "GitHub App creation not completed", + }); + } + + // Clear setup state + await db.updateAgent({ + id: agent.id, + github_app_setup: null, + }); + + const response: CompleteGitHubAppCreationResponse = { + success: true, + app_name: setup.appData.name, + app_url: setup.appData.htmlUrl, + install_url: `${setup.appData.htmlUrl}/installations/new`, + }; + return c.json(response); + } + ); + + // Cancel creation + app.post( + "/cancel-creation", + withAuth, + withAgentURLParam, + withAgentPermission("write"), + async (c) => { + const agent = c.get("agent"); + const db = await c.env.database(); + + // Clear setup state + await db.updateAgent({ + id: agent.id, + github_app_setup: null, + }); + + return c.body(null, 204); + } + ); +} + +/** + * Create HTML page for the callback response. + * @param showWizardHint - If true, shows a hint to return to the setup wizard + */ +function createCallbackHtml( + status: "success" | "error", + message: string, + showWizardHint?: boolean +): string { + const isSuccess = status === "success"; + const bgColor = isSuccess ? "#10b981" : "#ef4444"; + const icon = isSuccess + ? `` + : ``; + + const wizardHint = showWizardHint + ? `

You can close this window and return to the setup wizard.

` + : ""; + + return ` + + + + + GitHub App Setup - ${isSuccess ? "Success" : "Error"} + + + +
+
${icon}
+

${isSuccess ? "Success!" : "Something went wrong"}

+

${message}

+ ${wizardHint} +
+ +`; +} diff --git a/packages/api/src/routes/agents/setup-slack.client.ts b/packages/api/src/routes/agents/setup-slack.client.ts new file mode 100644 index 0000000..7f3e67b --- /dev/null +++ b/packages/api/src/routes/agents/setup-slack.client.ts @@ -0,0 +1,161 @@ +import { z } from "zod"; + +import { assertResponseStatus } from "../../client-helper"; +import type Client from "../../client.browser"; + +// Slack verification state stored on agent +export const schemaSlackVerification = z + .object({ + signingSecret: z.string(), + botToken: z.string(), + startedAt: z.string(), + lastEventAt: z.string().optional(), + dmReceivedAt: z.string().optional(), + dmChannel: z.string().optional(), + signatureFailedAt: z.string().optional(), + }) + .nullable(); + +export type SlackVerification = z.infer; + +// Start verification request/response +export const schemaStartSlackVerificationRequest = z.object({ + signing_secret: z.string().min(1), + bot_token: z.string().min(1), +}); + +export type StartSlackVerificationRequest = z.infer< + typeof schemaStartSlackVerificationRequest +>; + +export const schemaStartSlackVerificationResponse = z.object({ + webhook_url: z.string(), +}); + +export type StartSlackVerificationResponse = z.infer< + typeof schemaStartSlackVerificationResponse +>; + +// Verification status response +export const schemaSlackVerificationStatusResponse = z.object({ + active: z.boolean(), + started_at: z.string().optional(), + last_event_at: z.string().optional(), + dm_received: z.boolean(), + dm_channel: z.string().optional(), + signature_failed: z.boolean(), + signature_failed_at: z.string().optional(), +}); + +export type SlackVerificationStatusResponse = z.infer< + typeof schemaSlackVerificationStatusResponse +>; + +// Complete verification request +export const schemaCompleteSlackVerificationRequest = z.object({ + bot_token: z.string().min(1), + signing_secret: z.string().min(1), +}); + +export type CompleteSlackVerificationRequest = z.infer< + typeof schemaCompleteSlackVerificationRequest +>; + +export const schemaCompleteSlackVerificationResponse = z.object({ + success: z.boolean(), + bot_name: z.string().optional(), +}); + +export type CompleteSlackVerificationResponse = z.infer< + typeof schemaCompleteSlackVerificationResponse +>; + +// Webhook URL response +export const schemaSlackWebhookUrlResponse = z.object({ + webhook_url: z.string(), +}); + +export type SlackWebhookUrlResponse = z.infer< + typeof schemaSlackWebhookUrlResponse +>; + +export default class AgentSetupSlack { + private readonly client: Client; + + public constructor(client: Client) { + this.client = client; + } + + /** + * Get the webhook URL for Slack integration. + * This doesn't require any credentials and can be called before setup. + */ + public async getWebhookUrl( + agentId: string + ): Promise { + const resp = await this.client.request( + "GET", + `/api/agents/${agentId}/setup/slack/webhook-url` + ); + await assertResponseStatus(resp, 200); + return resp.json(); + } + + /** + * Start Slack verification for an agent. + * This sets up the webhook to listen for Slack events. + */ + public async startVerification( + agentId: string, + request: StartSlackVerificationRequest + ): Promise { + const resp = await this.client.request( + "POST", + `/api/agents/${agentId}/setup/slack/start-verification`, + JSON.stringify(request) + ); + await assertResponseStatus(resp, 200); + return resp.json(); + } + + /** + * Get the current verification status. + */ + public async getVerificationStatus( + agentId: string + ): Promise { + const resp = await this.client.request( + "GET", + `/api/agents/${agentId}/setup/slack/verification-status` + ); + await assertResponseStatus(resp, 200); + return resp.json(); + } + + /** + * Complete Slack verification and save credentials. + */ + public async completeVerification( + agentId: string, + request: CompleteSlackVerificationRequest + ): Promise { + const resp = await this.client.request( + "POST", + `/api/agents/${agentId}/setup/slack/complete-verification`, + JSON.stringify(request) + ); + await assertResponseStatus(resp, 200); + return resp.json(); + } + + /** + * Cancel ongoing Slack verification. + */ + public async cancelVerification(agentId: string): Promise { + const resp = await this.client.request( + "POST", + `/api/agents/${agentId}/setup/slack/cancel-verification` + ); + await assertResponseStatus(resp, 204); + } +} diff --git a/packages/api/src/routes/agents/setup-slack.server.ts b/packages/api/src/routes/agents/setup-slack.server.ts new file mode 100644 index 0000000..21c19c4 --- /dev/null +++ b/packages/api/src/routes/agents/setup-slack.server.ts @@ -0,0 +1,259 @@ +import { Hono } from "hono"; +import { validator } from "hono/validator"; + +import { + withAgentPermission, + withAgentURLParam, + withAuth, +} from "../../middleware"; +import type { Bindings } from "../../server"; +import { + schemaCompleteSlackVerificationRequest, + schemaStartSlackVerificationRequest, + type CompleteSlackVerificationResponse, + type SlackVerificationStatusResponse, + type StartSlackVerificationResponse, +} from "./setup-slack.client"; + +/** + * Verify Slack bot token by calling auth.test API. + */ +async function verifySlackBotToken( + botToken: string +): Promise<{ valid: boolean; error?: string; botName?: string }> { + try { + const resp = await fetch("https://slack.com/api/auth.test", { + method: "POST", + headers: { + Authorization: `Bearer ${botToken}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + }); + const data = (await resp.json()) as { + ok: boolean; + error?: string; + user?: string; + bot_id?: string; + }; + if (!data.ok) { + return { valid: false, error: data.error || "Invalid token" }; + } + return { valid: true, botName: data.user }; + } catch (error) { + return { + valid: false, + error: error instanceof Error ? error.message : "Verification failed", + }; + } +} + +export default function mountSetupSlack( + app: Hono<{ + Bindings: Bindings; + }> +) { + // Get webhook URL (no credentials required) + app.get( + "/webhook-url", + withAuth, + withAgentURLParam, + withAgentPermission("read"), + async (c) => { + const agent = c.get("agent"); + const db = await c.env.database(); + + // Get the agent's production deployment target for webhook URL + const target = await db.selectAgentDeploymentTargetByName( + agent.id, + "production" + ); + if (!target) { + return c.json({ error: "No deployment target found" }, 400); + } + + if (!c.env.accessUrl) { + return c.json( + { error: "Access URL not configured on this deployment" }, + 500 + ); + } + const webhookUrl = `${c.env.accessUrl.origin}/api/webhook/${target.request_id}/slack`; + + return c.json({ webhook_url: webhookUrl }); + } + ); + + // Start Slack verification + app.post( + "/start-verification", + withAuth, + withAgentURLParam, + withAgentPermission("write"), + validator("json", (value) => { + return schemaStartSlackVerificationRequest.parse(value); + }), + async (c) => { + const agent = c.get("agent"); + const req = c.req.valid("json"); + const db = await c.env.database(); + + // Get the agent's production deployment target for webhook URL + const target = await db.selectAgentDeploymentTargetByName( + agent.id, + "production" + ); + if (!target) { + return c.json({ error: "No deployment target found" }, 400); + } + + // Generate webhook URL using the access URL + if (!c.env.accessUrl) { + return c.json( + { error: "Access URL not configured on this deployment" }, + 500 + ); + } + const webhookUrl = `${c.env.accessUrl.origin}/api/webhook/${target.request_id}/slack`; + + // Store verification state + await db.updateAgent({ + id: agent.id, + slack_verification: { + signingSecret: req.signing_secret, + botToken: req.bot_token, + startedAt: new Date().toISOString(), + }, + }); + + const response: StartSlackVerificationResponse = { + webhook_url: webhookUrl, + }; + return c.json(response); + } + ); + + // Get verification status + app.get( + "/verification-status", + withAuth, + withAgentURLParam, + withAgentPermission("read"), + async (c) => { + const agent = c.get("agent"); + const verification = agent.slack_verification; + + const response: SlackVerificationStatusResponse = { + active: verification !== null, + started_at: verification?.startedAt, + last_event_at: verification?.lastEventAt, + dm_received: verification?.dmReceivedAt !== undefined, + dm_channel: verification?.dmChannel, + signature_failed: verification?.signatureFailedAt !== undefined, + signature_failed_at: verification?.signatureFailedAt, + }; + return c.json(response); + } + ); + + // Complete verification + app.post( + "/complete-verification", + withAuth, + withAgentURLParam, + withAgentPermission("write"), + validator("json", (value) => { + return schemaCompleteSlackVerificationRequest.parse(value); + }), + async (c) => { + const agent = c.get("agent"); + const req = c.req.valid("json"); + const db = await c.env.database(); + const userId = c.get("user_id"); + + // Verify the bot token + const verification = await verifySlackBotToken(req.bot_token); + if (!verification.valid) { + return c.json({ success: false, error: verification.error }, 400); + } + + // Save credentials as environment variables + // First check if they already exist and update, otherwise create + const existingVars = await db.selectAgentEnvironmentVariablesByAgentID({ + agentID: agent.id, + }); + + const botTokenVar = existingVars.find((v) => v.key === "SLACK_BOT_TOKEN"); + const signingSecretVar = existingVars.find( + (v) => v.key === "SLACK_SIGNING_SECRET" + ); + + if (botTokenVar) { + await db.updateAgentEnvironmentVariable(botTokenVar.id, { + value: req.bot_token, + secret: true, + updated_by: userId, + }); + } else { + await db.insertAgentEnvironmentVariable({ + agent_id: agent.id, + key: "SLACK_BOT_TOKEN", + value: req.bot_token, + secret: true, + target: ["preview", "production"], + created_by: userId, + updated_by: userId, + }); + } + + if (signingSecretVar) { + await db.updateAgentEnvironmentVariable(signingSecretVar.id, { + value: req.signing_secret, + secret: true, + updated_by: userId, + }); + } else { + await db.insertAgentEnvironmentVariable({ + agent_id: agent.id, + key: "SLACK_SIGNING_SECRET", + value: req.signing_secret, + secret: true, + target: ["preview", "production"], + created_by: userId, + updated_by: userId, + }); + } + + // Clear verification state + await db.updateAgent({ + id: agent.id, + slack_verification: null, + }); + + const response: CompleteSlackVerificationResponse = { + success: true, + bot_name: verification.botName, + }; + return c.json(response); + } + ); + + // Cancel verification + app.post( + "/cancel-verification", + withAuth, + withAgentURLParam, + withAgentPermission("write"), + async (c) => { + const agent = c.get("agent"); + const db = await c.env.database(); + + // Clear verification state + await db.updateAgent({ + id: agent.id, + slack_verification: null, + }); + + return c.body(null, 204); + } + ); +} diff --git a/packages/api/src/routes/onboarding/onboarding.client.ts b/packages/api/src/routes/onboarding/onboarding.client.ts new file mode 100644 index 0000000..3bb7c82 --- /dev/null +++ b/packages/api/src/routes/onboarding/onboarding.client.ts @@ -0,0 +1,119 @@ +import { z } from "zod"; +import { assertResponseStatus } from "../../client-helper"; +import type Client from "../../client.browser"; + +export const schemaDownloadAgentRequest = z.object({ + organization_id: z.string().uuid(), +}); + +export type DownloadAgentRequest = z.infer; + +export const schemaDownloadAgentResponse = z.object({ + file_id: z.string().uuid(), + entrypoint: z.string(), + version: z.string().optional(), +}); + +export type DownloadAgentResponse = z.infer; + +export const schemaDeployAgentRequest = z.object({ + organization_id: z.string().uuid(), + name: z.string().min(1).max(40), + file_id: z.string().uuid(), + env: z.array( + z.object({ + key: z.string(), + value: z.string(), + secret: z.boolean(), + }) + ), +}); + +export type DeployAgentRequest = z.infer; + +export const schemaDeployAgentResponse = z.object({ + id: z.string().uuid(), + name: z.string(), +}); + +export type DeployAgentResponse = z.infer; + +export const schemaValidateCredentialsRequest = z.object({ + type: z.enum(["github", "slack"]), + credentials: z.record(z.string(), z.string()), +}); + +export type ValidateCredentialsRequest = z.infer< + typeof schemaValidateCredentialsRequest +>; + +export const schemaValidateCredentialsResponse = z.object({ + valid: z.boolean(), + error: z.string().optional(), +}); + +export type ValidateCredentialsResponse = z.infer< + typeof schemaValidateCredentialsResponse +>; + +export default class Onboarding { + private readonly client: Client; + + public constructor(client: Client) { + this.client = client; + } + + /** + * Download the pre-built onboarding agent from GitHub Releases. + * + * @param request - The request body containing organization_id. + * @returns The file ID and entrypoint of the downloaded agent. + */ + public async downloadAgent( + request: DownloadAgentRequest + ): Promise { + const resp = await this.client.request( + "POST", + "/api/onboarding/download-agent", + JSON.stringify(request) + ); + await assertResponseStatus(resp, 200); + return resp.json(); + } + + /** + * Deploy the onboarding agent with the provided configuration. + * + * @param request - The deployment configuration. + * @returns The created agent's ID and name. + */ + public async deployAgent( + request: DeployAgentRequest + ): Promise { + const resp = await this.client.request( + "POST", + "/api/onboarding/deploy-agent", + JSON.stringify(request) + ); + await assertResponseStatus(resp, 200); + return resp.json(); + } + + /** + * Validate integration credentials before deployment. + * + * @param request - The credentials to validate. + * @returns Whether the credentials are valid and any error message. + */ + public async validateCredentials( + request: ValidateCredentialsRequest + ): Promise { + const resp = await this.client.request( + "POST", + "/api/onboarding/validate-credentials", + JSON.stringify(request) + ); + await assertResponseStatus(resp, 200); + return resp.json(); + } +} diff --git a/packages/api/src/routes/onboarding/onboarding.server.ts b/packages/api/src/routes/onboarding/onboarding.server.ts new file mode 100644 index 0000000..200a0dd --- /dev/null +++ b/packages/api/src/routes/onboarding/onboarding.server.ts @@ -0,0 +1,244 @@ +import { Hono } from "hono"; +import { HTTPException } from "hono/http-exception"; +import { validator } from "hono/validator"; +import { authorizeOrganization, withAuth } from "../../middleware"; +import type { Bindings } from "../../server"; +import { createAgentDeployment } from "../agents/deployments.server"; +import { + schemaDeployAgentRequest, + schemaDownloadAgentRequest, + schemaValidateCredentialsRequest, +} from "./onboarding.client"; + +export default function mountOnboarding(app: Hono<{ Bindings: Bindings }>) { + // Download the onboarding agent artifact from GitHub Releases + app.post( + "/download-agent", + withAuth, + validator("json", (value) => { + return schemaDownloadAgentRequest.parse(value); + }), + async (c) => { + const req = c.req.valid("json"); + await authorizeOrganization(c, req.organization_id); + + const releaseUrl = c.env.ONBOARDING_AGENT_RELEASE_URL; + if (!releaseUrl) { + throw new HTTPException(500, { + message: "Onboarding agent release URL not configured", + }); + } + + // Fetch release info from GitHub API + const releaseResp = await fetch(releaseUrl, { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "Blink-Server", + }, + }); + if (!releaseResp.ok) { + throw new HTTPException(502, { + message: `Failed to fetch release info: ${releaseResp.status}`, + }); + } + + const release = (await releaseResp.json()) as { + tag_name?: string; + assets?: Array<{ + name: string; + browser_download_url: string; + }>; + }; + + const agentAsset = release.assets?.find((a) => a.name === "agent.js"); + if (!agentAsset) { + throw new HTTPException(404, { + message: "Agent artifact not found in release", + }); + } + + // Download the artifact + const artifactResp = await fetch(agentAsset.browser_download_url, { + headers: { + "User-Agent": "Blink-Server", + }, + }); + if (!artifactResp.ok) { + throw new HTTPException(502, { + message: `Failed to download artifact: ${artifactResp.status}`, + }); + } + + const artifactData = await artifactResp.text(); + + // Upload to file storage + const { id } = await c.env.files.upload({ + user_id: c.get("user_id"), + organization_id: req.organization_id, + file: new File([artifactData], "agent.js", { + type: "application/javascript", + }), + }); + + return c.json({ + file_id: id, + entrypoint: "agent.js", + version: release.tag_name, + }); + } + ); + + // Deploy the onboarding agent with provided configuration + app.post( + "/deploy-agent", + withAuth, + validator("json", (value) => { + return schemaDeployAgentRequest.parse(value); + }), + async (c) => { + const req = c.req.valid("json"); + const org = await authorizeOrganization(c, req.organization_id); + const db = await c.env.database(); + + const agent = await db.insertAgent({ + organization_id: org.id, + created_by: c.get("user_id"), + name: req.name, + description: + "AI agent with GitHub, Slack, web search, and compute capabilities", + visibility: "organization", + }); + + // Grant admin permission to creator + await db.upsertAgentPermission({ + agent_id: agent.id, + user_id: agent.created_by, + permission: "admin", + created_by: agent.created_by, + }); + + // Insert environment variables + for (const env of req.env) { + await db.insertAgentEnvironmentVariable({ + agent_id: agent.id, + key: env.key, + value: env.value, + secret: env.secret, + target: ["preview", "production"], + created_by: c.get("user_id"), + updated_by: c.get("user_id"), + }); + } + + // Create deployment with the downloaded file + await createAgentDeployment({ + req: c.req.raw, + db: db, + bindings: c.env, + outputFiles: [{ path: "agent.js", id: req.file_id }], + entrypoint: "agent.js", + agentID: agent.id, + userID: c.get("user_id"), + organizationID: org.id, + target: "production", + }); + + return c.json({ id: agent.id, name: agent.name }); + } + ); + + // Validate integration credentials + app.post( + "/validate-credentials", + withAuth, + validator("json", (value) => { + return schemaValidateCredentialsRequest.parse(value); + }), + async (c) => { + const req = c.req.valid("json"); + + if (req.type === "github") { + try { + const appId = req.credentials.appId as string | undefined; + const privateKey = req.credentials.privateKey as string | undefined; + if (!appId || !privateKey) { + return c.json({ + valid: false, + error: "App ID and Private Key are required", + }); + } + + // Validate the private key format + if ( + !privateKey.includes("-----BEGIN") || + !privateKey.includes("PRIVATE KEY-----") + ) { + return c.json({ + valid: false, + error: + "Private key must be in PEM format (-----BEGIN ... PRIVATE KEY-----)", + }); + } + + // Validate app ID is numeric + if (!/^\d+$/.test(appId)) { + return c.json({ + valid: false, + error: "App ID must be numeric", + }); + } + + // Basic validation passed - full validation happens at runtime + return c.json({ valid: true }); + } catch (error) { + return c.json({ + valid: false, + error: + error instanceof Error + ? error.message + : "Invalid GitHub credentials", + }); + } + } + + if (req.type === "slack") { + try { + const botToken = req.credentials.botToken as string | undefined; + if (!botToken) { + return c.json({ + valid: false, + error: "Bot Token is required", + }); + } + + // Verify Slack bot token + const resp = await fetch("https://slack.com/api/auth.test", { + method: "POST", + headers: { + Authorization: `Bearer ${botToken}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + }); + const data = (await resp.json()) as { ok: boolean; error?: string }; + if (!data.ok) { + return c.json({ + valid: false, + error: data.error || "Invalid Slack token", + }); + } + return c.json({ valid: true }); + } catch (error) { + return c.json({ + valid: false, + error: + error instanceof Error + ? error.message + : "Failed to validate Slack token", + }); + } + } + + return c.json({ valid: false, error: "Unknown credential type" }); + } + ); +} diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index 6d2e9eb..5dbe310 100644 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -18,6 +18,7 @@ import mountDevhook from "./routes/devhook.server"; import mountFiles from "./routes/files.server"; import mountInvites from "./routes/invites.server"; import mountMessages from "./routes/messages.server"; +import mountOnboarding from "./routes/onboarding/onboarding.server"; import mountOrganizations from "./routes/organizations/organizations.server"; import type { OtelSpan } from "./routes/otlp/convert"; import mountOtlp from "./routes/otlp/otlp.server"; @@ -213,6 +214,11 @@ export interface Bindings { * Pathname will not be respected - /api is used. */ readonly apiBaseURL: URL; + /** + * accessUrl is the public URL used for external access (e.g., webhooks). + * This may differ from apiBaseURL when using tunnels or proxies. + */ + readonly accessUrl?: URL; readonly matchRequestHost?: (host: string) => string | undefined; readonly createRequestURL?: (id: string) => URL; @@ -220,6 +226,7 @@ export interface Bindings { readonly NODE_ENV: string; readonly AI_GATEWAY_API_KEY?: string; readonly TOOLS_EXA_API_KEY?: string; + readonly ONBOARDING_AGENT_RELEASE_URL?: string; // OAuth provider credentials readonly GITHUB_CLIENT_ID?: string; @@ -311,6 +318,7 @@ mountMessages(api.basePath("/messages")); mountTools(api.basePath("/tools")); mountOtlp(api.basePath("/otlp")); mountDevhook(api.basePath("/devhook")); +mountOnboarding(api.basePath("/onboarding")); // Webhook route for proxying requests to agents // The wildcard route handles subpaths like /api/webhook/:id/github/events diff --git a/packages/database/migrations/0001_colorful_silk_fever.sql b/packages/database/migrations/0001_colorful_silk_fever.sql new file mode 100644 index 0000000..69cbec1 --- /dev/null +++ b/packages/database/migrations/0001_colorful_silk_fever.sql @@ -0,0 +1 @@ +ALTER TABLE "agent" ADD COLUMN "slack_verification" jsonb; \ No newline at end of file diff --git a/packages/database/migrations/0002_glossy_hellcat.sql b/packages/database/migrations/0002_glossy_hellcat.sql new file mode 100644 index 0000000..2405ec5 --- /dev/null +++ b/packages/database/migrations/0002_glossy_hellcat.sql @@ -0,0 +1 @@ +ALTER TABLE "agent" ADD COLUMN "github_app_setup" jsonb; \ No newline at end of file diff --git a/packages/database/migrations/0003_bent_veda.sql b/packages/database/migrations/0003_bent_veda.sql new file mode 100644 index 0000000..803c611 --- /dev/null +++ b/packages/database/migrations/0003_bent_veda.sql @@ -0,0 +1 @@ +ALTER TABLE "agent" ADD COLUMN "onboarding_state" jsonb; \ No newline at end of file diff --git a/packages/database/migrations/meta/0001_snapshot.json b/packages/database/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..20a1c83 --- /dev/null +++ b/packages/database/migrations/meta/0001_snapshot.json @@ -0,0 +1,3263 @@ +{ + "id": "3045341c-ded5-49af-aca1-9fb857657746", + "prevId": "645fa823-648d-48c5-987d-c9d72bb0659e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent": { + "name": "agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "name": { + "name": "name", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_file_id": { + "name": "avatar_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_deployment_id": { + "name": "active_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_expire_ttl": { + "name": "chat_expire_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_deployment_number": { + "name": "last_deployment_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_number": { + "name": "last_run_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "slack_verification": { + "name": "slack_verification", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_name_unique": { + "name": "agent_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_organization_id_organization_id_fk": { + "name": "agent_organization_id_organization_id_fk", + "tableFrom": "agent", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "name_format": { + "name": "name_format", + "value": "\"agent\".\"name\" ~* '^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$'" + } + }, + "isRLSEnabled": false + }, + "public.agent_deployment": { + "name": "agent_deployment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_from": { + "name": "created_from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "entrypoint": { + "name": "entrypoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compatibility_version": { + "name": "compatibility_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "source_files": { + "name": "source_files", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "output_files": { + "name": "output_files", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_memory_mb": { + "name": "platform_memory_mb", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "platform_region": { + "name": "platform_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_metadata": { + "name": "platform_metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "direct_access_url": { + "name": "direct_access_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_deployment_agent_id_number_unique": { + "name": "agent_deployment_agent_id_number_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_deployment_agent_id_agent_id_fk": { + "name": "agent_deployment_agent_id_agent_id_fk", + "tableFrom": "agent_deployment", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_deployment_target_id_agent_deployment_target_id_fk": { + "name": "agent_deployment_target_id_agent_deployment_target_id_fk", + "tableFrom": "agent_deployment", + "tableTo": "agent_deployment_target", + "columnsFrom": ["target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_deployment_log": { + "name": "agent_deployment_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "agent_deployment_log_agent_id_agent_id_fk": { + "name": "agent_deployment_log_agent_id_agent_id_fk", + "tableFrom": "agent_deployment_log", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_deployment_target": { + "name": "agent_deployment_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_deployment_target_agent_id_target_unique": { + "name": "agent_deployment_target_agent_id_target_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_deployment_target_agent_id_agent_id_fk": { + "name": "agent_deployment_target_agent_id_agent_id_fk", + "tableFrom": "agent_deployment_target", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agent_deployment_target_request_id_unique": { + "name": "agent_deployment_target_request_id_unique", + "nullsNotDistinct": false, + "columns": ["request_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_variable": { + "name": "agent_environment_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_dek": { + "name": "encrypted_dek", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encryption_iv": { + "name": "encryption_iv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encryption_auth_tag": { + "name": "encryption_auth_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "target": { + "name": "target", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{\"preview\",\"production\"}'" + } + }, + "indexes": { + "agent_environment_variable_agent_id_idx": { + "name": "agent_environment_variable_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_env_key_prod_unique": { + "name": "agent_env_key_prod_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "'production' = ANY(\"agent_environment_variable\".\"target\")", + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_env_key_prev_unique": { + "name": "agent_env_key_prev_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "'preview' = ANY(\"agent_environment_variable\".\"target\")", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_variable_agent_id_agent_id_fk": { + "name": "agent_environment_variable_agent_id_agent_id_fk", + "tableFrom": "agent_environment_variable", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_log": { + "name": "agent_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "payload_str": { + "name": "payload_str", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_log_agent_time_idx": { + "name": "agent_log_agent_time_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_log_agent_id_agent_id_fk": { + "name": "agent_log_agent_id_agent_id_fk", + "tableFrom": "agent_log", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_permission": { + "name": "agent_permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "permission": { + "name": "permission", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_permission_agent_id_user_id_unique": { + "name": "agent_permission_agent_id_user_id_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_permission_agent_id_index": { + "name": "agent_permission_agent_id_index", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_permission_agent_id_agent_id_fk": { + "name": "agent_permission_agent_id_agent_id_fk", + "tableFrom": "agent_permission", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_permission_user_id_user_id_fk": { + "name": "agent_permission_user_id_user_id_fk", + "tableFrom": "agent_permission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_permission_created_by_user_id_fk": { + "name": "agent_permission_created_by_user_id_fk", + "tableFrom": "agent_permission", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_pin": { + "name": "agent_pin", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_pin_agent_id_user_id_unique": { + "name": "agent_pin_agent_id_user_id_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_pin_agent_id_agent_id_fk": { + "name": "agent_pin_agent_id_agent_id_fk", + "tableFrom": "agent_pin", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_pin_user_id_user_id_fk": { + "name": "agent_pin_user_id_user_id_fk", + "tableFrom": "agent_pin", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_storage_kv": { + "name": "agent_storage_kv", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_target_id": { + "name": "agent_deployment_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_storage_kv_agent_deployment_target_id_key_unique": { + "name": "agent_storage_kv_agent_deployment_target_id_key_unique", + "columns": [ + { + "expression": "agent_deployment_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_storage_kv_agent_id_agent_id_fk": { + "name": "agent_storage_kv_agent_id_agent_id_fk", + "tableFrom": "agent_storage_kv", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_storage_kv_agent_deployment_target_id_agent_deployment_target_id_fk": { + "name": "agent_storage_kv_agent_deployment_target_id_agent_deployment_target_id_fk", + "tableFrom": "agent_storage_kv", + "tableTo": "agent_deployment_target", + "columnsFrom": ["agent_deployment_target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_trace": { + "name": "agent_trace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "start_time": { + "name": "start_time", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload_original": { + "name": "payload_original", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_str": { + "name": "payload_str", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_trace_agent_time_idx": { + "name": "agent_trace_agent_time_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_time", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_trace_agent_id_agent_id_fk": { + "name": "agent_trace_agent_id_agent_id_fk", + "tableFrom": "agent_trace", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_lookup": { + "name": "key_lookup", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "key_suffix": { + "name": "key_suffix", + "type": "varchar(4)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_user_idx": { + "name": "api_key_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_lookup_idx": { + "name": "api_key_lookup_idx", + "columns": [ + { + "expression": "key_lookup", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_revoked_by_user_id_fk": { + "name": "api_key_revoked_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["revoked_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_lookup_unique": { + "name": "api_key_key_lookup_unique", + "nullsNotDistinct": false, + "columns": ["key_lookup"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_deployment_target_id": { + "name": "agent_deployment_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_key": { + "name": "agent_key", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "last_run_number": { + "name": "last_run_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expire_ttl": { + "name": "expire_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chat_organization_created_at_idx": { + "name": "chat_organization_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_organization_created_by": { + "name": "idx_chat_organization_created_by", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_visibility": { + "name": "idx_chat_visibility", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"visibility\" IN ('public', 'private', 'organization')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_expire_ttl": { + "name": "idx_chat_expire_ttl", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"expire_ttl\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_agent_deployment_target_id_key_unique": { + "name": "idx_chat_agent_deployment_target_id_key_unique", + "columns": [ + { + "expression": "agent_deployment_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_organization_id_organization_id_fk": { + "name": "chat_organization_id_organization_id_fk", + "tableFrom": "chat", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_agent_id_agent_id_fk": { + "name": "chat_agent_id_agent_id_fk", + "tableFrom": "chat", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "chat_agent_deployment_id_agent_deployment_id_fk": { + "name": "chat_agent_deployment_id_agent_deployment_id_fk", + "tableFrom": "chat", + "tableTo": "agent_deployment", + "columnsFrom": ["agent_deployment_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "chat_agent_deployment_target_id_agent_deployment_target_id_fk": { + "name": "chat_agent_deployment_target_id_agent_deployment_target_id_fk", + "tableFrom": "chat", + "tableTo": "agent_deployment_target", + "columnsFrom": ["agent_deployment_target_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_run": { + "name": "chat_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_step_number": { + "name": "last_step_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "chat_run_chat_id_number_unique": { + "name": "chat_run_chat_id_number_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_run_chat_id_chat_id_fk": { + "name": "chat_run_chat_id_chat_id_fk", + "tableFrom": "chat_run", + "tableTo": "chat", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_run_agent_id_agent_id_fk": { + "name": "chat_run_agent_id_agent_id_fk", + "tableFrom": "chat_run", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_run_step": { + "name": "chat_run_step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chat_run_id": { + "name": "chat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "interrupted_at": { + "name": "interrupted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "first_message_id": { + "name": "first_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_id": { + "name": "last_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_headers": { + "name": "response_headers", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "response_headers_redacted": { + "name": "response_headers_redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_body_redacted": { + "name": "response_body_redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "continuation_reason": { + "name": "continuation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time_to_first_token_micros": { + "name": "time_to_first_token_micros", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tool_calls_total": { + "name": "tool_calls_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_calls_completed": { + "name": "tool_calls_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_calls_errored": { + "name": "tool_calls_errored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "usage_cost_usd": { + "name": "usage_cost_usd", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "usage_model": { + "name": "usage_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_total_input_tokens": { + "name": "usage_total_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_output_tokens": { + "name": "usage_total_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_tokens": { + "name": "usage_total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_cached_input_tokens": { + "name": "usage_total_cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chat_run_step_chat_run_id_id_unique": { + "name": "chat_run_step_chat_run_id_id_unique", + "columns": [ + { + "expression": "chat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_run_step_single_streaming": { + "name": "chat_run_step_single_streaming", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat_run_step\".\"completed_at\" IS NULL AND \"chat_run_step\".\"error\" IS NULL AND \"chat_run_step\".\"interrupted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_run_step_agent_id_started_at_idx": { + "name": "chat_run_step_agent_id_started_at_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_run_step_agent_deployment_id_started_at_idx": { + "name": "chat_run_step_agent_deployment_id_started_at_idx", + "columns": [ + { + "expression": "agent_deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_run_step_chat_id_chat_id_fk": { + "name": "chat_run_step_chat_id_chat_id_fk", + "tableFrom": "chat_run_step", + "tableTo": "chat", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_run_step_chat_run_id_chat_run_id_fk": { + "name": "chat_run_step_chat_run_id_chat_run_id_fk", + "tableFrom": "chat_run_step", + "tableTo": "chat_run", + "columnsFrom": ["chat_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_user_state": { + "name": "chat_user_state", + "schema": "", + "columns": { + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "chat_user_state_chat_id_chat_id_fk": { + "name": "chat_user_state_chat_id_chat_id_fk", + "tableFrom": "chat_user_state", + "tableTo": "chat", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_state_user_id_user_id_fk": { + "name": "chat_user_state_user_id_user_id_fk", + "tableFrom": "chat_user_state", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_user_state_chat_id_user_id_pk": { + "name": "chat_user_state_chat_id_user_id_pk", + "columns": ["chat_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_verification": { + "name": "email_verification", + "schema": "", + "columns": { + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_email_verification_email_code": { + "name": "idx_email_verification_email_code", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file": { + "name": "file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_length": { + "name": "byte_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "pdf_page_count": { + "name": "pdf_page_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chat_run_id": { + "name": "chat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_run_step_id": { + "name": "chat_run_step_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "parts": { + "name": "parts", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_message_chat_role_created": { + "name": "idx_message_chat_role_created", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message\".\"role\" = 'user'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_chat_id_chat_id_fk": { + "name": "message_chat_id_chat_id_fk", + "tableFrom": "message", + "tableTo": "chat", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "personal_owner_user_id": { + "name": "personal_owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_tier": { + "name": "billing_tier", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "billing_interval": { + "name": "billing_interval", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'month'" + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metronome_customer_id": { + "name": "metronome_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metronome_contract_id": { + "name": "metronome_contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_billing_date": { + "name": "next_billing_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_entitled_at": { + "name": "billing_entitled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "personal_org_per_user": { + "name": "personal_org_per_user", + "columns": [ + { + "expression": "personal_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization\".\"kind\" = 'personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_personal_owner_user_id_user_id_fk": { + "name": "organization_personal_owner_user_id_user_id_fk", + "tableFrom": "organization", + "tableTo": "user", + "columnsFrom": ["personal_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_name_unique": { + "name": "organization_name_unique", + "nullsNotDistinct": false, + "columns": ["name"] + } + }, + "policies": {}, + "checkConstraints": { + "name_format": { + "name": "name_format", + "value": "\"organization\".\"name\" ~* '^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$'" + }, + "name_not_reserved": { + "name": "name_not_reserved", + "value": "\"organization\".\"name\" NOT IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41)" + }, + "personal_owner_presence": { + "name": "personal_owner_presence", + "value": "(\"organization\".\"kind\" = 'personal' AND \"organization\".\"personal_owner_user_id\" IS NOT NULL)\n OR (\"organization\".\"kind\" = 'organization' AND \"organization\".\"personal_owner_user_id\" IS NULL)" + }, + "personal_created_by_matches_owner": { + "name": "personal_created_by_matches_owner", + "value": "\"organization\".\"kind\" != 'personal' OR \"organization\".\"created_by\" = \"organization\".\"personal_owner_user_id\"" + } + }, + "isRLSEnabled": false + }, + "public.organization_billing_usage_event": { + "name": "organization_billing_usage_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "transaction_id": { + "name": "transaction_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(32, 18)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_billing_usage_event_org_txn_unique": { + "name": "organization_billing_usage_event_org_txn_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_invite": { + "name": "organization_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "invited_by": { + "name": "invited_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reusable": { + "name": "reusable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_accepted_at": { + "name": "last_accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_invite_organization_id_organization_id_fk": { + "name": "organization_invite_organization_id_organization_id_fk", + "tableFrom": "organization_invite", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_invite_invited_by_membership_fk": { + "name": "organization_invite_invited_by_membership_fk", + "tableFrom": "organization_invite", + "tableTo": "organization_membership", + "columnsFrom": ["organization_id", "invited_by"], + "columnsTo": ["organization_id", "user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_invite_code_unique": { + "name": "organization_invite_code_unique", + "nullsNotDistinct": false, + "columns": ["code"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_membership": { + "name": "organization_membership", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "billing_emails_opt_out": { + "name": "billing_emails_opt_out", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_membership_organization_id_organization_id_fk": { + "name": "organization_membership_organization_id_organization_id_fk", + "tableFrom": "organization_membership", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_membership_user_id_user_id_fk": { + "name": "organization_membership_user_id_user_id_fk", + "tableFrom": "organization_membership", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_membership_organization_id_user_id_pk": { + "name": "organization_membership_organization_id_user_id_pk", + "columns": ["organization_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_account": { + "name": "user_account", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_account_user_id_user_id_fk": { + "name": "user_account_user_id_user_id_fk", + "tableFrom": "user_account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_account_provider_provider_account_id_pk": { + "name": "user_account_provider_provider_account_id_pk", + "columns": ["provider", "provider_account_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.chat_run_step_with_status": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chat_run_id": { + "name": "chat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "interrupted_at": { + "name": "interrupted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "first_message_id": { + "name": "first_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_id": { + "name": "last_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_headers": { + "name": "response_headers", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "response_headers_redacted": { + "name": "response_headers_redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_body_redacted": { + "name": "response_body_redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "continuation_reason": { + "name": "continuation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time_to_first_token_micros": { + "name": "time_to_first_token_micros", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tool_calls_total": { + "name": "tool_calls_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_calls_completed": { + "name": "tool_calls_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_calls_errored": { + "name": "tool_calls_errored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "usage_cost_usd": { + "name": "usage_cost_usd", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "usage_model": { + "name": "usage_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_total_input_tokens": { + "name": "usage_total_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_output_tokens": { + "name": "usage_total_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_tokens": { + "name": "usage_total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_cached_input_tokens": { + "name": "usage_total_cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"id\", \"number\", \"chat_id\", \"chat_run_id\", \"agent_id\", \"agent_deployment_id\", \"started_at\", \"heartbeat_at\", \"completed_at\", \"interrupted_at\", \"first_message_id\", \"last_message_id\", \"error\", \"response_status\", \"response_headers\", \"response_headers_redacted\", \"response_body\", \"response_body_redacted\", \"response_message_id\", \"continuation_reason\", \"time_to_first_token_micros\", \"tool_calls_total\", \"tool_calls_completed\", \"tool_calls_errored\", \"usage_cost_usd\", \"usage_model\", \"usage_total_input_tokens\", \"usage_total_output_tokens\", \"usage_total_tokens\", \"usage_total_cached_input_tokens\", CASE\n WHEN \"error\" IS NOT NULL THEN 'error'\n WHEN \"interrupted_at\" IS NOT NULL THEN 'interrupted'\n WHEN \"completed_at\" IS NOT NULL THEN 'completed'\n WHEN \"continuation_reason\" IS NOT NULL THEN 'streaming'\n WHEN \"heartbeat_at\" < NOW() - INTERVAL '90 seconds' THEN 'stalled'\n ELSE 'streaming'\nEND as \"status\" from \"chat_run_step\"", + "name": "chat_run_step_with_status", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.chat_run_with_status": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_step_number": { + "name": "last_step_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"chat_run\".\"id\", \"chat_run\".\"number\", \"chat_run\".\"chat_id\", COALESCE(\"chat_run_step_with_status\".\"agent_id\", \"chat_run\".\"agent_id\") as \"agent_id\", COALESCE(\"chat_run_step_with_status\".\"agent_deployment_id\", \"chat_run\".\"agent_deployment_id\") as \"agent_deployment_id\", \"chat_run\".\"created_at\", \"chat_run\".\"last_step_number\", COALESCE(\"chat_run_step_with_status\".\"completed_at\", \"chat_run_step_with_status\".\"interrupted_at\", \"chat_run_step_with_status\".\"heartbeat_at\", \"chat_run_step_with_status\".\"started_at\", \"chat_run\".\"created_at\") as \"updated_at\", \"chat_run_step_with_status\".\"error\", \"status\" from \"chat_run\" left join \"chat_run_step_with_status\" on (\"chat_run\".\"id\" = \"chat_run_step_with_status\".\"chat_run_id\" and \"chat_run_step_with_status\".\"number\" = \"chat_run\".\"last_step_number\")", + "name": "chat_run_with_status", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.chat_with_status": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_target_id": { + "name": "agent_deployment_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_key": { + "name": "agent_key", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "last_run_number": { + "name": "last_run_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expire_ttl": { + "name": "expire_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"chat\".\"id\", \"chat\".\"created_at\", \"chat\".\"created_by\", \"chat\".\"organization_id\", \"chat\".\"visibility\", \"chat\".\"title\", \"chat\".\"metadata\", \"chat\".\"archived\", \"chat\".\"agent_id\", COALESCE(\"agent_deployment_id\", \"chat\".\"agent_deployment_id\") as \"agent_deployment_id\", \"chat\".\"agent_deployment_target_id\", \"chat\".\"agent_key\", \"chat\".\"last_run_number\", \"chat\".\"expire_ttl\", COALESCE(\"updated_at\", \"chat\".\"created_at\") as \"updated_at\", \"chat_run_with_status\".\"error\", CASE\n WHEN \"status\" IS NULL THEN 'idle'\n WHEN \"status\" IN ('error', 'stalled') THEN 'error'\n WHEN \"status\" = 'interrupted' THEN 'interrupted'\n WHEN \"status\" IN ('completed', 'idle') THEN 'idle'\n ELSE 'streaming'\n END as \"status\", CASE \n WHEN \"chat\".\"expire_ttl\" IS NULL THEN NULL\n ELSE COALESCE(\"updated_at\", \"chat\".\"created_at\") + (\"chat\".\"expire_ttl\" || ' seconds')::interval\n END as \"expires_at\" from \"chat\" left join \"chat_run_with_status\" on (\"chat\".\"id\" = \"chat_run_with_status\".\"chat_id\" and \"chat_run_with_status\".\"number\" = \"chat\".\"last_run_number\")", + "name": "chat_with_status", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.user_with_personal_organization": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"user\".\"id\", \"user\".\"created_at\", \"user\".\"updated_at\", \"user\".\"display_name\", \"user\".\"email\", \"user\".\"email_verified\", \"user\".\"password\", \"organization\".\"id\" as \"organization_id\", \"organization\".\"name\" as \"username\", \"organization\".\"avatar_url\" as \"avatar_url\" from \"user\" inner join \"organization\" on \"user\".\"id\" = \"organization\".\"personal_owner_user_id\"", + "name": "user_with_personal_organization", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/database/migrations/meta/0002_snapshot.json b/packages/database/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..8a706f3 --- /dev/null +++ b/packages/database/migrations/meta/0002_snapshot.json @@ -0,0 +1,3269 @@ +{ + "id": "ddfdf8e8-abf4-45aa-841b-22ebb97fbe11", + "prevId": "3045341c-ded5-49af-aca1-9fb857657746", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent": { + "name": "agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "name": { + "name": "name", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_file_id": { + "name": "avatar_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_deployment_id": { + "name": "active_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_expire_ttl": { + "name": "chat_expire_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_deployment_number": { + "name": "last_deployment_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_number": { + "name": "last_run_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "slack_verification": { + "name": "slack_verification", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "github_app_setup": { + "name": "github_app_setup", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_name_unique": { + "name": "agent_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_organization_id_organization_id_fk": { + "name": "agent_organization_id_organization_id_fk", + "tableFrom": "agent", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "name_format": { + "name": "name_format", + "value": "\"agent\".\"name\" ~* '^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$'" + } + }, + "isRLSEnabled": false + }, + "public.agent_deployment": { + "name": "agent_deployment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_from": { + "name": "created_from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "entrypoint": { + "name": "entrypoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compatibility_version": { + "name": "compatibility_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "source_files": { + "name": "source_files", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "output_files": { + "name": "output_files", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_memory_mb": { + "name": "platform_memory_mb", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "platform_region": { + "name": "platform_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_metadata": { + "name": "platform_metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "direct_access_url": { + "name": "direct_access_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_deployment_agent_id_number_unique": { + "name": "agent_deployment_agent_id_number_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_deployment_agent_id_agent_id_fk": { + "name": "agent_deployment_agent_id_agent_id_fk", + "tableFrom": "agent_deployment", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_deployment_target_id_agent_deployment_target_id_fk": { + "name": "agent_deployment_target_id_agent_deployment_target_id_fk", + "tableFrom": "agent_deployment", + "tableTo": "agent_deployment_target", + "columnsFrom": ["target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_deployment_log": { + "name": "agent_deployment_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "agent_deployment_log_agent_id_agent_id_fk": { + "name": "agent_deployment_log_agent_id_agent_id_fk", + "tableFrom": "agent_deployment_log", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_deployment_target": { + "name": "agent_deployment_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_deployment_target_agent_id_target_unique": { + "name": "agent_deployment_target_agent_id_target_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_deployment_target_agent_id_agent_id_fk": { + "name": "agent_deployment_target_agent_id_agent_id_fk", + "tableFrom": "agent_deployment_target", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agent_deployment_target_request_id_unique": { + "name": "agent_deployment_target_request_id_unique", + "nullsNotDistinct": false, + "columns": ["request_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_variable": { + "name": "agent_environment_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_dek": { + "name": "encrypted_dek", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encryption_iv": { + "name": "encryption_iv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encryption_auth_tag": { + "name": "encryption_auth_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "target": { + "name": "target", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{\"preview\",\"production\"}'" + } + }, + "indexes": { + "agent_environment_variable_agent_id_idx": { + "name": "agent_environment_variable_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_env_key_prod_unique": { + "name": "agent_env_key_prod_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "'production' = ANY(\"agent_environment_variable\".\"target\")", + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_env_key_prev_unique": { + "name": "agent_env_key_prev_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "'preview' = ANY(\"agent_environment_variable\".\"target\")", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_variable_agent_id_agent_id_fk": { + "name": "agent_environment_variable_agent_id_agent_id_fk", + "tableFrom": "agent_environment_variable", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_log": { + "name": "agent_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "payload_str": { + "name": "payload_str", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_log_agent_time_idx": { + "name": "agent_log_agent_time_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_log_agent_id_agent_id_fk": { + "name": "agent_log_agent_id_agent_id_fk", + "tableFrom": "agent_log", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_permission": { + "name": "agent_permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "permission": { + "name": "permission", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_permission_agent_id_user_id_unique": { + "name": "agent_permission_agent_id_user_id_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_permission_agent_id_index": { + "name": "agent_permission_agent_id_index", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_permission_agent_id_agent_id_fk": { + "name": "agent_permission_agent_id_agent_id_fk", + "tableFrom": "agent_permission", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_permission_user_id_user_id_fk": { + "name": "agent_permission_user_id_user_id_fk", + "tableFrom": "agent_permission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_permission_created_by_user_id_fk": { + "name": "agent_permission_created_by_user_id_fk", + "tableFrom": "agent_permission", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_pin": { + "name": "agent_pin", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_pin_agent_id_user_id_unique": { + "name": "agent_pin_agent_id_user_id_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_pin_agent_id_agent_id_fk": { + "name": "agent_pin_agent_id_agent_id_fk", + "tableFrom": "agent_pin", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_pin_user_id_user_id_fk": { + "name": "agent_pin_user_id_user_id_fk", + "tableFrom": "agent_pin", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_storage_kv": { + "name": "agent_storage_kv", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_target_id": { + "name": "agent_deployment_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_storage_kv_agent_deployment_target_id_key_unique": { + "name": "agent_storage_kv_agent_deployment_target_id_key_unique", + "columns": [ + { + "expression": "agent_deployment_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_storage_kv_agent_id_agent_id_fk": { + "name": "agent_storage_kv_agent_id_agent_id_fk", + "tableFrom": "agent_storage_kv", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_storage_kv_agent_deployment_target_id_agent_deployment_target_id_fk": { + "name": "agent_storage_kv_agent_deployment_target_id_agent_deployment_target_id_fk", + "tableFrom": "agent_storage_kv", + "tableTo": "agent_deployment_target", + "columnsFrom": ["agent_deployment_target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_trace": { + "name": "agent_trace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "start_time": { + "name": "start_time", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload_original": { + "name": "payload_original", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_str": { + "name": "payload_str", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_trace_agent_time_idx": { + "name": "agent_trace_agent_time_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_time", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_trace_agent_id_agent_id_fk": { + "name": "agent_trace_agent_id_agent_id_fk", + "tableFrom": "agent_trace", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_lookup": { + "name": "key_lookup", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "key_suffix": { + "name": "key_suffix", + "type": "varchar(4)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_user_idx": { + "name": "api_key_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_lookup_idx": { + "name": "api_key_lookup_idx", + "columns": [ + { + "expression": "key_lookup", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_revoked_by_user_id_fk": { + "name": "api_key_revoked_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["revoked_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_lookup_unique": { + "name": "api_key_key_lookup_unique", + "nullsNotDistinct": false, + "columns": ["key_lookup"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_deployment_target_id": { + "name": "agent_deployment_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_key": { + "name": "agent_key", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "last_run_number": { + "name": "last_run_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expire_ttl": { + "name": "expire_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chat_organization_created_at_idx": { + "name": "chat_organization_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_organization_created_by": { + "name": "idx_chat_organization_created_by", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_visibility": { + "name": "idx_chat_visibility", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"visibility\" IN ('public', 'private', 'organization')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_expire_ttl": { + "name": "idx_chat_expire_ttl", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"expire_ttl\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_agent_deployment_target_id_key_unique": { + "name": "idx_chat_agent_deployment_target_id_key_unique", + "columns": [ + { + "expression": "agent_deployment_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_organization_id_organization_id_fk": { + "name": "chat_organization_id_organization_id_fk", + "tableFrom": "chat", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_agent_id_agent_id_fk": { + "name": "chat_agent_id_agent_id_fk", + "tableFrom": "chat", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "chat_agent_deployment_id_agent_deployment_id_fk": { + "name": "chat_agent_deployment_id_agent_deployment_id_fk", + "tableFrom": "chat", + "tableTo": "agent_deployment", + "columnsFrom": ["agent_deployment_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "chat_agent_deployment_target_id_agent_deployment_target_id_fk": { + "name": "chat_agent_deployment_target_id_agent_deployment_target_id_fk", + "tableFrom": "chat", + "tableTo": "agent_deployment_target", + "columnsFrom": ["agent_deployment_target_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_run": { + "name": "chat_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_step_number": { + "name": "last_step_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "chat_run_chat_id_number_unique": { + "name": "chat_run_chat_id_number_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_run_chat_id_chat_id_fk": { + "name": "chat_run_chat_id_chat_id_fk", + "tableFrom": "chat_run", + "tableTo": "chat", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_run_agent_id_agent_id_fk": { + "name": "chat_run_agent_id_agent_id_fk", + "tableFrom": "chat_run", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_run_step": { + "name": "chat_run_step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chat_run_id": { + "name": "chat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "interrupted_at": { + "name": "interrupted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "first_message_id": { + "name": "first_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_id": { + "name": "last_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_headers": { + "name": "response_headers", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "response_headers_redacted": { + "name": "response_headers_redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_body_redacted": { + "name": "response_body_redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "continuation_reason": { + "name": "continuation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time_to_first_token_micros": { + "name": "time_to_first_token_micros", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tool_calls_total": { + "name": "tool_calls_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_calls_completed": { + "name": "tool_calls_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_calls_errored": { + "name": "tool_calls_errored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "usage_cost_usd": { + "name": "usage_cost_usd", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "usage_model": { + "name": "usage_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_total_input_tokens": { + "name": "usage_total_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_output_tokens": { + "name": "usage_total_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_tokens": { + "name": "usage_total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_cached_input_tokens": { + "name": "usage_total_cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chat_run_step_chat_run_id_id_unique": { + "name": "chat_run_step_chat_run_id_id_unique", + "columns": [ + { + "expression": "chat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_run_step_single_streaming": { + "name": "chat_run_step_single_streaming", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat_run_step\".\"completed_at\" IS NULL AND \"chat_run_step\".\"error\" IS NULL AND \"chat_run_step\".\"interrupted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_run_step_agent_id_started_at_idx": { + "name": "chat_run_step_agent_id_started_at_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_run_step_agent_deployment_id_started_at_idx": { + "name": "chat_run_step_agent_deployment_id_started_at_idx", + "columns": [ + { + "expression": "agent_deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_run_step_chat_id_chat_id_fk": { + "name": "chat_run_step_chat_id_chat_id_fk", + "tableFrom": "chat_run_step", + "tableTo": "chat", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_run_step_chat_run_id_chat_run_id_fk": { + "name": "chat_run_step_chat_run_id_chat_run_id_fk", + "tableFrom": "chat_run_step", + "tableTo": "chat_run", + "columnsFrom": ["chat_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_user_state": { + "name": "chat_user_state", + "schema": "", + "columns": { + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "chat_user_state_chat_id_chat_id_fk": { + "name": "chat_user_state_chat_id_chat_id_fk", + "tableFrom": "chat_user_state", + "tableTo": "chat", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_state_user_id_user_id_fk": { + "name": "chat_user_state_user_id_user_id_fk", + "tableFrom": "chat_user_state", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_user_state_chat_id_user_id_pk": { + "name": "chat_user_state_chat_id_user_id_pk", + "columns": ["chat_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_verification": { + "name": "email_verification", + "schema": "", + "columns": { + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_email_verification_email_code": { + "name": "idx_email_verification_email_code", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file": { + "name": "file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_length": { + "name": "byte_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "pdf_page_count": { + "name": "pdf_page_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chat_run_id": { + "name": "chat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_run_step_id": { + "name": "chat_run_step_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "parts": { + "name": "parts", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_message_chat_role_created": { + "name": "idx_message_chat_role_created", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message\".\"role\" = 'user'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_chat_id_chat_id_fk": { + "name": "message_chat_id_chat_id_fk", + "tableFrom": "message", + "tableTo": "chat", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "personal_owner_user_id": { + "name": "personal_owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_tier": { + "name": "billing_tier", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "billing_interval": { + "name": "billing_interval", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'month'" + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metronome_customer_id": { + "name": "metronome_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metronome_contract_id": { + "name": "metronome_contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_billing_date": { + "name": "next_billing_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_entitled_at": { + "name": "billing_entitled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "personal_org_per_user": { + "name": "personal_org_per_user", + "columns": [ + { + "expression": "personal_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization\".\"kind\" = 'personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_personal_owner_user_id_user_id_fk": { + "name": "organization_personal_owner_user_id_user_id_fk", + "tableFrom": "organization", + "tableTo": "user", + "columnsFrom": ["personal_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_name_unique": { + "name": "organization_name_unique", + "nullsNotDistinct": false, + "columns": ["name"] + } + }, + "policies": {}, + "checkConstraints": { + "name_format": { + "name": "name_format", + "value": "\"organization\".\"name\" ~* '^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$'" + }, + "name_not_reserved": { + "name": "name_not_reserved", + "value": "\"organization\".\"name\" NOT IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41)" + }, + "personal_owner_presence": { + "name": "personal_owner_presence", + "value": "(\"organization\".\"kind\" = 'personal' AND \"organization\".\"personal_owner_user_id\" IS NOT NULL)\n OR (\"organization\".\"kind\" = 'organization' AND \"organization\".\"personal_owner_user_id\" IS NULL)" + }, + "personal_created_by_matches_owner": { + "name": "personal_created_by_matches_owner", + "value": "\"organization\".\"kind\" != 'personal' OR \"organization\".\"created_by\" = \"organization\".\"personal_owner_user_id\"" + } + }, + "isRLSEnabled": false + }, + "public.organization_billing_usage_event": { + "name": "organization_billing_usage_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "transaction_id": { + "name": "transaction_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(32, 18)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_billing_usage_event_org_txn_unique": { + "name": "organization_billing_usage_event_org_txn_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_invite": { + "name": "organization_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "invited_by": { + "name": "invited_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reusable": { + "name": "reusable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_accepted_at": { + "name": "last_accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_invite_organization_id_organization_id_fk": { + "name": "organization_invite_organization_id_organization_id_fk", + "tableFrom": "organization_invite", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_invite_invited_by_membership_fk": { + "name": "organization_invite_invited_by_membership_fk", + "tableFrom": "organization_invite", + "tableTo": "organization_membership", + "columnsFrom": ["organization_id", "invited_by"], + "columnsTo": ["organization_id", "user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_invite_code_unique": { + "name": "organization_invite_code_unique", + "nullsNotDistinct": false, + "columns": ["code"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_membership": { + "name": "organization_membership", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "billing_emails_opt_out": { + "name": "billing_emails_opt_out", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_membership_organization_id_organization_id_fk": { + "name": "organization_membership_organization_id_organization_id_fk", + "tableFrom": "organization_membership", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_membership_user_id_user_id_fk": { + "name": "organization_membership_user_id_user_id_fk", + "tableFrom": "organization_membership", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_membership_organization_id_user_id_pk": { + "name": "organization_membership_organization_id_user_id_pk", + "columns": ["organization_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_account": { + "name": "user_account", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_account_user_id_user_id_fk": { + "name": "user_account_user_id_user_id_fk", + "tableFrom": "user_account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_account_provider_provider_account_id_pk": { + "name": "user_account_provider_provider_account_id_pk", + "columns": ["provider", "provider_account_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.chat_run_step_with_status": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chat_run_id": { + "name": "chat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "interrupted_at": { + "name": "interrupted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "first_message_id": { + "name": "first_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_id": { + "name": "last_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_headers": { + "name": "response_headers", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "response_headers_redacted": { + "name": "response_headers_redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_body_redacted": { + "name": "response_body_redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "continuation_reason": { + "name": "continuation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time_to_first_token_micros": { + "name": "time_to_first_token_micros", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tool_calls_total": { + "name": "tool_calls_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_calls_completed": { + "name": "tool_calls_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_calls_errored": { + "name": "tool_calls_errored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "usage_cost_usd": { + "name": "usage_cost_usd", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "usage_model": { + "name": "usage_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_total_input_tokens": { + "name": "usage_total_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_output_tokens": { + "name": "usage_total_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_tokens": { + "name": "usage_total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_cached_input_tokens": { + "name": "usage_total_cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"id\", \"number\", \"chat_id\", \"chat_run_id\", \"agent_id\", \"agent_deployment_id\", \"started_at\", \"heartbeat_at\", \"completed_at\", \"interrupted_at\", \"first_message_id\", \"last_message_id\", \"error\", \"response_status\", \"response_headers\", \"response_headers_redacted\", \"response_body\", \"response_body_redacted\", \"response_message_id\", \"continuation_reason\", \"time_to_first_token_micros\", \"tool_calls_total\", \"tool_calls_completed\", \"tool_calls_errored\", \"usage_cost_usd\", \"usage_model\", \"usage_total_input_tokens\", \"usage_total_output_tokens\", \"usage_total_tokens\", \"usage_total_cached_input_tokens\", CASE\n WHEN \"error\" IS NOT NULL THEN 'error'\n WHEN \"interrupted_at\" IS NOT NULL THEN 'interrupted'\n WHEN \"completed_at\" IS NOT NULL THEN 'completed'\n WHEN \"continuation_reason\" IS NOT NULL THEN 'streaming'\n WHEN \"heartbeat_at\" < NOW() - INTERVAL '90 seconds' THEN 'stalled'\n ELSE 'streaming'\nEND as \"status\" from \"chat_run_step\"", + "name": "chat_run_step_with_status", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.chat_run_with_status": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_step_number": { + "name": "last_step_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"chat_run\".\"id\", \"chat_run\".\"number\", \"chat_run\".\"chat_id\", COALESCE(\"chat_run_step_with_status\".\"agent_id\", \"chat_run\".\"agent_id\") as \"agent_id\", COALESCE(\"chat_run_step_with_status\".\"agent_deployment_id\", \"chat_run\".\"agent_deployment_id\") as \"agent_deployment_id\", \"chat_run\".\"created_at\", \"chat_run\".\"last_step_number\", COALESCE(\"chat_run_step_with_status\".\"completed_at\", \"chat_run_step_with_status\".\"interrupted_at\", \"chat_run_step_with_status\".\"heartbeat_at\", \"chat_run_step_with_status\".\"started_at\", \"chat_run\".\"created_at\") as \"updated_at\", \"chat_run_step_with_status\".\"error\", \"status\" from \"chat_run\" left join \"chat_run_step_with_status\" on (\"chat_run\".\"id\" = \"chat_run_step_with_status\".\"chat_run_id\" and \"chat_run_step_with_status\".\"number\" = \"chat_run\".\"last_step_number\")", + "name": "chat_run_with_status", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.chat_with_status": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_target_id": { + "name": "agent_deployment_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_key": { + "name": "agent_key", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "last_run_number": { + "name": "last_run_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expire_ttl": { + "name": "expire_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"chat\".\"id\", \"chat\".\"created_at\", \"chat\".\"created_by\", \"chat\".\"organization_id\", \"chat\".\"visibility\", \"chat\".\"title\", \"chat\".\"metadata\", \"chat\".\"archived\", \"chat\".\"agent_id\", COALESCE(\"agent_deployment_id\", \"chat\".\"agent_deployment_id\") as \"agent_deployment_id\", \"chat\".\"agent_deployment_target_id\", \"chat\".\"agent_key\", \"chat\".\"last_run_number\", \"chat\".\"expire_ttl\", COALESCE(\"updated_at\", \"chat\".\"created_at\") as \"updated_at\", \"chat_run_with_status\".\"error\", CASE\n WHEN \"status\" IS NULL THEN 'idle'\n WHEN \"status\" IN ('error', 'stalled') THEN 'error'\n WHEN \"status\" = 'interrupted' THEN 'interrupted'\n WHEN \"status\" IN ('completed', 'idle') THEN 'idle'\n ELSE 'streaming'\n END as \"status\", CASE \n WHEN \"chat\".\"expire_ttl\" IS NULL THEN NULL\n ELSE COALESCE(\"updated_at\", \"chat\".\"created_at\") + (\"chat\".\"expire_ttl\" || ' seconds')::interval\n END as \"expires_at\" from \"chat\" left join \"chat_run_with_status\" on (\"chat\".\"id\" = \"chat_run_with_status\".\"chat_id\" and \"chat_run_with_status\".\"number\" = \"chat\".\"last_run_number\")", + "name": "chat_with_status", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.user_with_personal_organization": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"user\".\"id\", \"user\".\"created_at\", \"user\".\"updated_at\", \"user\".\"display_name\", \"user\".\"email\", \"user\".\"email_verified\", \"user\".\"password\", \"organization\".\"id\" as \"organization_id\", \"organization\".\"name\" as \"username\", \"organization\".\"avatar_url\" as \"avatar_url\" from \"user\" inner join \"organization\" on \"user\".\"id\" = \"organization\".\"personal_owner_user_id\"", + "name": "user_with_personal_organization", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/database/migrations/meta/0003_snapshot.json b/packages/database/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000..af7307f --- /dev/null +++ b/packages/database/migrations/meta/0003_snapshot.json @@ -0,0 +1,3275 @@ +{ + "id": "1df64cdd-9c3b-4928-af91-6f2ae477e135", + "prevId": "ddfdf8e8-abf4-45aa-841b-22ebb97fbe11", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent": { + "name": "agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "name": { + "name": "name", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_file_id": { + "name": "avatar_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_deployment_id": { + "name": "active_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_expire_ttl": { + "name": "chat_expire_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_deployment_number": { + "name": "last_deployment_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_number": { + "name": "last_run_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "slack_verification": { + "name": "slack_verification", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "github_app_setup": { + "name": "github_app_setup", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "onboarding_state": { + "name": "onboarding_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_name_unique": { + "name": "agent_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_organization_id_organization_id_fk": { + "name": "agent_organization_id_organization_id_fk", + "tableFrom": "agent", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "name_format": { + "name": "name_format", + "value": "\"agent\".\"name\" ~* '^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$'" + } + }, + "isRLSEnabled": false + }, + "public.agent_deployment": { + "name": "agent_deployment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_from": { + "name": "created_from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "entrypoint": { + "name": "entrypoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compatibility_version": { + "name": "compatibility_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "source_files": { + "name": "source_files", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "output_files": { + "name": "output_files", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_memory_mb": { + "name": "platform_memory_mb", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "platform_region": { + "name": "platform_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_metadata": { + "name": "platform_metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "direct_access_url": { + "name": "direct_access_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_deployment_agent_id_number_unique": { + "name": "agent_deployment_agent_id_number_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_deployment_agent_id_agent_id_fk": { + "name": "agent_deployment_agent_id_agent_id_fk", + "tableFrom": "agent_deployment", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_deployment_target_id_agent_deployment_target_id_fk": { + "name": "agent_deployment_target_id_agent_deployment_target_id_fk", + "tableFrom": "agent_deployment", + "tableTo": "agent_deployment_target", + "columnsFrom": ["target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_deployment_log": { + "name": "agent_deployment_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "agent_deployment_log_agent_id_agent_id_fk": { + "name": "agent_deployment_log_agent_id_agent_id_fk", + "tableFrom": "agent_deployment_log", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_deployment_target": { + "name": "agent_deployment_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_deployment_target_agent_id_target_unique": { + "name": "agent_deployment_target_agent_id_target_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_deployment_target_agent_id_agent_id_fk": { + "name": "agent_deployment_target_agent_id_agent_id_fk", + "tableFrom": "agent_deployment_target", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agent_deployment_target_request_id_unique": { + "name": "agent_deployment_target_request_id_unique", + "nullsNotDistinct": false, + "columns": ["request_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_variable": { + "name": "agent_environment_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_dek": { + "name": "encrypted_dek", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encryption_iv": { + "name": "encryption_iv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encryption_auth_tag": { + "name": "encryption_auth_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "target": { + "name": "target", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{\"preview\",\"production\"}'" + } + }, + "indexes": { + "agent_environment_variable_agent_id_idx": { + "name": "agent_environment_variable_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_env_key_prod_unique": { + "name": "agent_env_key_prod_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "'production' = ANY(\"agent_environment_variable\".\"target\")", + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_env_key_prev_unique": { + "name": "agent_env_key_prev_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "'preview' = ANY(\"agent_environment_variable\".\"target\")", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_variable_agent_id_agent_id_fk": { + "name": "agent_environment_variable_agent_id_agent_id_fk", + "tableFrom": "agent_environment_variable", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_log": { + "name": "agent_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "payload_str": { + "name": "payload_str", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_log_agent_time_idx": { + "name": "agent_log_agent_time_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_log_agent_id_agent_id_fk": { + "name": "agent_log_agent_id_agent_id_fk", + "tableFrom": "agent_log", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_permission": { + "name": "agent_permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "permission": { + "name": "permission", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_permission_agent_id_user_id_unique": { + "name": "agent_permission_agent_id_user_id_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_permission_agent_id_index": { + "name": "agent_permission_agent_id_index", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_permission_agent_id_agent_id_fk": { + "name": "agent_permission_agent_id_agent_id_fk", + "tableFrom": "agent_permission", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_permission_user_id_user_id_fk": { + "name": "agent_permission_user_id_user_id_fk", + "tableFrom": "agent_permission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_permission_created_by_user_id_fk": { + "name": "agent_permission_created_by_user_id_fk", + "tableFrom": "agent_permission", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_pin": { + "name": "agent_pin", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_pin_agent_id_user_id_unique": { + "name": "agent_pin_agent_id_user_id_unique", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_pin_agent_id_agent_id_fk": { + "name": "agent_pin_agent_id_agent_id_fk", + "tableFrom": "agent_pin", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_pin_user_id_user_id_fk": { + "name": "agent_pin_user_id_user_id_fk", + "tableFrom": "agent_pin", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_storage_kv": { + "name": "agent_storage_kv", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_target_id": { + "name": "agent_deployment_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_storage_kv_agent_deployment_target_id_key_unique": { + "name": "agent_storage_kv_agent_deployment_target_id_key_unique", + "columns": [ + { + "expression": "agent_deployment_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_storage_kv_agent_id_agent_id_fk": { + "name": "agent_storage_kv_agent_id_agent_id_fk", + "tableFrom": "agent_storage_kv", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_storage_kv_agent_deployment_target_id_agent_deployment_target_id_fk": { + "name": "agent_storage_kv_agent_deployment_target_id_agent_deployment_target_id_fk", + "tableFrom": "agent_storage_kv", + "tableTo": "agent_deployment_target", + "columnsFrom": ["agent_deployment_target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_trace": { + "name": "agent_trace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "start_time": { + "name": "start_time", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload_original": { + "name": "payload_original", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_str": { + "name": "payload_str", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_trace_agent_time_idx": { + "name": "agent_trace_agent_time_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_time", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_trace_agent_id_agent_id_fk": { + "name": "agent_trace_agent_id_agent_id_fk", + "tableFrom": "agent_trace", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_lookup": { + "name": "key_lookup", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "key_suffix": { + "name": "key_suffix", + "type": "varchar(4)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_user_idx": { + "name": "api_key_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_lookup_idx": { + "name": "api_key_lookup_idx", + "columns": [ + { + "expression": "key_lookup", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_revoked_by_user_id_fk": { + "name": "api_key_revoked_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["revoked_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_lookup_unique": { + "name": "api_key_key_lookup_unique", + "nullsNotDistinct": false, + "columns": ["key_lookup"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_deployment_target_id": { + "name": "agent_deployment_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_key": { + "name": "agent_key", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "last_run_number": { + "name": "last_run_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expire_ttl": { + "name": "expire_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chat_organization_created_at_idx": { + "name": "chat_organization_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_organization_created_by": { + "name": "idx_chat_organization_created_by", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_visibility": { + "name": "idx_chat_visibility", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"visibility\" IN ('public', 'private', 'organization')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_expire_ttl": { + "name": "idx_chat_expire_ttl", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"expire_ttl\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_agent_deployment_target_id_key_unique": { + "name": "idx_chat_agent_deployment_target_id_key_unique", + "columns": [ + { + "expression": "agent_deployment_target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_organization_id_organization_id_fk": { + "name": "chat_organization_id_organization_id_fk", + "tableFrom": "chat", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_agent_id_agent_id_fk": { + "name": "chat_agent_id_agent_id_fk", + "tableFrom": "chat", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "chat_agent_deployment_id_agent_deployment_id_fk": { + "name": "chat_agent_deployment_id_agent_deployment_id_fk", + "tableFrom": "chat", + "tableTo": "agent_deployment", + "columnsFrom": ["agent_deployment_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "chat_agent_deployment_target_id_agent_deployment_target_id_fk": { + "name": "chat_agent_deployment_target_id_agent_deployment_target_id_fk", + "tableFrom": "chat", + "tableTo": "agent_deployment_target", + "columnsFrom": ["agent_deployment_target_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_run": { + "name": "chat_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_step_number": { + "name": "last_step_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "chat_run_chat_id_number_unique": { + "name": "chat_run_chat_id_number_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_run_chat_id_chat_id_fk": { + "name": "chat_run_chat_id_chat_id_fk", + "tableFrom": "chat_run", + "tableTo": "chat", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_run_agent_id_agent_id_fk": { + "name": "chat_run_agent_id_agent_id_fk", + "tableFrom": "chat_run", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_run_step": { + "name": "chat_run_step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chat_run_id": { + "name": "chat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "interrupted_at": { + "name": "interrupted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "first_message_id": { + "name": "first_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_id": { + "name": "last_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_headers": { + "name": "response_headers", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "response_headers_redacted": { + "name": "response_headers_redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_body_redacted": { + "name": "response_body_redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "continuation_reason": { + "name": "continuation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time_to_first_token_micros": { + "name": "time_to_first_token_micros", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tool_calls_total": { + "name": "tool_calls_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_calls_completed": { + "name": "tool_calls_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_calls_errored": { + "name": "tool_calls_errored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "usage_cost_usd": { + "name": "usage_cost_usd", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "usage_model": { + "name": "usage_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_total_input_tokens": { + "name": "usage_total_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_output_tokens": { + "name": "usage_total_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_tokens": { + "name": "usage_total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_cached_input_tokens": { + "name": "usage_total_cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chat_run_step_chat_run_id_id_unique": { + "name": "chat_run_step_chat_run_id_id_unique", + "columns": [ + { + "expression": "chat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_run_step_single_streaming": { + "name": "chat_run_step_single_streaming", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat_run_step\".\"completed_at\" IS NULL AND \"chat_run_step\".\"error\" IS NULL AND \"chat_run_step\".\"interrupted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_run_step_agent_id_started_at_idx": { + "name": "chat_run_step_agent_id_started_at_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_run_step_agent_deployment_id_started_at_idx": { + "name": "chat_run_step_agent_deployment_id_started_at_idx", + "columns": [ + { + "expression": "agent_deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_run_step_chat_id_chat_id_fk": { + "name": "chat_run_step_chat_id_chat_id_fk", + "tableFrom": "chat_run_step", + "tableTo": "chat", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_run_step_chat_run_id_chat_run_id_fk": { + "name": "chat_run_step_chat_run_id_chat_run_id_fk", + "tableFrom": "chat_run_step", + "tableTo": "chat_run", + "columnsFrom": ["chat_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_user_state": { + "name": "chat_user_state", + "schema": "", + "columns": { + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "chat_user_state_chat_id_chat_id_fk": { + "name": "chat_user_state_chat_id_chat_id_fk", + "tableFrom": "chat_user_state", + "tableTo": "chat", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_state_user_id_user_id_fk": { + "name": "chat_user_state_user_id_user_id_fk", + "tableFrom": "chat_user_state", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_user_state_chat_id_user_id_pk": { + "name": "chat_user_state_chat_id_user_id_pk", + "columns": ["chat_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_verification": { + "name": "email_verification", + "schema": "", + "columns": { + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_email_verification_email_code": { + "name": "idx_email_verification_email_code", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file": { + "name": "file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_length": { + "name": "byte_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "pdf_page_count": { + "name": "pdf_page_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chat_run_id": { + "name": "chat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_run_step_id": { + "name": "chat_run_step_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "parts": { + "name": "parts", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_message_chat_role_created": { + "name": "idx_message_chat_role_created", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message\".\"role\" = 'user'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_chat_id_chat_id_fk": { + "name": "message_chat_id_chat_id_fk", + "tableFrom": "message", + "tableTo": "chat", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "personal_owner_user_id": { + "name": "personal_owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_tier": { + "name": "billing_tier", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "billing_interval": { + "name": "billing_interval", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'month'" + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metronome_customer_id": { + "name": "metronome_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metronome_contract_id": { + "name": "metronome_contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_billing_date": { + "name": "next_billing_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_entitled_at": { + "name": "billing_entitled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "personal_org_per_user": { + "name": "personal_org_per_user", + "columns": [ + { + "expression": "personal_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization\".\"kind\" = 'personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_personal_owner_user_id_user_id_fk": { + "name": "organization_personal_owner_user_id_user_id_fk", + "tableFrom": "organization", + "tableTo": "user", + "columnsFrom": ["personal_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_name_unique": { + "name": "organization_name_unique", + "nullsNotDistinct": false, + "columns": ["name"] + } + }, + "policies": {}, + "checkConstraints": { + "name_format": { + "name": "name_format", + "value": "\"organization\".\"name\" ~* '^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$'" + }, + "name_not_reserved": { + "name": "name_not_reserved", + "value": "\"organization\".\"name\" NOT IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41)" + }, + "personal_owner_presence": { + "name": "personal_owner_presence", + "value": "(\"organization\".\"kind\" = 'personal' AND \"organization\".\"personal_owner_user_id\" IS NOT NULL)\n OR (\"organization\".\"kind\" = 'organization' AND \"organization\".\"personal_owner_user_id\" IS NULL)" + }, + "personal_created_by_matches_owner": { + "name": "personal_created_by_matches_owner", + "value": "\"organization\".\"kind\" != 'personal' OR \"organization\".\"created_by\" = \"organization\".\"personal_owner_user_id\"" + } + }, + "isRLSEnabled": false + }, + "public.organization_billing_usage_event": { + "name": "organization_billing_usage_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "transaction_id": { + "name": "transaction_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(32, 18)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_billing_usage_event_org_txn_unique": { + "name": "organization_billing_usage_event_org_txn_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_invite": { + "name": "organization_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "invited_by": { + "name": "invited_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reusable": { + "name": "reusable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_accepted_at": { + "name": "last_accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_invite_organization_id_organization_id_fk": { + "name": "organization_invite_organization_id_organization_id_fk", + "tableFrom": "organization_invite", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_invite_invited_by_membership_fk": { + "name": "organization_invite_invited_by_membership_fk", + "tableFrom": "organization_invite", + "tableTo": "organization_membership", + "columnsFrom": ["organization_id", "invited_by"], + "columnsTo": ["organization_id", "user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_invite_code_unique": { + "name": "organization_invite_code_unique", + "nullsNotDistinct": false, + "columns": ["code"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_membership": { + "name": "organization_membership", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "billing_emails_opt_out": { + "name": "billing_emails_opt_out", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_membership_organization_id_organization_id_fk": { + "name": "organization_membership_organization_id_organization_id_fk", + "tableFrom": "organization_membership", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_membership_user_id_user_id_fk": { + "name": "organization_membership_user_id_user_id_fk", + "tableFrom": "organization_membership", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_membership_organization_id_user_id_pk": { + "name": "organization_membership_organization_id_user_id_pk", + "columns": ["organization_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_account": { + "name": "user_account", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_account_user_id_user_id_fk": { + "name": "user_account_user_id_user_id_fk", + "tableFrom": "user_account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_account_provider_provider_account_id_pk": { + "name": "user_account_provider_provider_account_id_pk", + "columns": ["provider", "provider_account_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.chat_run_step_with_status": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chat_run_id": { + "name": "chat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_id": { + "name": "agent_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "interrupted_at": { + "name": "interrupted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "first_message_id": { + "name": "first_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_id": { + "name": "last_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_headers": { + "name": "response_headers", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "response_headers_redacted": { + "name": "response_headers_redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_body_redacted": { + "name": "response_body_redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "continuation_reason": { + "name": "continuation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time_to_first_token_micros": { + "name": "time_to_first_token_micros", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tool_calls_total": { + "name": "tool_calls_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_calls_completed": { + "name": "tool_calls_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_calls_errored": { + "name": "tool_calls_errored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "usage_cost_usd": { + "name": "usage_cost_usd", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "usage_model": { + "name": "usage_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_total_input_tokens": { + "name": "usage_total_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_output_tokens": { + "name": "usage_total_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_tokens": { + "name": "usage_total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_total_cached_input_tokens": { + "name": "usage_total_cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"id\", \"number\", \"chat_id\", \"chat_run_id\", \"agent_id\", \"agent_deployment_id\", \"started_at\", \"heartbeat_at\", \"completed_at\", \"interrupted_at\", \"first_message_id\", \"last_message_id\", \"error\", \"response_status\", \"response_headers\", \"response_headers_redacted\", \"response_body\", \"response_body_redacted\", \"response_message_id\", \"continuation_reason\", \"time_to_first_token_micros\", \"tool_calls_total\", \"tool_calls_completed\", \"tool_calls_errored\", \"usage_cost_usd\", \"usage_model\", \"usage_total_input_tokens\", \"usage_total_output_tokens\", \"usage_total_tokens\", \"usage_total_cached_input_tokens\", CASE\n WHEN \"error\" IS NOT NULL THEN 'error'\n WHEN \"interrupted_at\" IS NOT NULL THEN 'interrupted'\n WHEN \"completed_at\" IS NOT NULL THEN 'completed'\n WHEN \"continuation_reason\" IS NOT NULL THEN 'streaming'\n WHEN \"heartbeat_at\" < NOW() - INTERVAL '90 seconds' THEN 'stalled'\n ELSE 'streaming'\nEND as \"status\" from \"chat_run_step\"", + "name": "chat_run_step_with_status", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.chat_run_with_status": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_step_number": { + "name": "last_step_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"chat_run\".\"id\", \"chat_run\".\"number\", \"chat_run\".\"chat_id\", COALESCE(\"chat_run_step_with_status\".\"agent_id\", \"chat_run\".\"agent_id\") as \"agent_id\", COALESCE(\"chat_run_step_with_status\".\"agent_deployment_id\", \"chat_run\".\"agent_deployment_id\") as \"agent_deployment_id\", \"chat_run\".\"created_at\", \"chat_run\".\"last_step_number\", COALESCE(\"chat_run_step_with_status\".\"completed_at\", \"chat_run_step_with_status\".\"interrupted_at\", \"chat_run_step_with_status\".\"heartbeat_at\", \"chat_run_step_with_status\".\"started_at\", \"chat_run\".\"created_at\") as \"updated_at\", \"chat_run_step_with_status\".\"error\", \"status\" from \"chat_run\" left join \"chat_run_step_with_status\" on (\"chat_run\".\"id\" = \"chat_run_step_with_status\".\"chat_run_id\" and \"chat_run_step_with_status\".\"number\" = \"chat_run\".\"last_step_number\")", + "name": "chat_run_with_status", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.chat_with_status": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_deployment_target_id": { + "name": "agent_deployment_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_key": { + "name": "agent_key", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "last_run_number": { + "name": "last_run_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expire_ttl": { + "name": "expire_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"chat\".\"id\", \"chat\".\"created_at\", \"chat\".\"created_by\", \"chat\".\"organization_id\", \"chat\".\"visibility\", \"chat\".\"title\", \"chat\".\"metadata\", \"chat\".\"archived\", \"chat\".\"agent_id\", COALESCE(\"agent_deployment_id\", \"chat\".\"agent_deployment_id\") as \"agent_deployment_id\", \"chat\".\"agent_deployment_target_id\", \"chat\".\"agent_key\", \"chat\".\"last_run_number\", \"chat\".\"expire_ttl\", COALESCE(\"updated_at\", \"chat\".\"created_at\") as \"updated_at\", \"chat_run_with_status\".\"error\", CASE\n WHEN \"status\" IS NULL THEN 'idle'\n WHEN \"status\" IN ('error', 'stalled') THEN 'error'\n WHEN \"status\" = 'interrupted' THEN 'interrupted'\n WHEN \"status\" IN ('completed', 'idle') THEN 'idle'\n ELSE 'streaming'\n END as \"status\", CASE \n WHEN \"chat\".\"expire_ttl\" IS NULL THEN NULL\n ELSE COALESCE(\"updated_at\", \"chat\".\"created_at\") + (\"chat\".\"expire_ttl\" || ' seconds')::interval\n END as \"expires_at\" from \"chat\" left join \"chat_run_with_status\" on (\"chat\".\"id\" = \"chat_run_with_status\".\"chat_id\" and \"chat_run_with_status\".\"number\" = \"chat\".\"last_run_number\")", + "name": "chat_with_status", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.user_with_personal_organization": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"user\".\"id\", \"user\".\"created_at\", \"user\".\"updated_at\", \"user\".\"display_name\", \"user\".\"email\", \"user\".\"email_verified\", \"user\".\"password\", \"organization\".\"id\" as \"organization_id\", \"organization\".\"name\" as \"username\", \"organization\".\"avatar_url\" as \"avatar_url\" from \"user\" inner join \"organization\" on \"user\".\"id\" = \"organization\".\"personal_owner_user_id\"", + "name": "user_with_personal_organization", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index 6e98bbf..6a83a28 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -8,6 +8,27 @@ "when": 1763742838403, "tag": "0000_initial", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1765380821861, + "tag": "0001_colorful_silk_fever", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1765541098453, + "tag": "0002_glossy_hellcat", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1765556592306, + "tag": "0003_bent_veda", + "breakpoints": true } ] } diff --git a/packages/database/src/convert.ts b/packages/database/src/convert.ts index c000546..32b16a5 100644 --- a/packages/database/src/convert.ts +++ b/packages/database/src/convert.ts @@ -71,6 +71,7 @@ export const agent = ( pinned: "pinned" in agent ? agent.pinned : false, chat_expire_ttl: agent.chat_expire_ttl, user_permission: userPermission, + onboarding_state: agent.onboarding_state ?? null, }; }; diff --git a/packages/database/src/schema.ts b/packages/database/src/schema.ts index d57edef..7346dd3 100644 --- a/packages/database/src/schema.ts +++ b/packages/database/src/schema.ts @@ -488,6 +488,89 @@ export const agent = pgTable( .notNull() .default(0), last_run_number: integer("last_run_number").notNull().default(0), + + // Slack setup verification state (null when no verification in progress) + slack_verification: jsonb("slack_verification").$type<{ + signingSecret: string; + botToken: string; + startedAt: string; + lastEventAt?: string; + dmReceivedAt?: string; + dmChannel?: string; + signatureFailedAt?: string; + }>(), + + // GitHub App setup state (null when no setup in progress) + // Status flow: pending -> app_created -> completed + // - pending: waiting for user to create app on GitHub + // - app_created: app created, waiting for user to install it + // - completed: app created and installed + // - failed: error occurred + github_app_setup: jsonb("github_app_setup").$type<{ + sessionId: string; + manifestName: string; + organization?: string; + startedAt: string; + expiresAt: string; + status: "pending" | "app_created" | "completed" | "failed"; + error?: string; + installationId?: number; + appData?: { + id: number; + clientId: string; + clientSecret: string; + webhookSecret: string; + pem: string; + name: string; + htmlUrl: string; + slug: string; + }; + }>(), + + // Onboarding wizard state (null when onboarding is complete or not started) + onboarding_state: jsonb("onboarding_state").$type<{ + currentStep: + | "welcome" + | "llm-api-keys" + | "github-setup" + | "slack-setup" + | "web-search" + | "deploying" + | "success"; + finished?: boolean; + github?: { + appName: string; + appUrl: string; + installUrl: string; + appId?: string; + clientId?: string; + clientSecret?: string; + webhookSecret?: string; + privateKey?: string; + envVars?: { + appId: string; + clientId: string; + clientSecret: string; + webhookSecret: string; + privateKey: string; + }; + }; + slack?: { + botToken: string; + signingSecret: string; + envVars?: { + botToken: string; + signingSecret: string; + }; + }; + apiKeys?: { + aiProvider?: "anthropic" | "openai" | "vercel"; + aiApiKey?: string; + aiEnvVar?: string; + exaApiKey?: string; + exaEnvVar?: string; + }; + }>(), }, (table) => [ check( diff --git a/packages/server/scripts/build.ts b/packages/server/scripts/build.ts index dcfc756..029f989 100644 --- a/packages/server/scripts/build.ts +++ b/packages/server/scripts/build.ts @@ -7,6 +7,7 @@ import { rmSync, symlinkSync, writeFileSync, + existsSync, } from "fs"; import { join } from "path"; @@ -78,7 +79,8 @@ function buildNextSite() { // to create symlinks at the top level pointing to the actual packages. const bunDir = join(distDir, "site", "node_modules", ".bun"); const nodeModulesDir = join(distDir, "site", "node_modules"); - for (const entry of readdirSync(bunDir)) { + const bunDirExists = existsSync(bunDir); + for (const entry of bunDirExists ? readdirSync(bunDir) : []) { // Skip non-package entries if (entry === "node_modules" || entry.startsWith(".")) continue; diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index 895c474..ff67ad8 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -85,6 +85,7 @@ async function runServer(options: { port: string; dev?: boolean | string }) { authSecret, baseUrl, devProxy, + accessUrl, }); const box = boxen( diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 6397ea0..120d23c 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -21,12 +21,14 @@ interface ServerOptions { authSecret: string; baseUrl: string; devProxy?: string; // e.g. "localhost:3000" + accessUrl: string; } // Files are now stored in the database instead of in-memory export async function startServer(options: ServerOptions) { - const { port, postgresUrl, authSecret, baseUrl, devProxy } = options; + const { port, postgresUrl, authSecret, baseUrl, accessUrl, devProxy } = + options; const db = await connectToPostgres(postgresUrl); const querier = new Querier(db); @@ -120,6 +122,8 @@ export async function startServer(options: ServerOptions) { { AUTH_SECRET: authSecret, NODE_ENV: "development", + ONBOARDING_AGENT_RELEASE_URL: + "https://api.github.com/repos/hugodutka/blink-artifacts/releases/latest", agentStore: (deploymentTargetID) => { return { delete: async (key) => { @@ -174,6 +178,7 @@ export async function startServer(options: ServerOptions) { return querier; }, apiBaseURL: url, + accessUrl: new URL(accessUrl), auth: { handleWebSocketTokenRequest: async (id, request) => { // WebSocket upgrades are handled in the 'upgrade' event diff --git a/packages/site/.storybook/main.ts b/packages/site/.storybook/main.ts index b9f97bf..2d14d7b 100644 --- a/packages/site/.storybook/main.ts +++ b/packages/site/.storybook/main.ts @@ -4,6 +4,7 @@ import path from "path"; import { fileURLToPath } from "url"; import Inspect from "vite-plugin-inspect"; import { nodePolyfills } from "vite-plugin-node-polyfills"; +import { mergeConfig } from "vite"; const config: StorybookConfig = { stories: ["../**/*.stories.@(js|jsx|mjs|ts|tsx)"], @@ -87,7 +88,9 @@ const config: StorybookConfig = { config.plugins.push(Inspect()); config.plugins.push(nodePolyfills()); - return config; + return mergeConfig(config, { + server: { watch: { usePolling: true, interval: 1000 } }, + }); }, }; export default config; diff --git a/packages/site/app/(app)/[organization]/[agent]/layout.tsx b/packages/site/app/(app)/[organization]/[agent]/layout.tsx index 2cdad99..89d7ac2 100644 --- a/packages/site/app/(app)/[organization]/[agent]/layout.tsx +++ b/packages/site/app/(app)/[organization]/[agent]/layout.tsx @@ -21,6 +21,12 @@ export default async function AgentLayout({ getOrganization(session.user.id, organizationName), getAgent(organizationName, agentName), ]); + + // Redirect to onboarding if agent is being onboarded (finished === false) + if (agent.onboarding_state?.finished === false) { + return redirect(`/${organizationName}/~/onboarding/${agentName}`); + } + const user = await getUser(session.user.id); // Get organization kind from database for navigation diff --git a/packages/site/app/(app)/[organization]/[agent]/page.stories.tsx b/packages/site/app/(app)/[organization]/[agent]/page.stories.tsx index 44c995c..293ed67 100644 --- a/packages/site/app/(app)/[organization]/[agent]/page.stories.tsx +++ b/packages/site/app/(app)/[organization]/[agent]/page.stories.tsx @@ -64,6 +64,9 @@ export const Default: Story = { chat_expire_ttl: null, last_deployment_number: 0, last_run_number: 0, + slack_verification: null, + github_app_setup: null, + onboarding_state: null, }), }); }, diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/integrations/env-var-confirmation.stories.tsx b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/env-var-confirmation.stories.tsx new file mode 100644 index 0000000..1a40f57 --- /dev/null +++ b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/env-var-confirmation.stories.tsx @@ -0,0 +1,306 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Key, Search } from "lucide-react"; +import { useState } from "react"; +import { fn } from "storybook/test"; +import { SlackIcon } from "@/components/slack-icon"; +import { type EnvVarConfig, EnvVarConfirmation } from "./env-var-confirmation"; + +// GitHub icon component +function GitHubIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +// Wrapper component that manages state +function StatefulEnvVarConfirmation({ + initialEnvVars, + ...props +}: Omit< + React.ComponentProps, + "envVars" | "onEnvVarsChange" +> & { + initialEnvVars: EnvVarConfig[]; +}) { + const [envVars, setEnvVars] = useState(initialEnvVars); + return ( + + ); +} + +const meta: Meta = { + title: "Settings/Integrations/EnvVarConfirmation", + component: StatefulEnvVarConfirmation, + parameters: { + layout: "centered", + }, + args: { + onSave: fn(async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + }), + onCancel: fn(), + onBack: fn(), + saving: false, + }, + render: (args) => ( +
+ +
+ ), +}; + +export default meta; +type Story = StoryObj; + +// ============================================================================= +// LLM API Key +// ============================================================================= + +export const LLM_SingleKey: Story = { + args: { + title: "LLM API Key", + description: "Save your LLM API key as an environment variable", + icon: , + iconBgColor: "bg-amber-500", + initialEnvVars: [ + { + defaultKey: "ANTHROPIC_API_KEY", + currentKey: "ANTHROPIC_API_KEY", + value: "sk-ant-api03-abcdefghijklmnopqrstuvwxyz123456789", + secret: true, + }, + ], + }, +}; +LLM_SingleKey.storyName = "LLM - Single API Key"; + +// ============================================================================= +// Web Search (Exa) +// ============================================================================= + +export const WebSearch_SingleKey: Story = { + args: { + title: "Web Search (Exa)", + description: "Save your Exa API key as an environment variable", + icon: , + iconBgColor: "bg-blue-500", + initialEnvVars: [ + { + defaultKey: "EXA_API_KEY", + currentKey: "EXA_API_KEY", + value: "exa-12345678-abcd-efgh-ijkl-mnopqrstuvwx", + secret: true, + }, + ], + }, +}; +WebSearch_SingleKey.storyName = "Web Search - Single API Key"; + +// ============================================================================= +// GitHub App (Multiple Keys) +// ============================================================================= + +export const GitHub_MultipleKeys: Story = { + args: { + title: "GitHub App", + description: "Save your GitHub App credentials as environment variables", + icon: , + iconBgColor: "bg-[#24292f]", + initialEnvVars: [ + { + defaultKey: "GITHUB_APP_ID", + currentKey: "GITHUB_APP_ID", + value: "123456", + secret: false, + }, + { + defaultKey: "GITHUB_CLIENT_ID", + currentKey: "GITHUB_CLIENT_ID", + value: "Iv1.abc123def456ghi7", + secret: false, + }, + { + defaultKey: "GITHUB_CLIENT_SECRET", + currentKey: "GITHUB_CLIENT_SECRET", + value: "abcdef1234567890abcdef1234567890abcdef12", + secret: true, + }, + { + defaultKey: "GITHUB_WEBHOOK_SECRET", + currentKey: "GITHUB_WEBHOOK_SECRET", + value: "webhook-secret-12345", + secret: true, + }, + { + defaultKey: "GITHUB_PRIVATE_KEY", + currentKey: "GITHUB_PRIVATE_KEY", + value: + "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBdGVzdC...", + secret: true, + }, + ], + }, +}; +GitHub_MultipleKeys.storyName = "GitHub - Multiple Credentials"; + +// ============================================================================= +// Slack (Two Keys) +// ============================================================================= + +export const Slack_TwoKeys: Story = { + args: { + title: "Slack", + description: "Save your Slack credentials as environment variables", + icon: , + iconBgColor: "bg-[#4A154B]", + initialEnvVars: [ + { + defaultKey: "SLACK_BOT_TOKEN", + currentKey: "SLACK_BOT_TOKEN", + value: "xoxb-123456789012-123456789012-abcdefghijklmnopqrstuvwx", + secret: true, + }, + { + defaultKey: "SLACK_SIGNING_SECRET", + currentKey: "SLACK_SIGNING_SECRET", + value: "abcdef1234567890abcdef1234567890", + secret: true, + }, + ], + }, +}; +Slack_TwoKeys.storyName = "Slack - Two Credentials"; + +// ============================================================================= +// States +// ============================================================================= + +export const Saving: Story = { + args: { + title: "LLM API Key", + description: "Save your LLM API key as an environment variable", + icon: , + iconBgColor: "bg-amber-500", + saving: true, + initialEnvVars: [ + { + defaultKey: "ANTHROPIC_API_KEY", + currentKey: "ANTHROPIC_API_KEY", + value: "sk-ant-api03-abcdefghijklmnopqrstuvwxyz123456789", + secret: true, + }, + ], + }, +}; +Saving.storyName = "Saving State"; + +export const WithoutBackButton: Story = { + args: { + title: "LLM API Key", + description: "Save your LLM API key as an environment variable", + icon: , + iconBgColor: "bg-amber-500", + onBack: undefined, + initialEnvVars: [ + { + defaultKey: "ANTHROPIC_API_KEY", + currentKey: "ANTHROPIC_API_KEY", + value: "sk-ant-api03-abcdefghijklmnopqrstuvwxyz123456789", + secret: true, + }, + ], + }, +}; +WithoutBackButton.storyName = "Without Back Button"; + +// ============================================================================= +// Validation States +// ============================================================================= + +export const EmptyKeyName: Story = { + args: { + title: "LLM API Key", + description: "Save your LLM API key as an environment variable", + icon: , + iconBgColor: "bg-amber-500", + initialEnvVars: [ + { + defaultKey: "ANTHROPIC_API_KEY", + currentKey: "", + value: "sk-ant-api03-abcdefghijklmnopqrstuvwxyz123456789", + secret: true, + }, + ], + }, +}; +EmptyKeyName.storyName = "Validation - Empty Key Name"; + +export const DuplicateKeyNames: Story = { + args: { + title: "Slack", + description: "Save your Slack credentials as environment variables", + icon: , + iconBgColor: "bg-[#4A154B]", + initialEnvVars: [ + { + defaultKey: "SLACK_BOT_TOKEN", + currentKey: "SLACK_TOKEN", + value: "xoxb-123456789012-123456789012-abcdefghijklmnopqrstuvwx", + secret: true, + }, + { + defaultKey: "SLACK_SIGNING_SECRET", + currentKey: "SLACK_TOKEN", + value: "abcdef1234567890abcdef1234567890", + secret: true, + }, + ], + }, +}; +DuplicateKeyNames.storyName = "Validation - Duplicate Key Names"; + +// ============================================================================= +// Edited Key Names +// ============================================================================= + +export const EditedKeyNames: Story = { + args: { + title: "GitHub App", + description: "Save your GitHub App credentials as environment variables", + icon: , + iconBgColor: "bg-[#24292f]", + initialEnvVars: [ + { + defaultKey: "GITHUB_APP_ID", + currentKey: "MY_GH_APP_ID", + value: "123456", + secret: false, + }, + { + defaultKey: "GITHUB_CLIENT_ID", + currentKey: "MY_GH_CLIENT_ID", + value: "Iv1.abc123def456ghi7", + secret: false, + }, + { + defaultKey: "GITHUB_CLIENT_SECRET", + currentKey: "MY_GH_CLIENT_SECRET", + value: "abcdef1234567890abcdef1234567890abcdef12", + secret: true, + }, + ], + }, +}; +EditedKeyNames.storyName = "Edited Key Names"; diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/integrations/env-var-confirmation.tsx b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/env-var-confirmation.tsx new file mode 100644 index 0000000..0711eb9 --- /dev/null +++ b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/env-var-confirmation.tsx @@ -0,0 +1,183 @@ +"use client"; + +import { ArrowLeft, Eye, EyeOff, Info, Loader2 } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; + +export interface EnvVarConfig { + defaultKey: string; // Original suggested name (e.g., "SLACK_BOT_TOKEN") + currentKey: string; // User-editable name + value: string; // Actual value (masked in UI for secrets) + secret: boolean; +} + +export interface EnvVarConfirmationProps { + title: string; + description: string; + icon: React.ReactNode; + iconBgColor: string; + envVars: EnvVarConfig[]; + onEnvVarsChange: (envVars: EnvVarConfig[]) => void; + onSave: () => Promise; + onCancel: () => void; + onBack?: () => void; + saving: boolean; +} + +export function EnvVarConfirmation({ + title, + description, + icon, + iconBgColor, + envVars, + onEnvVarsChange, + onSave, + onCancel, + onBack, + saving, +}: EnvVarConfirmationProps) { + const [revealedIndices, setRevealedIndices] = useState>( + new Set() + ); + + const handleKeyChange = (index: number, newKey: string) => { + const updated = [...envVars]; + updated[index] = { ...updated[index], currentKey: newKey }; + onEnvVarsChange(updated); + }; + + const toggleReveal = (index: number) => { + setRevealedIndices((prev) => { + const next = new Set(prev); + if (next.has(index)) { + next.delete(index); + } else { + next.add(index); + } + return next; + }); + }; + + const maskValue = (value: string) => { + if (value.length <= 8) { + return "β€’β€’β€’β€’β€’β€’β€’β€’"; + } + return `${value.slice(0, 4)}β€’β€’β€’β€’${value.slice(-4)}`; + }; + + const hasEmptyKeys = envVars.some((env) => !env.currentKey.trim()); + const hasDuplicateKeys = + new Set(envVars.map((env) => env.currentKey)).size !== envVars.length; + + return ( + + +
+ {onBack && ( + + )} +
+ {icon} +
+
+ {title} + {description} +
+
+
+ +
+ +

+ Review the environment variables that will be saved. You can edit + the variable names if needed. +

+
+ +
+ {envVars.map((envVar, index) => ( +
+ handleKeyChange(index, e.target.value)} + placeholder={envVar.defaultKey} + disabled={saving} + className="shrink-0 font-mono text-sm" + style={{ + width: `calc(${Math.max(envVar.currentKey.length, 12)}ch + 1.75rem)`, + }} + /> +
+ + {envVar.secret + ? revealedIndices.has(index) + ? envVar.value + : maskValue(envVar.value) + : envVar.value} + + {envVar.secret && ( + + )} +
+
+ ))} +
+ + {hasEmptyKeys && ( +

+ All variable names must be filled in. +

+ )} + {hasDuplicateKeys && ( +

+ Variable names must be unique. +

+ )} + +
+ + +
+
+
+ ); +} diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/integrations/github-integration.tsx b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/github-integration.tsx new file mode 100644 index 0000000..fa90056 --- /dev/null +++ b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/github-integration.tsx @@ -0,0 +1,229 @@ +"use client"; + +import type { OnboardingState } from "@blink.so/api"; +import { + type GitHubAppCredentials, + GitHubSetupWizard, +} from "@/components/github-setup-wizard"; +import { useAPIClient } from "@/lib/api-client"; +import { EnvVarConfirmation } from "./env-var-confirmation"; +import { useIntegrationSetup } from "./use-integration-setup"; + +interface GitHubSetupResult { + appName: string; + appUrl: string; + installUrl: string; + credentials: GitHubAppCredentials; +} + +interface GitHubIntegrationProps { + agentId: string; + agentName: string; + organizationName: string; + onboardingState: OnboardingState | null; + onComplete: () => void; + onCancel: () => void; +} + +function GitHubIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +export function GitHubIntegration({ + agentId, + agentName, + organizationName, + onboardingState, + onComplete, + onCancel, +}: GitHubIntegrationProps) { + const client = useAPIClient(); + + const { + step, + setStep, + envVars, + setEnvVars, + saving, + handleSave, + handleSetupComplete, + } = useIntegrationSetup({ + agentId, + onboardingState, + onComplete, + successMessage: "GitHub integration configured successfully", + + hasSavedState: (state) => !!state?.github?.appId, + + buildSetupResult: (state) => { + const github = state.github; + if (!github) { + return { + appName: "", + appUrl: "", + installUrl: "", + credentials: { + appId: 0, + clientId: "", + clientSecret: "", + webhookSecret: "", + privateKey: "", + }, + }; + } + return { + appName: github.appName, + appUrl: github.appUrl, + installUrl: github.installUrl, + credentials: { + appId: Number(github.appId), + clientId: github.clientId ?? "", + clientSecret: github.clientSecret ?? "", + webhookSecret: github.webhookSecret ?? "", + privateKey: github.privateKey ?? "", + }, + }; + }, + + buildEnvVars: (state) => { + const github = state.github; + if (!github?.appId) return []; + const envVarNames = github.envVars; + return [ + { + defaultKey: "GITHUB_APP_ID", + currentKey: envVarNames?.appId ?? "GITHUB_APP_ID", + value: github.appId, + secret: false, + }, + { + defaultKey: "GITHUB_CLIENT_ID", + currentKey: envVarNames?.clientId ?? "GITHUB_CLIENT_ID", + value: github.clientId ?? "", + secret: false, + }, + { + defaultKey: "GITHUB_CLIENT_SECRET", + currentKey: envVarNames?.clientSecret ?? "GITHUB_CLIENT_SECRET", + value: github.clientSecret ?? "", + secret: true, + }, + { + defaultKey: "GITHUB_WEBHOOK_SECRET", + currentKey: envVarNames?.webhookSecret ?? "GITHUB_WEBHOOK_SECRET", + value: github.webhookSecret ?? "", + secret: true, + }, + { + defaultKey: "GITHUB_PRIVATE_KEY", + currentKey: envVarNames?.privateKey ?? "GITHUB_PRIVATE_KEY", + value: github.privateKey ?? "", + secret: true, + }, + ]; + }, + + buildOnboardingUpdate: (result, vars) => { + const envVarMap = Object.fromEntries( + vars.map((ev) => [ev.defaultKey, ev.currentKey]) + ); + return { + github: { + appName: result.appName, + appUrl: result.appUrl, + installUrl: result.installUrl, + appId: String(result.credentials.appId), + clientId: result.credentials.clientId, + clientSecret: result.credentials.clientSecret, + webhookSecret: result.credentials.webhookSecret, + privateKey: result.credentials.privateKey, + envVars: { + appId: envVarMap.GITHUB_APP_ID, + clientId: envVarMap.GITHUB_CLIENT_ID, + clientSecret: envVarMap.GITHUB_CLIENT_SECRET, + webhookSecret: envVarMap.GITHUB_WEBHOOK_SECRET, + privateKey: envVarMap.GITHUB_PRIVATE_KEY, + }, + }, + }; + }, + }); + + const onSetupComplete = (result: { + appName: string; + appUrl: string; + installUrl: string; + credentials: GitHubAppCredentials; + }) => { + handleSetupComplete(result, [ + { + defaultKey: "GITHUB_APP_ID", + currentKey: "GITHUB_APP_ID", + value: String(result.credentials.appId), + secret: false, + }, + { + defaultKey: "GITHUB_CLIENT_ID", + currentKey: "GITHUB_CLIENT_ID", + value: result.credentials.clientId, + secret: false, + }, + { + defaultKey: "GITHUB_CLIENT_SECRET", + currentKey: "GITHUB_CLIENT_SECRET", + value: result.credentials.clientSecret, + secret: true, + }, + { + defaultKey: "GITHUB_WEBHOOK_SECRET", + currentKey: "GITHUB_WEBHOOK_SECRET", + value: result.credentials.webhookSecret, + secret: true, + }, + { + defaultKey: "GITHUB_PRIVATE_KEY", + currentKey: "GITHUB_PRIVATE_KEY", + value: result.credentials.privateKey, + secret: true, + }, + ]); + }; + + if (step === "confirm") { + return ( + } + iconBgColor="bg-[#24292f]" + envVars={envVars} + onEnvVarsChange={setEnvVars} + onSave={handleSave} + onCancel={onCancel} + onBack={() => setStep("setup")} + saving={saving} + /> + ); + } + + return ( + + ); +} diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/integrations/integration-card.stories.tsx b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/integration-card.stories.tsx new file mode 100644 index 0000000..7d83e31 --- /dev/null +++ b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/integration-card.stories.tsx @@ -0,0 +1,179 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Key, Search } from "lucide-react"; +import { fn } from "storybook/test"; +import { SlackIcon } from "@/components/slack-icon"; +import { IntegrationCard } from "./integration-card"; + +// GitHub icon component +function GitHubIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +const meta: Meta = { + title: "Settings/Integrations/IntegrationCard", + component: IntegrationCard, + parameters: { + layout: "centered", + }, + args: { + onConfigure: fn(), + }, + render: (args) => ( +
+ +
+ ), +}; + +export default meta; +type Story = StoryObj; + +// ============================================================================= +// Not Configured States +// ============================================================================= + +export const LLM_NotConfigured: Story = { + args: { + title: "LLM API Key", + description: "Configure an API key for AI capabilities", + icon: , + iconBgColor: "bg-amber-500", + configured: false, + }, +}; +LLM_NotConfigured.storyName = "LLM - Not Configured"; + +export const WebSearch_NotConfigured: Story = { + args: { + title: "Web Search (Exa)", + description: "Enable web search capabilities", + icon: , + iconBgColor: "bg-blue-500", + configured: false, + }, +}; +WebSearch_NotConfigured.storyName = "Web Search - Not Configured"; + +export const GitHub_NotConfigured: Story = { + args: { + title: "GitHub", + description: "Connect to GitHub repositories", + icon: , + iconBgColor: "bg-[#24292f]", + configured: false, + }, +}; +GitHub_NotConfigured.storyName = "GitHub - Not Configured"; + +export const Slack_NotConfigured: Story = { + args: { + title: "Slack", + description: "Chat with your agent in Slack", + icon: , + iconBgColor: "bg-[#4A154B]", + configured: false, + }, +}; +Slack_NotConfigured.storyName = "Slack - Not Configured"; + +// ============================================================================= +// Configured States +// ============================================================================= + +export const LLM_Configured: Story = { + args: { + title: "LLM API Key", + description: "Configure an API key for AI capabilities", + icon: , + iconBgColor: "bg-amber-500", + configured: true, + }, +}; +LLM_Configured.storyName = "LLM - Configured"; + +export const WebSearch_Configured: Story = { + args: { + title: "Web Search (Exa)", + description: "Enable web search capabilities", + icon: , + iconBgColor: "bg-blue-500", + configured: true, + }, +}; +WebSearch_Configured.storyName = "Web Search - Configured"; + +export const GitHub_Configured: Story = { + args: { + title: "GitHub", + description: "Connect to GitHub repositories", + icon: , + iconBgColor: "bg-[#24292f]", + configured: true, + }, +}; +GitHub_Configured.storyName = "GitHub - Configured"; + +export const Slack_Configured: Story = { + args: { + title: "Slack", + description: "Chat with your agent in Slack", + icon: , + iconBgColor: "bg-[#4A154B]", + configured: true, + }, +}; +Slack_Configured.storyName = "Slack - Configured"; + +// ============================================================================= +// All Cards Grid +// ============================================================================= + +export const AllCards: Story = { + render: () => ( +
+ } + iconBgColor="bg-amber-500" + configured={false} + onConfigure={fn()} + /> + } + iconBgColor="bg-blue-500" + configured={true} + onConfigure={fn()} + /> + } + iconBgColor="bg-[#24292f]" + configured={false} + onConfigure={fn()} + /> + } + iconBgColor="bg-[#4A154B]" + configured={true} + onConfigure={fn()} + /> +
+ ), +}; +AllCards.storyName = "All Cards (Mixed States)"; diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/integrations/integration-card.tsx b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/integration-card.tsx new file mode 100644 index 0000000..c08f85f --- /dev/null +++ b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/integration-card.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { Check, Plus, Settings } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +interface IntegrationCardProps { + title: string; + description: string; + icon: React.ReactNode; + iconBgColor: string; + configured: boolean; + onConfigure: () => void; +} + +export function IntegrationCard({ + title, + description, + icon, + iconBgColor, + configured, + onConfigure, +}: IntegrationCardProps) { + return ( + + +
+
+ {icon} +
+
+ {title} + + {description} + +
+
+ {configured ? ( + <> +
+ + Connected +
+ + + ) : ( + + )} +
+
+
+
+ ); +} diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/integrations/integrations-manager.stories.tsx b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/integrations-manager.stories.tsx new file mode 100644 index 0000000..a84986e --- /dev/null +++ b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/integrations-manager.stories.tsx @@ -0,0 +1,536 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; +import { withFetch } from "@/.storybook/utils"; +import IntegrationsManager from "./integrations-manager"; + +const TEST_AGENT_ID = "test-agent-123"; +const TEST_WEBHOOK_URL = "https://api.blink.so/webhooks/test-webhook-id"; +const TEST_GITHUB_URL = "https://github.com/settings/apps/new"; +const TEST_MANIFEST = JSON.stringify({ + name: "Test App", + url: "https://blink.so", +}); +const TEST_SESSION_ID = "test-session-456"; + +// Create mock fetch decorator for all integrations +const createMockFetchDecorator = (options?: { + // Slack options + slackValidationValid?: boolean; + slackDmReceived?: boolean; + slackSignatureFailed?: boolean; + // GitHub options + githubCreationStatus?: + | "pending" + | "app_created" + | "completed" + | "failed" + | "expired"; + githubAppData?: { + id: number; + name: string; + html_url: string; + slug: string; + }; +}) => { + const { + slackValidationValid = true, + slackDmReceived = false, + slackSignatureFailed = false, + githubCreationStatus = "pending", + githubAppData = { + id: 12345, + name: "Test GitHub App", + html_url: "https://github.com/apps/test-github-app", + slug: "test-github-app", + }, + } = options ?? {}; + + return withFetch((url, init) => { + // ========================================================================== + // Slack API mocks + // ========================================================================== + + // GET /api/agents/{agentId}/setup/slack/webhook-url + if ( + url.pathname.includes("/setup/slack/webhook-url") && + (!init?.method || init.method === "GET") + ) { + return new Response(JSON.stringify({ webhook_url: TEST_WEBHOOK_URL }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + // POST /api/agents/{agentId}/setup/slack/start-verification + if ( + url.pathname.includes("/setup/slack/start-verification") && + init?.method === "POST" + ) { + return new Response(JSON.stringify({ webhook_url: TEST_WEBHOOK_URL }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + // GET /api/agents/{agentId}/setup/slack/verification-status + if ( + url.pathname.includes("/setup/slack/verification-status") && + (!init?.method || init.method === "GET") + ) { + return new Response( + JSON.stringify({ + active: true, + started_at: new Date().toISOString(), + last_event_at: slackDmReceived ? new Date().toISOString() : undefined, + dm_received: slackDmReceived, + dm_channel: slackDmReceived ? "D12345678" : undefined, + signature_failed: slackSignatureFailed, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // POST /api/onboarding/validate-credentials + if ( + url.pathname.includes("/onboarding/validate-credentials") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ + valid: slackValidationValid, + error: slackValidationValid ? undefined : "Invalid token", + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // POST /api/agents/{agentId}/setup/slack/complete-verification + if ( + url.pathname.includes("/setup/slack/complete-verification") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ success: true, bot_name: "Test Bot" }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // POST /api/agents/{agentId}/setup/slack/cancel-verification + if ( + url.pathname.includes("/setup/slack/cancel-verification") && + init?.method === "POST" + ) { + return new Response(null, { status: 204 }); + } + + // ========================================================================== + // GitHub API mocks + // ========================================================================== + + // GET /api/agents/{agentId}/setup/github/webhook-url + if ( + url.pathname.includes("/setup/github/webhook-url") && + (!init?.method || init.method === "GET") + ) { + return new Response(JSON.stringify({ webhook_url: TEST_WEBHOOK_URL }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + // POST /api/agents/{agentId}/setup/github/start-creation + if ( + url.pathname.includes("/setup/github/start-creation") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ + manifest: TEST_MANIFEST, + github_url: TEST_GITHUB_URL, + session_id: TEST_SESSION_ID, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // GET /api/agents/{agentId}/setup/github/creation-status/{sessionId} + if ( + url.pathname.includes("/setup/github/creation-status") && + (!init?.method || init.method === "GET") + ) { + return new Response( + JSON.stringify({ + status: githubCreationStatus, + app_data: + githubCreationStatus === "completed" || + githubCreationStatus === "app_created" + ? githubAppData + : undefined, + credentials: + githubCreationStatus === "completed" + ? { + app_id: githubAppData.id, + client_id: "Iv1.test123", + client_secret: "test-client-secret", + webhook_secret: "test-webhook-secret", + private_key: btoa("test-private-key"), + } + : undefined, + error: + githubCreationStatus === "failed" + ? "Something went wrong" + : undefined, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // POST /api/agents/{agentId}/setup/github/complete-creation + if ( + url.pathname.includes("/setup/github/complete-creation") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ + success: true, + app_name: githubAppData.name, + app_url: githubAppData.html_url, + install_url: `${githubAppData.html_url}/installations/new`, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // POST /api/agents/{agentId}/setup/github/cancel-creation + if ( + url.pathname.includes("/setup/github/cancel-creation") && + init?.method === "POST" + ) { + return new Response(null, { status: 204 }); + } + + // ========================================================================== + // Environment Variables API mocks + // ========================================================================== + + // POST /api/agents/{agentId}/env + if (url.pathname.includes("/env") && init?.method === "POST") { + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + return undefined; + }); +}; + +const meta: Meta = { + title: "Settings/Integrations/IntegrationsManager", + component: IntegrationsManager, + parameters: { + layout: "centered", + }, + args: { + agentId: TEST_AGENT_ID, + agentName: "Scout", + organizationId: "org-123", + organizationName: "my-org", + }, + render: (args) => ( +
+ +
+ ), + decorators: [createMockFetchDecorator()], +}; + +export default meta; +type Story = StoryObj; + +// ============================================================================= +// Default State (All Cards) +// ============================================================================= + +export const Default: Story = {}; +Default.storyName = "Default (All Cards)"; + +// ============================================================================= +// Interactive Flow with Controls +// ============================================================================= + +// Settings that the interactive fetch mock can read +const interactiveSettings = { + slackBotTokenValid: true, + slackSigningSecretValid: true, + slackPollCount: 0, + githubPollCount: 0, +}; + +function InteractiveFlowWrapper() { + const [slackBotTokenValid, setSlackBotTokenValid] = useState(true); + const [slackSigningSecretValid, setSlackSigningSecretValid] = useState(true); + const [key, setKey] = useState(0); + + // Update global settings when state changes + interactiveSettings.slackBotTokenValid = slackBotTokenValid; + interactiveSettings.slackSigningSecretValid = slackSigningSecretValid; + + const resetIntegrations = () => { + interactiveSettings.slackPollCount = 0; + interactiveSettings.githubPollCount = 0; + setKey((k) => k + 1); + }; + + return ( +
+
+ +
+
+

Test Controls

+ +
+

Slack

+ + + +
+ +
+ + + +

+ Toggle checkboxes to simulate different API responses. GitHub will + auto-progress through stages after clicking "Create & install on + GitHub". +

+
+
+ ); +} + +export const InteractiveFlow: Story = { + render: () => , + decorators: [ + withFetch((url, init) => { + // ========================================================================== + // Slack API mocks (interactive) + // ========================================================================== + + if ( + url.pathname.includes("/setup/slack/webhook-url") && + (!init?.method || init.method === "GET") + ) { + return new Response(JSON.stringify({ webhook_url: TEST_WEBHOOK_URL }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + if ( + url.pathname.includes("/setup/slack/start-verification") && + init?.method === "POST" + ) { + interactiveSettings.slackPollCount = 0; + return new Response(JSON.stringify({ webhook_url: TEST_WEBHOOK_URL }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + if ( + url.pathname.includes("/setup/slack/verification-status") && + (!init?.method || init.method === "GET") + ) { + interactiveSettings.slackPollCount++; + const dmReceived = interactiveSettings.slackPollCount >= 3; + const signatureFailed = + dmReceived && !interactiveSettings.slackSigningSecretValid; + return new Response( + JSON.stringify({ + active: true, + started_at: new Date().toISOString(), + last_event_at: + interactiveSettings.slackPollCount > 1 + ? new Date().toISOString() + : undefined, + dm_received: dmReceived, + dm_channel: dmReceived ? "D12345678" : undefined, + signature_failed: signatureFailed, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + if ( + url.pathname.includes("/onboarding/validate-credentials") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ + valid: interactiveSettings.slackBotTokenValid, + error: interactiveSettings.slackBotTokenValid + ? undefined + : "Invalid bot token", + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + if ( + url.pathname.includes("/setup/slack/complete-verification") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ success: true, bot_name: "Scout Bot" }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + if ( + url.pathname.includes("/setup/slack/cancel-verification") && + init?.method === "POST" + ) { + return new Response(null, { status: 204 }); + } + + // ========================================================================== + // GitHub API mocks (interactive - auto-progresses) + // ========================================================================== + + if ( + url.pathname.includes("/setup/github/webhook-url") && + (!init?.method || init.method === "GET") + ) { + return new Response(JSON.stringify({ webhook_url: TEST_WEBHOOK_URL }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + if ( + url.pathname.includes("/setup/github/start-creation") && + init?.method === "POST" + ) { + interactiveSettings.githubPollCount = 0; + return new Response( + JSON.stringify({ + manifest: TEST_MANIFEST, + github_url: TEST_GITHUB_URL, + session_id: TEST_SESSION_ID, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + if ( + url.pathname.includes("/setup/github/creation-status") && + (!init?.method || init.method === "GET") + ) { + interactiveSettings.githubPollCount++; + // Simulate progression: pending -> app_created -> completed + let status: "pending" | "app_created" | "completed" = "pending"; + if (interactiveSettings.githubPollCount >= 5) { + status = "completed"; + } else if (interactiveSettings.githubPollCount >= 3) { + status = "app_created"; + } + + const appData = { + id: 12345, + name: "my-org-Scout", + html_url: "https://github.com/apps/my-org-scout", + slug: "my-org-scout", + }; + + return new Response( + JSON.stringify({ + status, + app_data: status !== "pending" ? appData : undefined, + credentials: + status === "completed" + ? { + app_id: appData.id, + client_id: "Iv1.test123", + client_secret: "test-client-secret-12345", + webhook_secret: "test-webhook-secret-67890", + private_key: btoa( + "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----" + ), + } + : undefined, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + if ( + url.pathname.includes("/setup/github/complete-creation") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ + success: true, + app_name: "my-org-Scout", + app_url: "https://github.com/apps/my-org-scout", + install_url: + "https://github.com/apps/my-org-scout/installations/new", + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + if ( + url.pathname.includes("/setup/github/cancel-creation") && + init?.method === "POST" + ) { + return new Response(null, { status: 204 }); + } + + // ========================================================================== + // Environment Variables API mocks + // ========================================================================== + + if (url.pathname.includes("/env") && init?.method === "POST") { + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + return undefined; + }), + ], +}; +InteractiveFlow.storyName = "Interactive Flow"; diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/integrations/integrations-manager.tsx b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/integrations-manager.tsx new file mode 100644 index 0000000..63406df --- /dev/null +++ b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/integrations-manager.tsx @@ -0,0 +1,188 @@ +"use client"; + +import type { OnboardingState } from "@blink.so/api"; +import { Key, Search } from "lucide-react"; +import { useState } from "react"; +import { SlackIcon } from "@/components/slack-icon"; +import { GitHubIntegration } from "./github-integration"; +import { IntegrationCard } from "./integration-card"; +import { LlmIntegration } from "./llm-integration"; +import { SlackIntegration } from "./slack-integration"; +import { WebSearchIntegration } from "./web-search-integration"; + +type ActiveSetup = "llm" | "web-search" | "github" | "slack" | null; + +interface IntegrationsManagerProps { + agentId: string; + agentName: string; + organizationId: string; + organizationName: string; + onboardingState: OnboardingState | null; +} + +// GitHub icon component +function GitHubIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +export default function IntegrationsManager({ + agentId, + agentName, + organizationName, + onboardingState, +}: IntegrationsManagerProps) { + const [activeSetup, setActiveSetup] = useState(null); + // Derive initial configured status from onboardingState, track locally for immediate updates + const [configured, setConfigured] = useState({ + llm: !!onboardingState?.apiKeys?.aiApiKey, + webSearch: !!onboardingState?.apiKeys?.exaApiKey, + github: !!onboardingState?.github?.appId, + slack: !!onboardingState?.slack, + }); + + const handleComplete = (integration: keyof typeof configured) => { + setConfigured((prev) => ({ ...prev, [integration]: true })); + setActiveSetup(null); + }; + + const handleCancel = () => { + setActiveSetup(null); + }; + + // Render active setup wizard + if (activeSetup === "llm") { + return ( +
+
+

LLM API Key Setup

+

+ Configure an API key for AI capabilities. +

+
+ handleComplete("llm")} + onCancel={handleCancel} + /> +
+ ); + } + + if (activeSetup === "web-search") { + return ( +
+
+

Web Search Setup

+

+ Enable web search capabilities for your agent. +

+
+ handleComplete("webSearch")} + onCancel={handleCancel} + /> +
+ ); + } + + if (activeSetup === "github") { + return ( +
+
+

GitHub Integration

+

+ Connect your agent to GitHub repositories. +

+
+ handleComplete("github")} + onCancel={handleCancel} + /> +
+ ); + } + + if (activeSetup === "slack") { + return ( +
+
+

Slack Integration

+

+ Connect your agent to Slack to chat with it directly in your + workspace. +

+
+ handleComplete("slack")} + onCancel={handleCancel} + /> +
+ ); + } + + // Render integration cards grid + return ( +
+
+

Integrations

+

+ Connect your agent to external services to extend its capabilities. +

+
+
+ } + iconBgColor="bg-amber-500" + configured={configured.llm} + onConfigure={() => setActiveSetup("llm")} + /> + } + iconBgColor="bg-blue-500" + configured={configured.webSearch} + onConfigure={() => setActiveSetup("web-search")} + /> + } + iconBgColor="bg-[#24292f]" + configured={configured.github} + onConfigure={() => setActiveSetup("github")} + /> + } + iconBgColor="bg-[#4A154B]" + configured={configured.slack} + onConfigure={() => setActiveSetup("slack")} + /> +
+
+ ); +} diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/integrations/llm-integration.tsx b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/llm-integration.tsx new file mode 100644 index 0000000..92c33d6 --- /dev/null +++ b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/llm-integration.tsx @@ -0,0 +1,117 @@ +"use client"; + +import type { OnboardingState } from "@blink.so/api"; +import { Key } from "lucide-react"; +import { + getEnvVarKeyForProvider, + type LlmApiKeysResult, + LlmApiKeysSetup, +} from "@/components/llm-api-keys-setup"; +import { EnvVarConfirmation } from "./env-var-confirmation"; +import { useIntegrationSetup } from "./use-integration-setup"; + +interface LlmIntegrationProps { + agentId: string; + onboardingState: OnboardingState | null; + onComplete: () => void; + onCancel: () => void; +} + +export function LlmIntegration({ + agentId, + onboardingState, + onComplete, + onCancel, +}: LlmIntegrationProps) { + const { + step, + setStep, + setupResult, + envVars, + setEnvVars, + saving, + handleSave, + handleSetupComplete, + } = useIntegrationSetup({ + agentId, + onboardingState, + onComplete, + successMessage: "LLM API key configured successfully", + + hasSavedState: (state) => + !!(state?.apiKeys?.aiApiKey && state?.apiKeys?.aiProvider), + + buildSetupResult: (state) => { + const apiKeys = state.apiKeys; + return { + provider: apiKeys?.aiProvider ?? "anthropic", + apiKey: apiKeys?.aiApiKey ?? "", + }; + }, + + buildEnvVars: (state) => { + const apiKeys = state.apiKeys; + if (!apiKeys?.aiApiKey || !apiKeys?.aiProvider) return []; + const envVarKey = + apiKeys.aiEnvVar ?? getEnvVarKeyForProvider(apiKeys.aiProvider); + return [ + { + defaultKey: envVarKey, + currentKey: envVarKey, + value: apiKeys.aiApiKey, + secret: true, + }, + ]; + }, + + buildOnboardingUpdate: (result, vars, currentState) => ({ + apiKeys: { + ...currentState?.apiKeys, + aiProvider: result.provider, + aiApiKey: result.apiKey, + aiEnvVar: vars[0].currentKey, + }, + }), + }); + + const onSetupComplete = (result: LlmApiKeysResult) => { + const envVarKey = getEnvVarKeyForProvider(result.provider); + handleSetupComplete(result, [ + { + defaultKey: envVarKey, + currentKey: envVarKey, + value: result.apiKey, + secret: true, + }, + ]); + }; + + if (step === "confirm") { + return ( + } + iconBgColor="bg-amber-500" + envVars={envVars} + onEnvVarsChange={setEnvVars} + onSave={handleSave} + onCancel={onCancel} + onBack={() => setStep("setup")} + saving={saving} + /> + ); + } + + return ( + + ); +} diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/integrations/page.tsx b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/page.tsx new file mode 100644 index 0000000..ac65a15 --- /dev/null +++ b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/page.tsx @@ -0,0 +1,39 @@ +import { notFound, redirect } from "next/navigation"; +import { auth } from "@/app/(auth)/auth"; +import { getAgent, getOrganization } from "../../../layout"; +import IntegrationsManager from "./integrations-manager"; + +export default async function IntegrationsSettingsPage({ + params, +}: { + params: Promise<{ organization: string; agent: string }>; +}) { + const session = await auth(); + if (!session || !session?.user?.id) { + return redirect("/login"); + } + + const { organization: organizationName, agent: agentName } = await params; + const [organization, agent] = await Promise.all([ + getOrganization(session.user.id, organizationName), + getAgent(organizationName, agentName), + ]); + + // Check if user has admin permission for this agent + const permission = agent.user_permission ?? "read"; + if (permission !== "admin") { + return notFound(); + } + + return ( +
+ +
+ ); +} diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/integrations/slack-integration.tsx b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/slack-integration.tsx new file mode 100644 index 0000000..5af4af6 --- /dev/null +++ b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/slack-integration.tsx @@ -0,0 +1,136 @@ +"use client"; + +import type { OnboardingState } from "@blink.so/api"; +import { SlackIcon } from "@/components/slack-icon"; +import { SlackSetupWizard } from "@/components/slack-setup-wizard"; +import { useAPIClient } from "@/lib/api-client"; +import { EnvVarConfirmation } from "./env-var-confirmation"; +import { useIntegrationSetup } from "./use-integration-setup"; + +interface SlackCredentials { + botToken: string; + signingSecret: string; +} + +interface SlackIntegrationProps { + agentId: string; + agentName: string; + onboardingState: OnboardingState | null; + onComplete: () => void; + onCancel: () => void; +} + +export function SlackIntegration({ + agentId, + agentName, + onboardingState, + onComplete, + onCancel, +}: SlackIntegrationProps) { + const client = useAPIClient(); + + const { + step, + setStep, + envVars, + setEnvVars, + saving, + handleSave, + handleSetupComplete, + } = useIntegrationSetup({ + agentId, + onboardingState, + onComplete, + successMessage: "Slack integration configured successfully", + + hasSavedState: (state) => !!state?.slack?.botToken, + + buildSetupResult: (state) => { + const slack = state.slack; + return { + botToken: slack?.botToken ?? "", + signingSecret: slack?.signingSecret ?? "", + }; + }, + + buildEnvVars: (state) => { + const slack = state.slack; + if (!slack?.botToken) return []; + const envVarNames = slack.envVars; + return [ + { + defaultKey: "SLACK_BOT_TOKEN", + currentKey: envVarNames?.botToken ?? "SLACK_BOT_TOKEN", + value: slack.botToken, + secret: true, + }, + { + defaultKey: "SLACK_SIGNING_SECRET", + currentKey: envVarNames?.signingSecret ?? "SLACK_SIGNING_SECRET", + value: slack.signingSecret, + secret: true, + }, + ]; + }, + + buildOnboardingUpdate: (result, vars) => { + const envVarMap = Object.fromEntries( + vars.map((ev) => [ev.defaultKey, ev.currentKey]) + ); + return { + slack: { + botToken: result.botToken, + signingSecret: result.signingSecret, + envVars: { + botToken: envVarMap.SLACK_BOT_TOKEN, + signingSecret: envVarMap.SLACK_SIGNING_SECRET, + }, + }, + }; + }, + }); + + const onSetupComplete = (result: SlackCredentials) => { + handleSetupComplete(result, [ + { + defaultKey: "SLACK_BOT_TOKEN", + currentKey: "SLACK_BOT_TOKEN", + value: result.botToken, + secret: true, + }, + { + defaultKey: "SLACK_SIGNING_SECRET", + currentKey: "SLACK_SIGNING_SECRET", + value: result.signingSecret, + secret: true, + }, + ]); + }; + + if (step === "confirm") { + return ( + } + iconBgColor="bg-[#4A154B]" + envVars={envVars} + onEnvVarsChange={setEnvVars} + onSave={handleSave} + onCancel={onCancel} + onBack={() => setStep("setup")} + saving={saving} + /> + ); + } + + return ( + + ); +} diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/integrations/use-integration-setup.ts b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/use-integration-setup.ts new file mode 100644 index 0000000..62d33d6 --- /dev/null +++ b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/use-integration-setup.ts @@ -0,0 +1,149 @@ +"use client"; + +import type { OnboardingState } from "@blink.so/api"; +import { useCallback, useState } from "react"; +import { toast } from "sonner"; +import { useAPIClient } from "@/lib/api-client"; +import type { EnvVarConfig } from "./env-var-confirmation"; + +type SetupStep = "setup" | "confirm"; + +interface UseIntegrationSetupOptions { + agentId: string; + onboardingState: OnboardingState | null; + onComplete: () => void; + + /** Check if there's saved state to restore */ + hasSavedState: (state: OnboardingState | null) => boolean; + + /** Build the setup result from saved state */ + buildSetupResult: (state: OnboardingState) => TSetupResult; + + /** Build the env vars from saved state */ + buildEnvVars: (state: OnboardingState) => EnvVarConfig[]; + + /** Build the onboarding state update from setup result and env vars */ + buildOnboardingUpdate: ( + setupResult: TSetupResult, + envVars: EnvVarConfig[], + currentState: OnboardingState | null + ) => Partial; + + /** Success message to show */ + successMessage: string; +} + +interface UseIntegrationSetupReturn { + step: SetupStep; + setStep: (step: SetupStep) => void; + setupResult: TSetupResult | null; + setSetupResult: (result: TSetupResult | null) => void; + envVars: EnvVarConfig[]; + setEnvVars: (envVars: EnvVarConfig[]) => void; + saving: boolean; + handleSave: () => Promise; + handleSetupComplete: (result: TSetupResult, envVars: EnvVarConfig[]) => void; +} + +export function useIntegrationSetup({ + agentId, + onboardingState, + onComplete, + hasSavedState, + buildSetupResult, + buildEnvVars, + buildOnboardingUpdate, + successMessage, +}: UseIntegrationSetupOptions): UseIntegrationSetupReturn { + const client = useAPIClient(); + const hasSaved = hasSavedState(onboardingState); + + // Always start at setup step - saved state pre-populates forms but doesn't skip ahead + const [step, setStep] = useState("setup"); + + const [setupResult, setSetupResult] = useState(() => { + if (hasSaved && onboardingState) { + return buildSetupResult(onboardingState); + } + return null; + }); + + const [envVars, setEnvVars] = useState(() => { + if (hasSaved && onboardingState) { + return buildEnvVars(onboardingState); + } + return []; + }); + + const [saving, setSaving] = useState(false); + + const handleSetupComplete = useCallback( + (result: TSetupResult, newEnvVars: EnvVarConfig[]) => { + setSetupResult(result); + setEnvVars(newEnvVars); + setStep("confirm"); + }, + [] + ); + + const handleSave = useCallback(async () => { + if (!setupResult) return; + + setSaving(true); + try { + // Save env vars + await Promise.all( + envVars.map((envVar) => + client.agents.env.create({ + agent_id: agentId, + key: envVar.currentKey, + value: envVar.value, + secret: envVar.secret, + target: ["preview", "production"], + upsert: true, + }) + ) + ); + + // Update onboarding_state + const update = buildOnboardingUpdate( + setupResult, + envVars, + onboardingState + ); + await client.agents.updateOnboarding(agentId, update); + + toast.success(successMessage); + onComplete(); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Failed to save environment variables" + ); + } finally { + setSaving(false); + } + }, [ + agentId, + client, + envVars, + onboardingState, + onComplete, + setupResult, + buildOnboardingUpdate, + successMessage, + ]); + + return { + step, + setStep, + setupResult, + setSetupResult, + envVars, + setEnvVars, + saving, + handleSave, + handleSetupComplete, + }; +} diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/integrations/web-search-integration.tsx b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/web-search-integration.tsx new file mode 100644 index 0000000..fe67cbd --- /dev/null +++ b/packages/site/app/(app)/[organization]/[agent]/settings/integrations/web-search-integration.tsx @@ -0,0 +1,108 @@ +"use client"; + +import type { OnboardingState } from "@blink.so/api"; +import { Search } from "lucide-react"; +import { + EXA_ENV_VAR_KEY, + type WebSearchResult, + WebSearchSetup, +} from "@/components/web-search-setup"; +import { EnvVarConfirmation } from "./env-var-confirmation"; +import { useIntegrationSetup } from "./use-integration-setup"; + +interface WebSearchIntegrationProps { + agentId: string; + onboardingState: OnboardingState | null; + onComplete: () => void; + onCancel: () => void; +} + +export function WebSearchIntegration({ + agentId, + onboardingState, + onComplete, + onCancel, +}: WebSearchIntegrationProps) { + const { + step, + setStep, + setupResult, + envVars, + setEnvVars, + saving, + handleSave, + handleSetupComplete, + } = useIntegrationSetup({ + agentId, + onboardingState, + onComplete, + successMessage: "Web search configured successfully", + + hasSavedState: (state) => !!state?.apiKeys?.exaApiKey, + + buildSetupResult: (state) => { + const apiKeys = state.apiKeys; + return { + exaApiKey: apiKeys?.exaApiKey ?? "", + }; + }, + + buildEnvVars: (state) => { + const apiKeys = state.apiKeys; + if (!apiKeys?.exaApiKey) return []; + const envVarKey = apiKeys.exaEnvVar ?? EXA_ENV_VAR_KEY; + return [ + { + defaultKey: EXA_ENV_VAR_KEY, + currentKey: envVarKey, + value: apiKeys.exaApiKey, + secret: true, + }, + ]; + }, + + buildOnboardingUpdate: (result, vars, currentState) => ({ + apiKeys: { + ...currentState?.apiKeys, + exaApiKey: result.exaApiKey, + exaEnvVar: vars[0].currentKey, + }, + }), + }); + + const onSetupComplete = (result: WebSearchResult) => { + handleSetupComplete(result, [ + { + defaultKey: EXA_ENV_VAR_KEY, + currentKey: EXA_ENV_VAR_KEY, + value: result.exaApiKey, + secret: true, + }, + ]); + }; + + if (step === "confirm") { + return ( + } + iconBgColor="bg-blue-500" + envVars={envVars} + onEnvVarsChange={setEnvVars} + onSave={handleSave} + onCancel={onCancel} + onBack={() => setStep("setup")} + saving={saving} + /> + ); + } + + return ( + + ); +} diff --git a/packages/site/app/(app)/[organization]/[agent]/settings/navigation.tsx b/packages/site/app/(app)/[organization]/[agent]/settings/navigation.tsx index 8f35faa..ef18fd4 100644 --- a/packages/site/app/(app)/[organization]/[agent]/settings/navigation.tsx +++ b/packages/site/app/(app)/[organization]/[agent]/settings/navigation.tsx @@ -17,11 +17,17 @@ export function AgentSettingsNav() { label: "Environment Variables", href: `${baseHref}/env`, }, + { + value: "integrations", + label: "Integrations", + href: `${baseHref}/integrations`, + }, { value: "webhooks", label: "Webhooks", href: `${baseHref}/webhooks` }, ]; const getActiveTab = (pathname: string | null) => { if (pathname?.includes("/settings/env")) return "environment"; + if (pathname?.includes("/settings/integrations")) return "integrations"; if (pathname?.includes("/settings/webhooks")) return "webhooks"; return "general"; }; diff --git a/packages/site/app/(app)/[organization]/layout.tsx b/packages/site/app/(app)/[organization]/layout.tsx index cae5272..4fee53a 100644 --- a/packages/site/app/(app)/[organization]/layout.tsx +++ b/packages/site/app/(app)/[organization]/layout.tsx @@ -62,6 +62,16 @@ export const getUser = cache(async (userID: string) => { }); export const getAgent = cache( + async (organizationName: string, agentName: string) => { + const agent = await getAgentOrNull(organizationName, agentName); + if (!agent) { + return notFound(); + } + return agent; + } +); + +export const getAgentOrNull = cache( async (organizationName: string, agentName: string) => { const session = await auth(); const userID = session?.user?.id; @@ -72,7 +82,7 @@ export const getAgent = cache( userID, }); if (!agent) { - return notFound(); + return null; } // Get the production deployment target's request_id const productionTarget = await db.selectAgentDeploymentTargetByName( @@ -104,7 +114,7 @@ export const getAgent = cache( }); // If permission is undefined, user doesn't have access if (userPermission === undefined) { - return notFound(); + return null; } } } diff --git a/packages/site/app/(app)/[organization]/page.stories.tsx b/packages/site/app/(app)/[organization]/page.stories.tsx index ae3cbd3..d384b19 100644 --- a/packages/site/app/(app)/[organization]/page.stories.tsx +++ b/packages/site/app/(app)/[organization]/page.stories.tsx @@ -66,6 +66,9 @@ export const Default: Story = { chat_expire_ttl: null, last_deployment_number: 0, last_run_number: 0, + slack_verification: null, + github_app_setup: null, + onboarding_state: null, active_deployment_created_by: "1", active_deployment_created_at: new Date(), }, @@ -84,6 +87,9 @@ export const Default: Story = { chat_expire_ttl: null, last_deployment_number: 0, last_run_number: 0, + slack_verification: null, + github_app_setup: null, + onboarding_state: null, active_deployment_created_by: null, active_deployment_created_at: null, }, diff --git a/packages/site/app/(app)/[organization]/page.tsx b/packages/site/app/(app)/[organization]/page.tsx index d9fc8f8..0a550ff 100644 --- a/packages/site/app/(app)/[organization]/page.tsx +++ b/packages/site/app/(app)/[organization]/page.tsx @@ -113,6 +113,25 @@ export default async function Page({ const isPersonal = organization.id === user.organization_id; + const DEFAULT_AGENT_NAME = "blink"; + + // Find an agent with onboarding in progress (finished === false) + const onboardingAgent = agents.find( + (a) => a.onboarding_state?.finished === false + ); + + // Redirect to onboarding if organization has no agents + if (agents.length === 0) { + return redirect(`/${organizationName}/~/onboarding/${DEFAULT_AGENT_NAME}`); + } + + // Redirect to agent onboarding if there's an agent being onboarded + if (onboardingAgent) { + return redirect( + `/${organizationName}/~/onboarding/${onboardingAgent.name}` + ); + } + return (
diff --git a/packages/site/app/(app)/[organization]/~/onboarding/[agent]/page.tsx b/packages/site/app/(app)/[organization]/~/onboarding/[agent]/page.tsx new file mode 100644 index 0000000..dea4112 --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/[agent]/page.tsx @@ -0,0 +1,62 @@ +import type { Metadata } from "next"; +import { redirect } from "next/navigation"; +import { auth } from "@/app/(auth)/auth"; +import Header from "@/components/header"; +import { getAgentOrNull, getOrganization, getUser } from "../../../layout"; +import { OrganizationNavigation } from "../../../navigation"; +import { AgentOnboardingWizard } from "./wizard"; + +export const metadata: Metadata = { + title: "Setup - Blink", +}; + +export default async function AgentOnboardingPage({ + params, +}: { + params: Promise<{ organization: string; agent: string }>; +}) { + const session = await auth(); + if (!session?.user?.id) { + return redirect("/login"); + } + + const { organization: organizationName, agent: agentName } = await params; + const [organization, agent] = await Promise.all([ + getOrganization(session.user.id, organizationName), + getAgentOrNull(organizationName, agentName), + ]); + const user = await getUser(session.user.id); + + // If agent exists but is not in onboarding, redirect to agent page + if (agent && agent.onboarding_state?.finished !== false) { + return redirect(`/${organizationName}/${agentName}`); + } + + const isPersonal = organization.id === user.organization_id; + + return ( +
+
+ +
+ +
+
+ ); +} diff --git a/packages/site/app/(app)/[organization]/~/onboarding/[agent]/wizard.tsx b/packages/site/app/(app)/[organization]/~/onboarding/[agent]/wizard.tsx new file mode 100644 index 0000000..c3ab950 --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/[agent]/wizard.tsx @@ -0,0 +1,80 @@ +"use client"; + +import type Client from "@blink.so/api"; +import type { OnboardingState } from "@blink.so/api"; +import { useAPIClient } from "@/lib/api-client"; +import { type AgentInfo, WizardContent } from "../components/wizard-content"; + +export type { OnboardingState }; +export type OnboardingStep = OnboardingState["currentStep"]; + +/** + * AgentOnboardingWizard with client injection support for testing. + * When client is provided, useAPIClient() is not called. + */ +export function AgentOnboardingWizard({ + organizationId, + organizationName, + agentName, + agent, + client, + initialState, +}: { + organizationId: string; + organizationName: string; + agentName: string; + /** Existing agent (for resuming onboarding) */ + agent?: AgentInfo; + /** Optional client for testing/stories */ + client?: Client; + /** Optional initial state for testing/stories - bypasses API calls */ + initialState?: Partial< + OnboardingState & { agentId?: string; agentName?: string } + >; +}) { + if (client) { + return ( + + ); + } + + return ( + + ); +} + +function AgentOnboardingWizardWithHook({ + organizationId, + organizationName, + agentName, + agent, +}: { + organizationId: string; + organizationName: string; + agentName: string; + agent?: AgentInfo; +}) { + const client = useAPIClient(); + + return ( + + ); +} diff --git a/packages/site/app/(app)/[organization]/~/onboarding/components/progress-indicator.tsx b/packages/site/app/(app)/[organization]/~/onboarding/components/progress-indicator.tsx new file mode 100644 index 0000000..1085743 --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/components/progress-indicator.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { Check } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const stepLabels: Record = { + welcome: "Welcome", + "llm-api-keys": "LLM", + "github-setup": "GitHub", + "slack-setup": "Slack", + "web-search": "Web Search", + deploying: "Deploy", +}; + +interface ProgressIndicatorProps { + steps: string[]; + currentStep: string; + onStepClick?: (step: string) => void; +} + +export function ProgressIndicator({ + steps, + currentStep, + onStepClick, +}: ProgressIndicatorProps) { + const currentIndex = steps.indexOf(currentStep); + + return ( +
+ {steps.map((step, index) => { + const isComplete = index < currentIndex; + const isCurrent = index === currentIndex; + + return ( +
+ + {index < steps.length - 1 && ( +
+ )} +
+ ); + })} +
+ ); +} diff --git a/packages/site/app/(app)/[organization]/~/onboarding/components/wizard-content.tsx b/packages/site/app/(app)/[organization]/~/onboarding/components/wizard-content.tsx new file mode 100644 index 0000000..943d154 --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/components/wizard-content.tsx @@ -0,0 +1,244 @@ +"use client"; + +import type Client from "@blink.so/api"; +import type { OnboardingState } from "@blink.so/api"; +import { useRouter } from "next/navigation"; +import { useCallback, useState } from "react"; +import { DeployingStep } from "../steps/deploying"; +import { GitHubSetupStep } from "../steps/github-setup"; +import { LlmApiKeysStep } from "../steps/llm-api-keys"; +import { SlackSetupStep } from "../steps/slack-setup"; +import { SuccessStep } from "../steps/success"; +import { WebSearchStep } from "../steps/web-search"; +import { WelcomeStep } from "../steps/welcome"; +import { ProgressIndicator } from "./progress-indicator"; + +export type { OnboardingState }; +export type OnboardingStep = OnboardingState["currentStep"]; + +export interface AgentInfo { + id: string; + name: string; + onboarding_state: OnboardingState; +} + +interface WizardContentProps { + organizationId: string; + organizationName: string; + client: Client; + /** Agent with existing onboarding state (for resuming onboarding) */ + initialAgent?: AgentInfo; + /** Optional initial state for testing/stories - bypasses API calls */ + initialState?: Partial< + OnboardingState & { + agentId?: string; + agentName?: string; + deployingInitialStatus?: "summary" | "deploying" | "error"; + } + >; + /** Name for the agent to create (defaults to "blink") */ + agentName?: string; +} + +export function WizardContent({ + organizationId, + organizationName, + client, + initialAgent, + initialState, + agentName = "blink", +}: WizardContentProps) { + const router = useRouter(); + const skipPersistence = initialState !== undefined; + + // Track agent info (either passed in or created during onboarding) + const [agentInfo, setAgentInfo] = useState( + initialAgent + ); + + // State is derived from agentInfo or initialState for testing + const [state, setState] = useState(() => { + if (initialState) { + return { + currentStep: initialState.currentStep ?? "welcome", + finished: initialState.finished ?? false, + github: initialState.github, + slack: initialState.slack, + apiKeys: initialState.apiKeys, + }; + } + if (initialAgent) { + return initialAgent.onboarding_state; + } + return { currentStep: "welcome", finished: false }; + }); + + // For testing: track agentId and agentName from initialState + const [testAgentId] = useState(initialState?.agentId); + const [testAgentName] = useState(initialState?.agentName ?? agentName); + + const updateOnboardingState = useCallback( + async (updates: Partial) => { + const newState = { ...state, ...updates }; + setState(newState); + + // Persist to API unless in test mode + if (!skipPersistence && agentInfo) { + try { + await client.agents.updateOnboarding(agentInfo.id, newState); + } catch (error) { + console.error("Failed to update onboarding state:", error); + } + } + }, + [state, skipPersistence, agentInfo, client] + ); + + const goToStep = useCallback( + async (step: OnboardingStep) => { + await updateOnboardingState({ currentStep: step }); + }, + [updateOnboardingState] + ); + + const clearAndRedirect = useCallback(async () => { + // Clear onboarding state via API + if (!skipPersistence && agentInfo) { + try { + await client.agents.clearOnboarding(agentInfo.id); + } catch (error) { + console.error("Failed to clear onboarding state:", error); + } + } + const redirectName = agentInfo?.name ?? testAgentName; + router.push(`/${organizationName}/${redirectName}`); + }, [ + skipPersistence, + agentInfo, + client, + router, + organizationName, + testAgentName, + ]); + + const handleAgentCreated = useCallback((agent: AgentInfo) => { + setAgentInfo(agent); + setState(agent.onboarding_state); + }, []); + + const steps: OnboardingStep[] = [ + "welcome", + "llm-api-keys", + "github-setup", + "slack-setup", + "web-search", + "deploying", + "success", + ]; + + // Effective agent ID and name - may be undefined if agent not yet created + const effectiveAgentId = agentInfo?.id ?? testAgentId; + const effectiveAgentName = agentInfo?.name ?? testAgentName; + + return ( +
+ goToStep(step as OnboardingStep)} + /> + +
+ {state.currentStep === "welcome" && ( + goToStep("llm-api-keys")} + client={client} + organizationId={organizationId} + existingAgentId={effectiveAgentId} + onAgentCreated={handleAgentCreated} + agentName={agentName} + /> + )} + + {state.currentStep === "llm-api-keys" && ( + { + updateOnboardingState({ + apiKeys: { ...state.apiKeys, ...values }, + currentStep: "github-setup", + }); + }} + onSkip={() => goToStep("github-setup")} + onBack={() => goToStep("welcome")} + /> + )} + + {state.currentStep === "github-setup" && effectiveAgentId && ( + { + updateOnboardingState({ github, currentStep: "slack-setup" }); + }} + onSkip={() => goToStep("slack-setup")} + onBack={() => goToStep("llm-api-keys")} + /> + )} + + {state.currentStep === "slack-setup" && effectiveAgentId && ( + { + updateOnboardingState({ slack, currentStep: "web-search" }); + }} + onSkip={() => goToStep("web-search")} + onBack={() => goToStep("github-setup")} + /> + )} + + {state.currentStep === "web-search" && ( + { + updateOnboardingState({ + apiKeys: { ...state.apiKeys, exaApiKey }, + currentStep: "deploying", + }); + }} + onSkip={() => goToStep("deploying")} + onBack={() => goToStep("slack-setup")} + /> + )} + + {state.currentStep === "deploying" && effectiveAgentId && ( + { + goToStep("success"); + }} + initialStatus={initialState?.deployingInitialStatus} + /> + )} + + {state.currentStep === "success" && ( + + )} +
+
+ ); +} diff --git a/packages/site/app/(app)/[organization]/~/onboarding/steps/deploying.tsx b/packages/site/app/(app)/[organization]/~/onboarding/steps/deploying.tsx new file mode 100644 index 0000000..2cb6c0e --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/steps/deploying.tsx @@ -0,0 +1,343 @@ +"use client"; + +import type Client from "@blink.so/api"; +import { + AlertCircle, + ArrowLeft, + Check, + Github, + Key, + Loader2, + Rocket, + Search, +} from "lucide-react"; +import { useRef, useState } from "react"; +import { toast } from "sonner"; +import { SlackIcon } from "@/components/slack-icon"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import type { OnboardingStep } from "../wizard"; + +type Status = "summary" | "deploying" | "error"; + +interface DeployingStepProps { + client: Client; + organizationId: string; + agentId: string; + slack?: { + botToken: string; + signingSecret: string; + }; + apiKeys?: { + aiProvider?: "anthropic" | "openai" | "vercel"; + aiApiKey?: string; + exaApiKey?: string; + }; + github?: { + appName: string; + appUrl: string; + installUrl: string; + }; + goToStep: (step: OnboardingStep) => void; + onSuccess: (agentId: string) => void; + /** Initial status for testing/stories */ + initialStatus?: Status; +} + +const providerNames: Record = { + anthropic: "Anthropic", + openai: "OpenAI", + vercel: "Vercel AI Gateway", +}; + +export function DeployingStep({ + client, + organizationId, + agentId, + slack, + apiKeys, + github, + goToStep, + onSuccess, + initialStatus = "summary", +}: DeployingStepProps) { + const [status, setStatus] = useState(initialStatus); + const [errorMessage, setErrorMessage] = useState(null); + const hasStartedRef = useRef(false); + + const deploy = async () => { + if (hasStartedRef.current) return; + hasStartedRef.current = true; + setStatus("deploying"); + + try { + // Download the agent files + const downloadResult = await client.onboarding.downloadAgent({ + organization_id: organizationId, + }); + const fileId = downloadResult.file_id; + + // Build environment variables + const env: Array<{ key: string; value: string; secret: boolean }> = []; + + if (slack?.botToken) { + env.push({ + key: "SLACK_BOT_TOKEN", + value: slack.botToken, + secret: true, + }); + } + if (slack?.signingSecret) { + env.push({ + key: "SLACK_SIGNING_SECRET", + value: slack.signingSecret, + secret: true, + }); + } + if (apiKeys?.exaApiKey) { + env.push({ + key: "EXA_API_KEY", + value: apiKeys.exaApiKey, + secret: true, + }); + } + // Set the appropriate API key based on the selected provider + if (apiKeys?.aiApiKey && apiKeys?.aiProvider) { + const envKeyMap: Record = { + anthropic: "ANTHROPIC_API_KEY", + openai: "OPENAI_API_KEY", + vercel: "AI_GATEWAY_API_KEY", + }; + env.push({ + key: envKeyMap[apiKeys.aiProvider], + value: apiKeys.aiApiKey, + secret: true, + }); + } + + // Set environment variables on the existing agent + const envResults = await Promise.allSettled( + env.map(async (variable) => { + await client.agents.env.create({ + agent_id: agentId, + key: variable.key, + value: variable.value, + secret: variable.secret, + upsert: true, + }); + }) + ); + const errEnvResults = envResults.filter( + (result) => result.status === "rejected" + ); + if (errEnvResults.length > 0) { + throw new Error( + `Failed to set environment variables: ${errEnvResults.map((result) => result.reason).join(", ")}` + ); + } + + // Deploy the agent with the downloaded files + await client.agents.deployments.create({ + agent_id: agentId, + output_files: [{ path: "agent.js", id: fileId }], + entrypoint: "agent.js", + target: "production", + }); + + onSuccess(agentId); + } catch (error) { + setStatus("error"); + hasStartedRef.current = false; + const message = + error instanceof Error ? error.message : "Deployment failed"; + setErrorMessage(message); + toast.error(message); + } + }; + + // Configuration items for the summary + const configItems = [ + { + id: "llm" as const, + label: "LLM API Key", + configured: !!apiKeys?.aiApiKey, + value: apiKeys?.aiProvider + ? providerNames[apiKeys.aiProvider] + : undefined, + notConfiguredDescription: "The agent will not be able to respond.", + icon: Key, + step: "llm-api-keys" as OnboardingStep, + }, + { + id: "github" as const, + label: "GitHub", + configured: !!github?.appName, + value: "Connected", + notConfiguredDescription: + "The agent will not be able to access GitHub repositories.", + icon: Github, + step: "github-setup" as OnboardingStep, + }, + { + id: "slack" as const, + label: "Slack", + configured: !!slack?.botToken, + value: "Connected", + notConfiguredDescription: "The agent will not be available in Slack.", + IconComponent: SlackIcon, + step: "slack-setup" as OnboardingStep, + }, + { + id: "web-search" as const, + label: "Web Search", + configured: !!apiKeys?.exaApiKey, + value: "Exa", + notConfiguredDescription: "The agent will not be able to search the web.", + icon: Search, + step: "web-search" as OnboardingStep, + }, + ]; + + if (status === "error") { + return ( +
+ + +
+ +
+ Deployment Failed + + {errorMessage || "Something went wrong during deployment."} Please + check the server logs. + +
+ + + +
+
+ ); + } + + if (status === "deploying") { + return ( +
+ + +
+ +
+ Deploying Your Agent + + This may take a moment. Please don't close this page. + +
+ + + +
+
+ ); + } + + // Summary view + return ( +
+ + +
+ +
+ Ready to Deploy + + Review your configuration before deploying your agent. + +
+ +
+ {configItems.map((item) => { + const IconComponent = item.IconComponent; + const LucideIcon = item.icon; + return ( +
+
+ {item.configured ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ {IconComponent ? ( + + ) : LucideIcon ? ( + + ) : null} + {item.label} +
+ {!item.configured && ( + + {item.notConfiguredDescription} + + )} +
+
+
+ {item.configured ? ( + + {item.value} + + ) : ( + + Not configured + + )} + +
+
+ ); + })} +
+ + {configItems.some((item) => !item.configured) && ( +

+ You may still deploy the agent, but its functionality will be + limited. +

+ )} + +
+ + +
+
+
+
+ ); +} diff --git a/packages/site/app/(app)/[organization]/~/onboarding/steps/github-setup.tsx b/packages/site/app/(app)/[organization]/~/onboarding/steps/github-setup.tsx new file mode 100644 index 0000000..d715c46 --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/steps/github-setup.tsx @@ -0,0 +1,162 @@ +"use client"; + +import type Client from "@blink.so/api"; +import { ArrowLeft, ArrowRight, Github } from "lucide-react"; +import { useState } from "react"; +import { + GitHubSetupWizard, + type GitHubAppCredentials, +} from "@/components/github-setup-wizard"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { toast } from "sonner"; + +interface GitHubSetupStepProps { + client: Client; + agentId: string; + agentName: string; + organizationName: string; + onComplete: (result: { + appName: string; + appUrl: string; + installUrl: string; + }) => void; + onSkip: () => void; + onBack?: () => void; +} + +/** + * Save GitHub credentials as environment variables + */ +async function saveGitHubEnvVars( + client: Client, + agentId: string, + credentials: GitHubAppCredentials +): Promise { + const envVars = [ + { + key: "GITHUB_APP_ID", + value: String(credentials.appId), + secret: false, + }, + { + key: "GITHUB_CLIENT_ID", + value: credentials.clientId, + secret: false, + }, + { + key: "GITHUB_CLIENT_SECRET", + value: credentials.clientSecret, + secret: true, + }, + { + key: "GITHUB_WEBHOOK_SECRET", + value: credentials.webhookSecret, + secret: true, + }, + { + key: "GITHUB_PRIVATE_KEY", + value: credentials.privateKey, + secret: true, + }, + ]; + + await Promise.all( + envVars.map((envVar) => + client.agents.env.create({ + agent_id: agentId, + key: envVar.key, + value: envVar.value, + secret: envVar.secret, + target: ["preview", "production"], + upsert: true, + }) + ) + ); +} + +export function GitHubSetupStep({ + client, + agentId, + agentName, + organizationName, + onComplete, + onSkip, + onBack, +}: GitHubSetupStepProps) { + const [showWizard, setShowWizard] = useState(false); + + const handleWizardComplete = async (result: { + appName: string; + appUrl: string; + installUrl: string; + credentials: GitHubAppCredentials; + }) => { + try { + // Save credentials as env vars + await saveGitHubEnvVars(client, agentId, result.credentials); + + // Continue to next step + onComplete({ + appName: result.appName, + appUrl: result.appUrl, + installUrl: result.installUrl, + }); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Failed to save GitHub credentials" + ); + } + }; + + if (!showWizard) { + return ( + + +
+ +
+

Connect to GitHub

+

+ Create a GitHub App to enable PR reviews, issue responses, and + repository access. +

+ +

+ You can also set this up later in Settings > Integrations +

+
+ {onBack ? ( + + ) : ( +
+ )} + +
+ + + ); + } + + return ( + setShowWizard(false)} + onSkip={onSkip} + /> + ); +} diff --git a/packages/site/app/(app)/[organization]/~/onboarding/steps/llm-api-keys.tsx b/packages/site/app/(app)/[organization]/~/onboarding/steps/llm-api-keys.tsx new file mode 100644 index 0000000..62ff1aa --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/steps/llm-api-keys.tsx @@ -0,0 +1,256 @@ +"use client"; + +import { ArrowLeft, Check, ExternalLink, Key } from "lucide-react"; +import { useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { cn } from "@/lib/utils"; + +type AIProvider = "anthropic" | "openai" | "vercel"; + +interface LlmApiKeysStepProps { + initialValues?: { + aiProvider?: AIProvider; + aiApiKey?: string; + }; + onContinue: (values: { aiProvider?: AIProvider; aiApiKey?: string }) => void; + onSkip: () => void; + onBack: () => void; +} + +const providers: { + id: AIProvider; + name: string; + description: string; + placeholder: string; + helpUrl: string; + createKeyText: string; +}[] = [ + { + id: "anthropic", + name: "Anthropic", + description: "Claude models", + placeholder: "sk-ant-...", + helpUrl: "https://console.anthropic.com/settings/keys", + createKeyText: "Create Anthropic API Key", + }, + { + id: "openai", + name: "OpenAI", + description: "GPT models", + placeholder: "sk-...", + helpUrl: "https://platform.openai.com/api-keys", + createKeyText: "Create OpenAI API Key", + }, + { + id: "vercel", + name: "Vercel AI Gateway", + description: "Unified gateway for multiple AI providers", + placeholder: "vck_...", + helpUrl: "https://vercel.com/ai-gateway", + createKeyText: "Create Vercel AI Gateway API Key", + }, +]; + +export function LlmApiKeysStep({ + initialValues, + onContinue, + onSkip, + onBack, +}: LlmApiKeysStepProps) { + const [aiProvider, setAIProvider] = useState( + initialValues?.aiProvider + ); + const [aiApiKey, setAIApiKey] = useState(initialValues?.aiApiKey || ""); + const [hasOpenedKeyPage, setHasOpenedKeyPage] = useState(false); + + const selectedProvider = providers.find((p) => p.id === aiProvider); + + const currentStep = useMemo(() => { + if (!aiProvider) return 1; + if (!hasOpenedKeyPage) return 2; + return 3; + }, [aiProvider, hasOpenedKeyPage]); + + const handleContinue = () => { + onContinue({ + aiProvider: aiProvider, + aiApiKey: aiApiKey || undefined, + }); + }; + + const StepNumber = ({ + num, + active, + completed, + }: { + num: number; + active: boolean; + completed: boolean; + }) => ( +
+ {completed ? : num} +
+ ); + + return ( + + +
+
+ +
+ LLM API Key Setup +
+ + Configure an API key for AI capabilities. + +
+ + {/* Step 1: Select Provider */} +
+ 1} + /> +
+ +
+ {providers.map((provider) => ( + + ))} +
+
+
+ + {/* Step 2: Create API Key */} +
+ +
+

= 2 ? "" : "text-muted-foreground" + }`} + > + Create an API key +

+ +
+
+ + {/* Step 3: Enter API Key */} +
+ = 2} + completed={!!aiApiKey.trim()} + /> +
+ + setAIApiKey(e.target.value)} + disabled={!aiProvider} + data-1p-ignore + autoComplete="off" + /> +
+
+ + {/* Footer */} +
+ +
+ + +
+
+
+
+ ); +} diff --git a/packages/site/app/(app)/[organization]/~/onboarding/steps/slack-setup.tsx b/packages/site/app/(app)/[organization]/~/onboarding/steps/slack-setup.tsx new file mode 100644 index 0000000..cb8dab5 --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/steps/slack-setup.tsx @@ -0,0 +1,73 @@ +"use client"; + +import type Client from "@blink.so/api"; +import { ArrowLeft, ArrowRight } from "lucide-react"; +import { useState } from "react"; +import { SlackIcon } from "@/components/slack-icon"; +import { SlackSetupWizard } from "@/components/slack-setup-wizard"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; + +interface SlackSetupStepProps { + client: Client; + agentId: string; + agentName: string; + onComplete: (slack: { botToken: string; signingSecret: string }) => void; + onSkip: () => void; + onBack: () => void; +} + +export function SlackSetupStep({ + client, + agentId, + agentName, + onComplete, + onSkip, + onBack, +}: SlackSetupStepProps) { + const [showWizard, setShowWizard] = useState(false); + + if (!showWizard) { + return ( + + +
+ +
+

Connect to Slack

+

+ Let your agent respond to messages in Slack channels and DMs. +

+ +

+ You can also set this up later in Settings > Integrations +

+
+ + +
+
+
+ ); + } + + return ( + setShowWizard(false)} + onSkip={onSkip} + /> + ); +} diff --git a/packages/site/app/(app)/[organization]/~/onboarding/steps/success.tsx b/packages/site/app/(app)/[organization]/~/onboarding/steps/success.tsx new file mode 100644 index 0000000..a739d18 --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/steps/success.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { ArrowRight, CheckCircle2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +interface SuccessStepProps { + agentName: string; + organizationName: string; + onFinish: () => void; +} + +export function SuccessStep({ + agentName, + organizationName, + onFinish, +}: SuccessStepProps) { + return ( +
+ + +
+ +
+ Agent Deployed! + + Your agent {agentName} has been successfully + deployed and is ready to use. + +
+ +
+

Next Steps

+
    +
  • Start a chat with your agent to test it out
  • +
  • Configure additional environment variables in settings
  • +
  • Set up webhooks for GitHub and Slack integrations
  • +
+
+ + +
+
+
+ ); +} diff --git a/packages/site/app/(app)/[organization]/~/onboarding/steps/web-search.tsx b/packages/site/app/(app)/[organization]/~/onboarding/steps/web-search.tsx new file mode 100644 index 0000000..3cb2cb4 --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/steps/web-search.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { ArrowLeft, Check, ExternalLink, Info, Search } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +interface WebSearchStepProps { + initialValue?: string; + onContinue: (exaApiKey?: string) => void; + onSkip: () => void; + onBack: () => void; +} + +export function WebSearchStep({ + initialValue, + onContinue, + onSkip, + onBack, +}: WebSearchStepProps) { + const [exaApiKey, setExaApiKey] = useState(initialValue || ""); + const [hasOpenedKeyPage, setHasOpenedKeyPage] = useState(false); + + const handleContinue = () => { + onContinue(exaApiKey || undefined); + }; + + const StepNumber = ({ + num, + active, + completed, + }: { + num: number; + active: boolean; + completed: boolean; + }) => ( +
+ {completed ? : num} +
+ ); + + return ( + + +
+
+ +
+ Web Search Setup +
+ + Enable web search capabilities for your agent via Exa. + +
+ +
+ +

+ Exa is a web search provider for AI agents.{" "} + + Learn more + +

+
+ + {/* Step 1: Create API Key */} +
+ +
+

Create an Exa API key

+ +
+
+ + {/* Step 2: Enter API Key */} +
+ +
+ + setExaApiKey(e.target.value)} + data-1p-ignore + autoComplete="off" + /> +
+
+ + {/* Footer */} +
+ +
+ + +
+
+
+
+ ); +} diff --git a/packages/site/app/(app)/[organization]/~/onboarding/steps/welcome.tsx b/packages/site/app/(app)/[organization]/~/onboarding/steps/welcome.tsx new file mode 100644 index 0000000..d461bc7 --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/steps/welcome.tsx @@ -0,0 +1,141 @@ +"use client"; + +import type Client from "@blink.so/api"; +import type { OnboardingState } from "@blink.so/api"; +import { Bot, Github, Globe, Loader2, MessageSquare } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +interface AgentInfo { + id: string; + name: string; + onboarding_state: OnboardingState; +} + +interface WelcomeStepProps { + onContinue: () => void; + client: Client; + organizationId: string; + existingAgentId?: string; + onAgentCreated: (agent: AgentInfo) => void; + /** Name for the agent to create (defaults to "blink") */ + agentName?: string; +} + +export function WelcomeStep({ + onContinue, + client, + organizationId, + existingAgentId, + onAgentCreated, + agentName = "blink", +}: WelcomeStepProps) { + const [loading, setLoading] = useState(false); + + const handleGetStarted = async () => { + // If agent already exists, just continue + if (existingAgentId) { + onContinue(); + return; + } + + setLoading(true); + try { + // Create agent + const initialOnboardingState: OnboardingState = { + currentStep: "welcome", + finished: false, + }; + + const agent = await client.agents.create({ + organization_id: organizationId, + name: agentName, + onboarding_state: initialOnboardingState, + }); + + onAgentCreated({ + id: agent.id, + name: agent.name, + onboarding_state: agent.onboarding_state!, + }); + + onContinue(); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to create agent" + ); + } finally { + setLoading(false); + } + }; + + return ( + + +
+ +
+ Deploy Your First Agent + + Get started with a pre-built AI agent that includes powerful + integrations for GitHub, Slack, and web search. + +
+ +
+
+ +
+
GitHub Integration
+
+ Review PRs, respond to issues, and receive webhooks +
+
+
+
+ +
+
Slack Integration
+
+ Chat with your agent directly in Slack +
+
+
+
+ +
+
Web Search
+
+ Search the web for up-to-date information +
+
+
+
+ + +
+
+ ); +} diff --git a/packages/site/app/(app)/[organization]/~/onboarding/wizard.stories.tsx b/packages/site/app/(app)/[organization]/~/onboarding/wizard.stories.tsx new file mode 100644 index 0000000..db9b0bc --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/wizard.stories.tsx @@ -0,0 +1,422 @@ +import type { OnboardingState } from "@blink.so/api"; +import Client from "@blink.so/api"; +import type { Meta, StoryObj } from "@storybook/react"; +import { withFetch } from "@/.storybook/utils"; +import { OnboardingWizard } from "./wizard"; + +// Extended state type for stories that includes agentId and agentName for testing +type StoryOnboardingState = Partial< + OnboardingState & { + agentId?: string; + agentName?: string; + deployingInitialStatus?: "summary" | "deploying" | "error"; + } +>; + +const TEST_ORGANIZATION_ID = "org-123"; +const TEST_ORGANIZATION_NAME = "test-org"; +const TEST_AGENT_ID = "agent-456"; +const TEST_FILE_ID = "file-789"; +const TEST_WEBHOOK_URL = "https://api.blink.so/webhooks/slack/test-webhook-id"; +const TEST_GITHUB_WEBHOOK_URL = + "https://api.blink.so/api/webhook/test-id/github"; +const TEST_GITHUB_SESSION_ID = "github-session-123"; +const TEST_GITHUB_MANIFEST_URL = + "https://github.com/settings/apps/new?manifest=..."; + +// Create a mock client for stories +const mockClient = new Client({ baseURL: "http://localhost:6006" }); + +// Track state across mocked API calls +const mockState = { + pollCount: 0, + githubPollCount: 0, +}; + +// Create comprehensive fetch mock for all onboarding API calls +const createMockFetchDecorator = (options?: { hangDeployment?: boolean }) => { + return withFetch((url, init) => { + const { hangDeployment = false } = options || {}; + const method = init?.method || "GET"; + + // POST /api/onboarding/download-agent + if ( + url.pathname.includes("/onboarding/download-agent") && + method === "POST" + ) { + return new Response(JSON.stringify({ file_id: TEST_FILE_ID }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + // POST /api/agents (create agent) + if (url.pathname === "/api/agents" && method === "POST") { + return new Response( + JSON.stringify({ + id: TEST_AGENT_ID, + name: "blink", + description: + "AI agent with GitHub, Slack, and web search integrations", + onboarding_state: { currentStep: "welcome" }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // POST /api/onboarding/validate-credentials + if ( + url.pathname.includes("/onboarding/validate-credentials") && + method === "POST" + ) { + return new Response(JSON.stringify({ valid: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + // GET /api/agents/{agentId}/setup/github/webhook-url + if ( + url.pathname.includes("/setup/github/webhook-url") && + method === "GET" + ) { + return new Response( + JSON.stringify({ webhook_url: TEST_GITHUB_WEBHOOK_URL }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // POST /api/agents/{agentId}/setup/github/start-creation + if ( + url.pathname.includes("/setup/github/start-creation") && + method === "POST" + ) { + mockState.githubPollCount = 0; + return new Response( + JSON.stringify({ + manifest_url: TEST_GITHUB_MANIFEST_URL, + session_id: TEST_GITHUB_SESSION_ID, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // GET /api/agents/{agentId}/setup/github/creation-status/{sessionId} + if ( + url.pathname.includes("/setup/github/creation-status") && + method === "GET" + ) { + mockState.githubPollCount++; + const completed = mockState.githubPollCount >= 3; + return new Response( + JSON.stringify({ + status: completed ? "completed" : "pending", + app_data: completed + ? { + id: 12345, + name: "Scout", + html_url: "https://github.com/apps/scout", + slug: "scout", + } + : undefined, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // POST /api/agents/{agentId}/setup/github/complete-creation + if ( + url.pathname.includes("/setup/github/complete-creation") && + method === "POST" + ) { + return new Response( + JSON.stringify({ + success: true, + app_name: "Scout", + app_url: "https://github.com/apps/scout", + install_url: "https://github.com/apps/scout/installations/new", + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // POST /api/agents/{agentId}/setup/github/cancel-creation + if ( + url.pathname.includes("/setup/github/cancel-creation") && + method === "POST" + ) { + return new Response(null, { status: 204 }); + } + + // GET /api/agents/{agentId}/setup/slack/webhook-url + if (url.pathname.includes("/setup/slack/webhook-url") && method === "GET") { + return new Response(JSON.stringify({ webhook_url: TEST_WEBHOOK_URL }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + // POST /api/agents/{agentId}/setup/slack/start-verification + if ( + url.pathname.includes("/setup/slack/start-verification") && + method === "POST" + ) { + mockState.pollCount = 0; + return new Response(JSON.stringify({ webhook_url: TEST_WEBHOOK_URL }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + // GET /api/agents/{agentId}/setup/slack/verification-status + if ( + url.pathname.includes("/setup/slack/verification-status") && + method === "GET" + ) { + mockState.pollCount++; + const dmReceived = mockState.pollCount >= 3; + return new Response( + JSON.stringify({ + active: true, + started_at: new Date().toISOString(), + last_event_at: + mockState.pollCount > 1 ? new Date().toISOString() : undefined, + dm_received: dmReceived, + dm_channel: dmReceived ? "D12345678" : undefined, + signature_failed: false, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // POST /api/agents/{agentId}/setup/slack/complete-verification + if ( + url.pathname.includes("/setup/slack/complete-verification") && + method === "POST" + ) { + return new Response( + JSON.stringify({ success: true, bot_name: "Scout Bot" }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // POST /api/agents/{agentId}/setup/slack/cancel-verification + if ( + url.pathname.includes("/setup/slack/cancel-verification") && + method === "POST" + ) { + return new Response(null, { status: 204 }); + } + + // POST /api/agents/{agentId}/env (create env variable) + if (url.pathname.includes("/env") && method === "POST") { + return new Response( + JSON.stringify({ id: "env-123", key: "TEST_KEY", secret: false }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // POST /api/agents/{agentId}/deployments (create deployment) + if (url.pathname.includes("/deployments") && method === "POST") { + if (hangDeployment) { + // Return a promise that never resolves to keep the deploying step visible + return new Promise(() => {}); + } + return new Response( + JSON.stringify({ id: "deployment-123", status: "success" }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + return undefined; + }); +}; + +const meta: Meta = { + title: "Onboarding/OnboardingWizard", + component: OnboardingWizard, + parameters: { + layout: "fullscreen", + nextjs: { + appDirectory: true, + navigation: { + push: () => {}, + }, + }, + }, + args: { + organizationId: TEST_ORGANIZATION_ID, + organizationName: TEST_ORGANIZATION_NAME, + }, + render: (args) => , + decorators: [ + createMockFetchDecorator(), + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +// Base state for steps that need an existing agent +const withAgentState: StoryOnboardingState = { + agentName: "blink", + agentId: TEST_AGENT_ID, +}; + +// ============================================================================= +// FULL FLOW +// ============================================================================= + +export const FullFlow: Story = {}; +FullFlow.storyName = "Full Flow (from Welcome)"; + +// ============================================================================= +// INDIVIDUAL STEPS +// ============================================================================= + +export const Step1_Welcome: Story = { + args: { + initialState: { + currentStep: "welcome", + }, + }, +}; +Step1_Welcome.storyName = "Step 1: Welcome"; + +export const Step2_LlmApiKeys: Story = { + args: { + initialState: { + ...withAgentState, + currentStep: "llm-api-keys", + }, + }, +}; +Step2_LlmApiKeys.storyName = "Step 2: LLM API Keys"; + +export const Step3_GitHubSetup: Story = { + args: { + initialState: { + ...withAgentState, + currentStep: "github-setup", + }, + }, +}; +Step3_GitHubSetup.storyName = "Step 3: GitHub Setup"; + +export const Step4_SlackSetup: Story = { + args: { + initialState: { + ...withAgentState, + currentStep: "slack-setup", + }, + }, +}; +Step4_SlackSetup.storyName = "Step 4: Slack Setup"; + +export const Step5_WebSearch: Story = { + args: { + initialState: { + ...withAgentState, + currentStep: "web-search", + }, + }, +}; +Step5_WebSearch.storyName = "Step 5: Web Search"; + +export const Step6_Summary_Empty: Story = { + args: { + initialState: { + ...withAgentState, + currentStep: "deploying", + }, + }, +}; +Step6_Summary_Empty.storyName = "Step 6: Summary (Nothing Configured)"; + +export const Step6_Summary_AllConfigured: Story = { + args: { + initialState: { + ...withAgentState, + currentStep: "deploying", + apiKeys: { + aiProvider: "anthropic", + aiApiKey: "sk-ant-xxx", + exaApiKey: "exa-xxx", + }, + github: { + appName: "Scout", + appUrl: "https://github.com/apps/scout", + installUrl: "https://github.com/apps/scout/installations/new", + }, + slack: { + botToken: "xoxb-xxx", + signingSecret: "xxx", + }, + }, + }, +}; +Step6_Summary_AllConfigured.storyName = "Step 6: Summary (All Configured)"; + +export const Step6_Summary_Partial: Story = { + args: { + initialState: { + ...withAgentState, + currentStep: "deploying", + apiKeys: { + aiProvider: "openai", + aiApiKey: "sk-xxx", + }, + github: { + appName: "MyApp", + appUrl: "https://github.com/apps/myapp", + installUrl: "https://github.com/apps/myapp/installations/new", + }, + }, + }, +}; +Step6_Summary_Partial.storyName = "Step 6: Summary (Partial Config)"; + +export const Step6_Deploying: Story = { + args: { + initialState: { + ...withAgentState, + currentStep: "deploying", + deployingInitialStatus: "deploying", + }, + }, + decorators: [ + createMockFetchDecorator({ hangDeployment: true }), + (Story) => ( +
+ +
+ ), + ], +}; +Step6_Deploying.storyName = "Step 6: Deploying (In Progress)"; + +export const Step6_DeployError: Story = { + args: { + initialState: { + ...withAgentState, + currentStep: "deploying", + deployingInitialStatus: "error", + }, + }, +}; +Step6_DeployError.storyName = "Step 6: Deploy Error"; + +export const Step7_Success: Story = { + args: { + initialState: { + ...withAgentState, + currentStep: "success", + }, + }, +}; +Step7_Success.storyName = "Step 7: Success"; diff --git a/packages/site/app/(app)/[organization]/~/onboarding/wizard.tsx b/packages/site/app/(app)/[organization]/~/onboarding/wizard.tsx new file mode 100644 index 0000000..a344349 --- /dev/null +++ b/packages/site/app/(app)/[organization]/~/onboarding/wizard.tsx @@ -0,0 +1,77 @@ +"use client"; + +import type Client from "@blink.so/api"; +import type { OnboardingState } from "@blink.so/api"; +import { useAPIClient } from "@/lib/api-client"; +import { type AgentInfo, WizardContent } from "./components/wizard-content"; + +export type { OnboardingState }; +export type OnboardingStep = OnboardingState["currentStep"]; + +/** + * OnboardingWizard with client injection support for testing. + * When client is provided, useAPIClient() is not called. + */ +export function OnboardingWizard({ + organizationId, + organizationName, + agent, + client, + initialState, +}: { + organizationId: string; + organizationName: string; + /** Agent with existing onboarding state (for resuming onboarding) */ + agent?: AgentInfo; + /** Optional client for testing/stories */ + client?: Client; + /** Optional initial state for testing/stories - bypasses API calls */ + initialState?: Partial< + OnboardingState & { + agentId?: string; + agentName?: string; + deployingInitialStatus?: "summary" | "deploying" | "error"; + } + >; +}) { + if (client) { + return ( + + ); + } + + return ( + + ); +} + +function OnboardingWizardWithHook({ + organizationId, + organizationName, + agent, +}: { + organizationId: string; + organizationName: string; + agent?: AgentInfo; +}) { + const client = useAPIClient(); + + return ( + + ); +} diff --git a/packages/site/components/github-setup-wizard.stories.tsx b/packages/site/components/github-setup-wizard.stories.tsx new file mode 100644 index 0000000..5168060 --- /dev/null +++ b/packages/site/components/github-setup-wizard.stories.tsx @@ -0,0 +1,256 @@ +import Client from "@blink.so/api"; +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "storybook/test"; +import { withFetch } from "@/.storybook/utils"; +import { + GitHubSetupWizard, + type GitHubSetupWizardInitialState, +} from "./github-setup-wizard"; + +const TEST_AGENT_ID = "test-agent-123"; +const TEST_GITHUB_URL = "https://github.com/settings/apps/new"; +const TEST_MANIFEST = JSON.stringify({ + name: "Test App", + url: "https://blink.so", +}); +const TEST_SESSION_ID = "test-session-456"; + +const mockClient = new Client(); + +const createMockFetchDecorator = (options?: { + creationStatus?: + | "pending" + | "app_created" + | "completed" + | "failed" + | "expired"; + completeSuccess?: boolean; + appData?: { + id: number; + name: string; + html_url: string; + slug: string; + }; +}) => { + const { + creationStatus = "pending", + completeSuccess = true, + appData = { + id: 12345, + name: "Test GitHub App", + html_url: "https://github.com/apps/test-github-app", + slug: "test-github-app", + }, + } = options ?? {}; + + return withFetch((url, init) => { + // POST /api/agents/{agentId}/setup/github/start-creation + if ( + url.pathname.includes("/setup/github/start-creation") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ + manifest: TEST_MANIFEST, + github_url: TEST_GITHUB_URL, + session_id: TEST_SESSION_ID, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // GET /api/agents/{agentId}/setup/github/creation-status/{sessionId} + if ( + url.pathname.includes("/setup/github/creation-status") && + (!init?.method || init.method === "GET") + ) { + return new Response( + JSON.stringify({ + status: creationStatus, + app_data: + creationStatus === "completed" || creationStatus === "app_created" + ? appData + : undefined, + credentials: + creationStatus === "completed" + ? { + app_id: appData.id, + client_id: "Iv1.test123", + client_secret: "test-client-secret", + webhook_secret: "test-webhook-secret", + private_key: btoa("test-private-key"), + } + : undefined, + error: + creationStatus === "failed" ? "Something went wrong" : undefined, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // POST /api/agents/{agentId}/setup/github/complete-creation + if ( + url.pathname.includes("/setup/github/complete-creation") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ + success: completeSuccess, + app_name: appData.name, + app_url: appData.html_url, + install_url: `${appData.html_url}/installations/new`, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // POST /api/agents/{agentId}/setup/github/cancel-creation + if ( + url.pathname.includes("/setup/github/cancel-creation") && + init?.method === "POST" + ) { + return new Response(null, { status: 204 }); + } + + return undefined; + }); +}; + +const meta: Meta = { + title: "Components/GitHubSetupWizard", + component: GitHubSetupWizard, + parameters: { + layout: "centered", + }, + args: { + agentId: TEST_AGENT_ID, + agentName: "Scout", + organizationName: "my-org", + onComplete: fn(), + onBack: fn(), + onSkip: fn(), + }, + render: (args) => ( +
+ +
+ ), + decorators: [createMockFetchDecorator()], +}; + +export default meta; +type Story = StoryObj; + +const withInitialState = (state: GitHubSetupWizardInitialState): Story => ({ + args: { + initialState: state, + }, +}); + +// ============================================================================= +// Step 1 & 2: Organization & Create Button +// ============================================================================= + +export const Initial: Story = withInitialState({}); +Initial.storyName = "Initial"; + +export const WithOrganization: Story = withInitialState({ + organization: "my-github-org", +}); +WithOrganization.storyName = "With Organization"; + +// ============================================================================= +// Step 3: Waiting / Completed / Failed +// ============================================================================= + +export const WaitingForAppCreation: Story = withInitialState({ + hasOpenedGitHub: true, + sessionId: TEST_SESSION_ID, + manifestData: { manifest: TEST_MANIFEST, github_url: TEST_GITHUB_URL }, + creationStatus: "pending", +}); +WaitingForAppCreation.storyName = "Waiting for App Creation"; + +export const WaitingForInstallation: Story = { + ...withInitialState({ + hasOpenedGitHub: true, + sessionId: TEST_SESSION_ID, + manifestData: { manifest: TEST_MANIFEST, github_url: TEST_GITHUB_URL }, + creationStatus: "app_created", + appData: { + id: 12345, + name: "my-org-Scout", + html_url: "https://github.com/apps/my-org-scout", + slug: "my-org-scout", + }, + }), + decorators: [createMockFetchDecorator({ creationStatus: "app_created" })], +}; +WaitingForInstallation.storyName = "Waiting for Installation"; + +export const Completed: Story = { + ...withInitialState({ + hasOpenedGitHub: true, + sessionId: TEST_SESSION_ID, + manifestData: { manifest: TEST_MANIFEST, github_url: TEST_GITHUB_URL }, + creationStatus: "completed", + appData: { + id: 12345, + name: "my-org-Scout", + html_url: "https://github.com/apps/my-org-scout", + slug: "my-org-scout", + }, + }), + decorators: [createMockFetchDecorator({ creationStatus: "completed" })], +}; +Completed.storyName = "Completed"; + +export const Failed: Story = { + ...withInitialState({ + hasOpenedGitHub: true, + sessionId: TEST_SESSION_ID, + manifestData: { manifest: TEST_MANIFEST, github_url: TEST_GITHUB_URL }, + creationStatus: "failed", + error: "GitHub API error: 422 Unprocessable Entity", + }), + decorators: [createMockFetchDecorator({ creationStatus: "failed" })], +}; +Failed.storyName = "Failed"; + +export const Expired: Story = { + ...withInitialState({ + hasOpenedGitHub: true, + sessionId: TEST_SESSION_ID, + manifestData: { manifest: TEST_MANIFEST, github_url: TEST_GITHUB_URL }, + creationStatus: "expired", + }), + decorators: [createMockFetchDecorator({ creationStatus: "expired" })], +}; +Expired.storyName = "Expired"; + +export const Completing: Story = { + ...withInitialState({ + hasOpenedGitHub: true, + sessionId: TEST_SESSION_ID, + manifestData: { manifest: TEST_MANIFEST, github_url: TEST_GITHUB_URL }, + creationStatus: "completed", + appData: { + id: 12345, + name: "my-org-Scout", + html_url: "https://github.com/apps/my-org-scout", + slug: "my-org-scout", + }, + completing: true, + }), + decorators: [createMockFetchDecorator({ creationStatus: "completed" })], +}; +Completing.storyName = "Completing"; diff --git a/packages/site/components/github-setup-wizard.tsx b/packages/site/components/github-setup-wizard.tsx new file mode 100644 index 0000000..faac5ba --- /dev/null +++ b/packages/site/components/github-setup-wizard.tsx @@ -0,0 +1,550 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import type Client from "@blink.so/api"; +import { + AlertCircle, + ArrowLeft, + Check, + ExternalLink, + Loader2, +} from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { toast } from "sonner"; + +export interface GitHubSetupWizardInitialState { + organization?: string; + sessionId?: string; + hasOpenedGitHub?: boolean; + manifestData?: { + manifest: string; + github_url: string; + }; + creationStatus?: + | "pending" + | "app_created" + | "completed" + | "failed" + | "expired"; + appData?: { + id: number; + name: string; + html_url: string; + slug: string; + }; + completing?: boolean; + error?: string; +} + +export interface GitHubAppCredentials { + appId: number; + clientId: string; + clientSecret: string; + webhookSecret: string; + privateKey: string; // base64-encoded PEM +} + +interface GitHubSetupWizardProps { + client: Client; + agentId: string; + agentName: string; + organizationName: string; + onComplete: (result: { + appName: string; + appUrl: string; + installUrl: string; + credentials: GitHubAppCredentials; + }) => void; + onBack?: () => void; + onSkip?: () => void; + initialState?: GitHubSetupWizardInitialState; +} + +export function GitHubSetupWizard({ + client, + agentId, + agentName, + organizationName, + onComplete, + onBack, + onSkip, + initialState, +}: GitHubSetupWizardProps) { + // Default app name is organizationName-agentName (user can change on GitHub) + const appName = `${organizationName}-${agentName}`; + + // Form state + const [githubOrganization, setGithubOrganization] = useState( + initialState?.organization ?? "" + ); + + // Session state + const [sessionId, setSessionId] = useState( + initialState?.sessionId ?? null + ); + const [manifestData, setManifestData] = useState<{ + manifest: string; + github_url: string; + } | null>(initialState?.manifestData ?? null); + const [hasOpenedGitHub, setHasOpenedGitHub] = useState( + initialState?.hasOpenedGitHub ?? false + ); + + // Creation status + const [creationStatus, setCreationStatus] = useState< + "pending" | "app_created" | "completed" | "failed" | "expired" | null + >(initialState?.creationStatus ?? null); + const [appData, setAppData] = useState<{ + id: number; + name: string; + html_url: string; + slug: string; + } | null>(initialState?.appData ?? null); + const [credentials, setCredentials] = useState( + null + ); + const [error, setError] = useState( + initialState?.error ?? null + ); + + // Completion state + const [completing, setCompleting] = useState( + initialState?.completing ?? false + ); + const [starting, setStarting] = useState(false); + + const pollingRef = useRef(null); + + // Determine current step + // Step 1 is completed only when user has clicked "Create GitHub App" (step 2 button) + const currentStep = useMemo(() => { + if (!hasOpenedGitHub) return 2; + if (creationStatus === "pending") return 3; + if (creationStatus === "app_created") return 3; // Still waiting for installation + if (creationStatus === "completed") return 4; + return 2; + }, [hasOpenedGitHub, creationStatus]); + + // Submit manifest to GitHub via form POST (opens in new tab) + const submitManifestForm = useCallback( + (githubUrl: string, manifest: string) => { + const form = document.createElement("form"); + form.method = "POST"; + form.action = githubUrl; + form.target = "_blank"; + + const input = document.createElement("input"); + input.type = "hidden"; + input.name = "manifest"; + input.value = manifest; + + form.appendChild(input); + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); + }, + [] + ); + + // Track the organization used for the current session + const [sessionOrganization, setSessionOrganization] = useState( + null + ); + + // Start creation flow and open GitHub in a new tab via form submission + const startCreation = useCallback(async () => { + if (starting) return; + setStarting(true); + setError(null); + + const orgToUse = githubOrganization.trim() || undefined; + + try { + const result = await client.agents.setupGitHub.startCreation(agentId, { + name: appName, + organization: orgToUse, + }); + setSessionId(result.session_id); + setSessionOrganization(githubOrganization); + setManifestData({ + manifest: result.manifest, + github_url: result.github_url, + }); + setCreationStatus("pending"); + + // Submit form to GitHub - this opens in a new tab and POSTs the manifest + submitManifestForm(result.github_url, result.manifest); + setHasOpenedGitHub(true); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to start creation" + ); + setError( + error instanceof Error ? error.message : "Failed to start creation" + ); + } finally { + setStarting(false); + } + }, [ + client, + agentId, + appName, + githubOrganization, + starting, + submitManifestForm, + ]); + + // Poll for creation status + const pollCreationStatus = useCallback(async () => { + if (!sessionId) return null; + + try { + const status = await client.agents.setupGitHub.getCreationStatus( + agentId, + sessionId + ); + setCreationStatus(status.status); + if (status.app_data) { + setAppData(status.app_data); + } + // Store credentials when status is completed + if (status.credentials) { + setCredentials({ + appId: status.credentials.app_id, + clientId: status.credentials.client_id, + clientSecret: status.credentials.client_secret, + webhookSecret: status.credentials.webhook_secret, + privateKey: status.credentials.private_key, + }); + } + if (status.error) { + setError(status.error); + } + return status; + } catch (error) { + console.error("Failed to poll creation status:", error); + return null; + } + }, [client, agentId, sessionId]); + + // Start polling when in pending or app_created state + useEffect(() => { + if ( + (creationStatus === "pending" || creationStatus === "app_created") && + sessionId + ) { + const poll = async () => { + const status = await pollCreationStatus(); + if ( + status?.status === "completed" || + status?.status === "failed" || + status?.status === "expired" + ) { + if (pollingRef.current) { + clearInterval(pollingRef.current); + } + } + }; + poll(); + pollingRef.current = setInterval(poll, 2000); + + return () => { + if (pollingRef.current) { + clearInterval(pollingRef.current); + } + }; + } + }, [creationStatus, sessionId, pollCreationStatus]); + + // Complete setup + const completeSetup = useCallback(async () => { + if (!sessionId || !credentials || !appData) return; + setCompleting(true); + + try { + // Call completeCreation to clear the server-side setup state + const result = await client.agents.setupGitHub.completeCreation(agentId, { + session_id: sessionId, + }); + + if (result.success) { + // Pass credentials to onComplete so the caller can save them as env vars + onComplete({ + appName: appData.name, + appUrl: appData.html_url, + installUrl: `${appData.html_url}/installations/new`, + credentials, + }); + } else { + toast.error("Failed to complete setup"); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to complete setup" + ); + } finally { + setCompleting(false); + } + }, [client, agentId, sessionId, credentials, appData, onComplete]); + + // Reset to try again + const handleRetry = () => { + setSessionId(null); + setSessionOrganization(null); + setManifestData(null); + setHasOpenedGitHub(false); + setCreationStatus(null); + setAppData(null); + setCredentials(null); + setError(null); + }; + + // Step indicator component + const StepNumber = ({ + num, + active, + completed, + }: { + num: number; + active: boolean; + completed: boolean; + }) => ( +
+ {completed ? : num} +
+ ); + + // GitHub icon + const GitHubIcon = ({ className }: { className?: string }) => ( + + + + ); + + return ( + + +
+
+ +
+ GitHub App Setup +
+ + Create a GitHub App to connect your agent to GitHub repositories. + +
+ + {/* Step 1: Organization (optional) - completed when Create button is clicked */} +
+ +
+ + setGithubOrganization(e.target.value)} + disabled={creationStatus === "completed"} + /> +

+ Enter a GitHub organization name to create the app under, or leave + blank for a personal app. +

+
+
+ + {/* Step 2: Create GitHub App */} +
+ 2} + /> +
+

= 2 ? "" : "text-muted-foreground" + }`} + > + Create and install the GitHub App +

+

+ Click the button to open GitHub. You'll create the app and + then install it on your repositories. +

+ +
+
+ + {/* Step 3: Waiting for app creation */} + {creationStatus === "pending" && ( +
+ +
+

Creating GitHub App...

+
+ + Complete the app creation on GitHub +
+
+
+ )} + + {/* Step 3: Waiting for installation */} + {creationStatus === "app_created" && ( +
+ +
+

Installing GitHub App...

+
+ + + App created! Now install it on your repositories and return + here + +
+
+
+ )} + + {/* Step 3/4: Error state */} + {(creationStatus === "failed" || creationStatus === "expired") && ( +
+
+ +
+
+

+ {creationStatus === "expired" + ? "Session expired" + : "Creation failed"} +

+ {error && ( +

{error}

+ )} + +
+
+ )} + + {/* Step 3: Success */} + {creationStatus === "completed" && appData && ( +
+ +
+
+

+ GitHub App created and installed! +

+

+ Click Continue below to proceed. +

+
+ +
+
+ )} + + {/* Actions */} +
+
+ {onBack && ( + + )} +
+
+ {onSkip && ( + + )} + +
+
+
+
+ ); +} diff --git a/packages/site/components/llm-api-keys-setup.tsx b/packages/site/components/llm-api-keys-setup.tsx new file mode 100644 index 0000000..064fb92 --- /dev/null +++ b/packages/site/components/llm-api-keys-setup.tsx @@ -0,0 +1,276 @@ +"use client"; + +import { Check, ExternalLink, Key } from "lucide-react"; +import { useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { cn } from "@/lib/utils"; + +export type AIProvider = "anthropic" | "openai" | "vercel"; + +export interface LlmApiKeysResult { + provider: AIProvider; + apiKey: string; +} + +export interface LlmApiKeysSetupProps { + initialValues?: { + provider?: AIProvider; + apiKey?: string; + }; + onComplete: (result: LlmApiKeysResult) => void; + onCancel?: () => void; + completing?: boolean; +} + +const providers: { + id: AIProvider; + name: string; + description: string; + placeholder: string; + helpUrl: string; + createKeyText: string; + envVarKey: string; +}[] = [ + { + id: "anthropic", + name: "Anthropic", + description: "Claude models", + placeholder: "sk-ant-...", + helpUrl: "https://console.anthropic.com/settings/keys", + createKeyText: "Create Anthropic API Key", + envVarKey: "ANTHROPIC_API_KEY", + }, + { + id: "openai", + name: "OpenAI", + description: "GPT models", + placeholder: "sk-...", + helpUrl: "https://platform.openai.com/api-keys", + createKeyText: "Create OpenAI API Key", + envVarKey: "OPENAI_API_KEY", + }, + { + id: "vercel", + name: "Vercel AI Gateway", + description: "Unified gateway for multiple AI providers", + placeholder: "vck_...", + helpUrl: "https://vercel.com/ai-gateway", + createKeyText: "Create Vercel AI Gateway API Key", + envVarKey: "VERCEL_AI_GATEWAY_API_KEY", + }, +]; + +export function getEnvVarKeyForProvider(provider: AIProvider): string { + const p = providers.find((p) => p.id === provider); + return p?.envVarKey || "AI_API_KEY"; +} + +function StepNumber({ + num, + active, + completed, +}: { + num: number; + active: boolean; + completed: boolean; +}) { + return ( +
+ {completed ? : num} +
+ ); +} + +export function LlmApiKeysSetup({ + initialValues, + onComplete, + onCancel, + completing, +}: LlmApiKeysSetupProps) { + const [aiProvider, setAIProvider] = useState( + initialValues?.provider + ); + const [aiApiKey, setAIApiKey] = useState(initialValues?.apiKey || ""); + const [hasOpenedKeyPage, setHasOpenedKeyPage] = useState(false); + + const selectedProvider = providers.find((p) => p.id === aiProvider); + + const currentStep = useMemo(() => { + if (!aiProvider) return 1; + if (!hasOpenedKeyPage) return 2; + return 3; + }, [aiProvider, hasOpenedKeyPage]); + + const handleComplete = () => { + if (aiProvider && aiApiKey.trim()) { + onComplete({ + provider: aiProvider, + apiKey: aiApiKey.trim(), + }); + } + }; + + const canComplete = aiProvider && aiApiKey.trim(); + + return ( + + +
+
+ +
+ LLM API Key Setup +
+ + Configure an API key for AI capabilities. + +
+ + {/* Step 1: Select Provider */} +
+ 1} + /> +
+ +
+ {providers.map((provider) => ( + + ))} +
+
+
+ + {/* Step 2: Create API Key */} +
+ +
+

= 2 ? "" : "text-muted-foreground" + }`} + > + Create an API key +

+ +
+
+ + {/* Step 3: Enter API Key */} +
+ = 2} + completed={!!aiApiKey.trim()} + /> +
+ + setAIApiKey(e.target.value)} + disabled={!aiProvider || completing} + data-1p-ignore + autoComplete="off" + /> +
+
+ + {/* Footer */} +
+ {onCancel && ( + + )} + +
+
+
+ ); +} diff --git a/packages/site/components/slack-icon.tsx b/packages/site/components/slack-icon.tsx new file mode 100644 index 0000000..0beecf6 --- /dev/null +++ b/packages/site/components/slack-icon.tsx @@ -0,0 +1,14 @@ +export function SlackIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/packages/site/components/slack-setup-wizard.stories.tsx b/packages/site/components/slack-setup-wizard.stories.tsx new file mode 100644 index 0000000..54734d6 --- /dev/null +++ b/packages/site/components/slack-setup-wizard.stories.tsx @@ -0,0 +1,584 @@ +import Client from "@blink.so/api"; +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "storybook/test"; +import { useState } from "react"; +import { withFetch } from "@/.storybook/utils"; +import { + SlackSetupWizard, + type SlackSetupWizardInitialState, +} from "./slack-setup-wizard"; + +const TEST_AGENT_ID = "test-agent-123"; +const TEST_WEBHOOK_URL = "https://api.blink.so/webhooks/slack/test-webhook-id"; + +// Create a client that will have its fetch calls intercepted by withFetch +const mockClient = new Client(); + +// Create a mock API response helper +const createMockFetchDecorator = (options?: { + validationValid?: boolean; + validationError?: string; + dmReceived?: boolean; + signatureFailed?: boolean; + completeSuccess?: boolean; +}) => { + const { + validationValid = true, + validationError, + dmReceived = false, + signatureFailed = false, + completeSuccess = true, + } = options ?? {}; + + return withFetch((url, init) => { + // GET /api/agents/{agentId}/setup/slack/webhook-url + if ( + url.pathname.includes("/setup/slack/webhook-url") && + (!init?.method || init.method === "GET") + ) { + return new Response( + JSON.stringify({ + webhook_url: TEST_WEBHOOK_URL, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // POST /api/agents/{agentId}/setup/slack/start-verification + if ( + url.pathname.includes("/setup/slack/start-verification") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ + webhook_url: TEST_WEBHOOK_URL, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // GET /api/agents/{agentId}/setup/slack/verification-status + if ( + url.pathname.includes("/setup/slack/verification-status") && + (!init?.method || init.method === "GET") + ) { + return new Response( + JSON.stringify({ + active: true, + started_at: new Date().toISOString(), + last_event_at: dmReceived ? new Date().toISOString() : undefined, + dm_received: dmReceived, + dm_channel: dmReceived ? "D12345678" : undefined, + signature_failed: signatureFailed, + signature_failed_at: signatureFailed + ? new Date().toISOString() + : undefined, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // POST /api/onboarding/validate-credentials + if ( + url.pathname.includes("/onboarding/validate-credentials") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ + valid: validationValid, + error: validationError, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // POST /api/agents/{agentId}/setup/slack/complete-verification + if ( + url.pathname.includes("/setup/slack/complete-verification") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ + success: completeSuccess, + bot_name: "Test Bot", + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // POST /api/agents/{agentId}/setup/slack/cancel-verification + if ( + url.pathname.includes("/setup/slack/cancel-verification") && + init?.method === "POST" + ) { + return new Response(null, { status: 204 }); + } + + return undefined; + }); +}; + +const meta: Meta = { + title: "Components/SlackSetupWizard", + component: SlackSetupWizard, + parameters: { + layout: "centered", + }, + args: { + agentId: TEST_AGENT_ID, + agentName: "Scout", + onComplete: fn(), + onCancel: fn(), + onBack: fn(), + onSkip: fn(), + }, + render: (args) => ( +
+ +
+ ), + decorators: [createMockFetchDecorator()], +}; + +export default meta; +type Story = StoryObj; + +// Helper to create stories with specific initial state +const withInitialState = (state: SlackSetupWizardInitialState): Story => ({ + args: { + initialState: state, + }, +}); + +// ============================================================================= +// STEP 1: App Name +// ============================================================================= + +export const Step1_AppName: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "", +}); +Step1_AppName.storyName = "Step 1: App Name (empty)"; + +export const Step1_AppNameFilled: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", +}); +Step1_AppNameFilled.storyName = "Step 1: App Name (filled)"; + +// ============================================================================= +// STEP 2: Create Slack App +// ============================================================================= + +export const Step2_LoadingWebhookUrl: Story = withInitialState({ + webhookUrl: null, + loadingWebhookUrl: true, + appName: "Scout", +}); +Step2_LoadingWebhookUrl.storyName = "Step 2: Loading Webhook URL"; + +export const Step2_CreateSlackApp: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: false, +}); +Step2_CreateSlackApp.storyName = "Step 2: Create Slack App"; + +export const Step2_SlackOpened: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, +}); +Step2_SlackOpened.storyName = "Step 2: Slack Opened (can open again)"; + +// ============================================================================= +// STEP 3: App ID +// ============================================================================= + +export const Step3_AppId: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, + appId: "", +}); +Step3_AppId.storyName = "Step 3: App ID (empty)"; + +export const Step3_AppIdFilled: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, + appId: "A0123456789", +}); +Step3_AppIdFilled.storyName = "Step 3: App ID (filled)"; + +// ============================================================================= +// STEP 4: Signing Secret +// ============================================================================= + +export const Step4_SigningSecret: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, + appId: "A0123456789", + signingSecret: "", +}); +Step4_SigningSecret.storyName = "Step 4: Signing Secret (empty)"; + +export const Step4_SigningSecretFilled: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, + appId: "A0123456789", + signingSecret: "abc123secret", +}); +Step4_SigningSecretFilled.storyName = "Step 4: Signing Secret (filled)"; + +// ============================================================================= +// STEP 5: Bot Token +// ============================================================================= + +export const Step5_BotToken: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, + appId: "A0123456789", + signingSecret: "abc123secret", + botToken: "", +}); +Step5_BotToken.storyName = "Step 5: Bot Token (empty)"; + +export const Step5_BotTokenFilled: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, + appId: "A0123456789", + signingSecret: "abc123secret", + botToken: "xoxb-123456789-abcdefghijklmnop", +}); +Step5_BotTokenFilled.storyName = "Step 5: Bot Token (filled, not validated)"; + +export const Step5_BotTokenValidating: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, + appId: "A0123456789", + signingSecret: "abc123secret", + botToken: "xoxb-123456789-abcdefghijklmnop", + validatingToken: true, +}); +Step5_BotTokenValidating.storyName = "Step 5: Bot Token (validating)"; + +export const Step5_BotTokenValidated: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, + appId: "A0123456789", + signingSecret: "abc123secret", + botToken: "xoxb-123456789-abcdefghijklmnop", + tokenValidated: true, +}); +Step5_BotTokenValidated.storyName = "Step 5: Bot Token (validated)"; + +// ============================================================================= +// STEP 6: DM Verification +// ============================================================================= + +export const Step6_WaitingForDM: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, + appId: "A0123456789", + signingSecret: "abc123secret", + botToken: "xoxb-123456789-abcdefghijklmnop", + tokenValidated: true, + verificationStarted: true, + dmReceived: false, +}); +Step6_WaitingForDM.storyName = "Step 6: Waiting for DM"; + +export const Step6_DMReceived: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, + appId: "A0123456789", + signingSecret: "abc123secret", + botToken: "xoxb-123456789-abcdefghijklmnop", + tokenValidated: true, + verificationStarted: true, + dmReceived: true, +}); +Step6_DMReceived.storyName = "Step 6: DM Received (ready to complete)"; + +export const Step6_Completing: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, + appId: "A0123456789", + signingSecret: "abc123secret", + botToken: "xoxb-123456789-abcdefghijklmnop", + tokenValidated: true, + verificationStarted: true, + dmReceived: true, + completing: true, +}); +Step6_Completing.storyName = "Step 6: Completing Setup"; + +export const Step6_SigningSecretError: Story = withInitialState({ + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + hasOpenedSlack: true, + appId: "A0123456789", + signingSecret: "wrong-secret", + signingSecretError: true, + botToken: "xoxb-123456789-abcdefghijklmnop", + tokenValidated: true, + verificationStarted: true, + dmReceived: true, +}); +Step6_SigningSecretError.storyName = "Step 6: Signing Secret Error"; + +// ============================================================================= +// OTHER VARIATIONS +// ============================================================================= + +export const WithoutBackButton: Story = { + args: { + onBack: undefined, + initialState: { + webhookUrl: TEST_WEBHOOK_URL, + loadingWebhookUrl: false, + appName: "Scout", + }, + }, +}; +WithoutBackButton.storyName = "Without Back Button"; + +// Global settings that the fetch mock can read +const interactiveSettings = { + botTokenValid: true, + signingSecretValid: true, + pollCount: 0, +}; + +// Interactive wrapper component with controls +function InteractiveFlowWrapper() { + const [botTokenValid, setBotTokenValid] = useState(true); + const [signingSecretValid, setSigningSecretValid] = useState(true); + const [key, setKey] = useState(0); + + // Update global settings when state changes + interactiveSettings.botTokenValid = botTokenValid; + interactiveSettings.signingSecretValid = signingSecretValid; + + const resetWizard = () => { + interactiveSettings.pollCount = 0; + setKey((k) => k + 1); + }; + + return ( +
+
+ +
+
+

Test Controls

+ +
+ + + +
+ +
+ + + +

+ Toggle the checkboxes to simulate different API responses. The wizard + will use these settings for validation and verification. +

+
+
+ ); +} + +// Interactive story that simulates the full flow with controls +export const InteractiveFlow: Story = { + render: () => , + decorators: [ + withFetch((url, init) => { + // Get webhook URL + if ( + url.pathname.includes("/setup/slack/webhook-url") && + (!init?.method || init.method === "GET") + ) { + return new Response( + JSON.stringify({ + webhook_url: TEST_WEBHOOK_URL, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Start verification + if ( + url.pathname.includes("/setup/slack/start-verification") && + init?.method === "POST" + ) { + interactiveSettings.pollCount = 0; // Reset poll count when starting verification + return new Response( + JSON.stringify({ + webhook_url: TEST_WEBHOOK_URL, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Verification status - simulate DM being received after 3 polls + if ( + url.pathname.includes("/setup/slack/verification-status") && + (!init?.method || init.method === "GET") + ) { + interactiveSettings.pollCount++; + const dmReceived = interactiveSettings.pollCount >= 3; + const signatureFailed = + dmReceived && !interactiveSettings.signingSecretValid; + return new Response( + JSON.stringify({ + active: true, + started_at: new Date().toISOString(), + last_event_at: + interactiveSettings.pollCount > 1 + ? new Date().toISOString() + : undefined, + dm_received: dmReceived, + dm_channel: dmReceived ? "D12345678" : undefined, + signature_failed: signatureFailed, + signature_failed_at: signatureFailed + ? new Date().toISOString() + : undefined, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Validate credentials (bot token) + if ( + url.pathname.includes("/onboarding/validate-credentials") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ + valid: interactiveSettings.botTokenValid, + error: interactiveSettings.botTokenValid + ? undefined + : "Invalid bot token", + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Complete verification + if ( + url.pathname.includes("/setup/slack/complete-verification") && + init?.method === "POST" + ) { + return new Response( + JSON.stringify({ + success: true, + bot_name: "Scout Bot", + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Cancel verification + if ( + url.pathname.includes("/setup/slack/cancel-verification") && + init?.method === "POST" + ) { + return new Response(null, { status: 204 }); + } + + return undefined; + }), + ], +}; +InteractiveFlow.storyName = "Interactive Flow"; diff --git a/packages/site/components/slack-setup-wizard.tsx b/packages/site/components/slack-setup-wizard.tsx new file mode 100644 index 0000000..5b91964 --- /dev/null +++ b/packages/site/components/slack-setup-wizard.tsx @@ -0,0 +1,721 @@ +"use client"; + +import type Client from "@blink.so/api"; +import { + AlertCircle, + ArrowLeft, + Check, + ExternalLink, + Loader2, +} from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { toast } from "sonner"; +import { SlackIcon } from "@/components/slack-icon"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + createAgentSlackManifest, + createSlackAppUrl, +} from "@/lib/slack-manifest"; + +export interface SlackSetupWizardInitialState { + webhookUrl?: string | null; + loadingWebhookUrl?: boolean; + appName?: string; + hasOpenedSlack?: boolean; + appId?: string; + signingSecret?: string; + botToken?: string; + validatingToken?: boolean; + tokenValidated?: boolean; + botTokenError?: boolean; + signingSecretError?: boolean; + verificationStarted?: boolean; + dmReceived?: boolean; + completing?: boolean; +} + +interface SlackSetupWizardProps { + client: Client; + agentId: string; + agentName: string; + onComplete: (credentials: { + botToken: string; + signingSecret: string; + }) => void; + onCancel: () => void; + onBack?: () => void; + onSkip?: () => void; + /** Initial state for stories/testing - allows rendering in specific states */ + initialState?: SlackSetupWizardInitialState; +} + +export function SlackSetupWizard({ + client, + agentId, + agentName, + onComplete, + onCancel, + onBack, + onSkip, + initialState, +}: SlackSetupWizardProps) { + // Webhook URL state (fetched from backend unless provided in initialState) + const [webhookUrl, setWebhookUrl] = useState( + initialState?.webhookUrl ?? null + ); + const [loadingWebhookUrl, setLoadingWebhookUrl] = useState( + initialState?.loadingWebhookUrl ?? initialState?.webhookUrl === undefined + ); + + // Form state + const [appName, setAppName] = useState(initialState?.appName ?? agentName); + const [hasOpenedSlack, setHasOpenedSlack] = useState( + initialState?.hasOpenedSlack ?? false + ); + const [appId, setAppId] = useState(initialState?.appId ?? ""); + const [signingSecret, setSigningSecret] = useState( + initialState?.signingSecret ?? "" + ); + const [botToken, setBotToken] = useState(initialState?.botToken ?? ""); + + // Validation state + const [validatingToken, setValidatingToken] = useState( + initialState?.validatingToken ?? false + ); + const [tokenValidated, setTokenValidated] = useState( + initialState?.tokenValidated ?? false + ); + const [botTokenError, setBotTokenError] = useState( + initialState?.botTokenError ?? false + ); + const [signingSecretError, setSigningSecretError] = useState( + initialState?.signingSecretError ?? false + ); + + // Verification state + const [startingVerification, setStartingVerification] = useState(false); + const [verificationStarted, setVerificationStarted] = useState( + initialState?.verificationStarted ?? false + ); + const [verificationStatus, setVerificationStatus] = useState<{ + active: boolean; + lastEventAt?: string; + dmReceived: boolean; + signatureFailed: boolean; + }>({ + active: initialState?.verificationStarted ?? false, + dmReceived: initialState?.dmReceived ?? false, + signatureFailed: false, + }); + const [completing, setCompleting] = useState( + initialState?.completing ?? false + ); + const pollingRef = useRef(null); + const autoValidationTriggeredRef = useRef(false); + + // Fetch webhook URL on mount (skip if provided in initialState) + useEffect(() => { + if (initialState?.webhookUrl !== undefined) return; + + async function fetchWebhookUrl() { + try { + const result = await client.agents.setupSlack.getWebhookUrl(agentId); + setWebhookUrl(result.webhook_url); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to load webhook URL" + ); + } finally { + setLoadingWebhookUrl(false); + } + } + fetchWebhookUrl(); + }, [client, agentId, initialState?.webhookUrl]); + + // Determine which step is active (1-indexed for display) + const currentStep = useMemo(() => { + if (!appName.trim()) return 1; + if (!hasOpenedSlack) return 2; + if (!appId.trim()) return 3; + if (!signingSecret.trim()) return 4; + if (!tokenValidated) return 5; + return 6; + }, [appName, hasOpenedSlack, appId, signingSecret, tokenValidated]); + + // Generate manifest URL client-side + const manifestUrl = useMemo(() => { + if (!webhookUrl) return null; + const manifest = createAgentSlackManifest(appName, webhookUrl); + return createSlackAppUrl(manifest); + }, [appName, webhookUrl]); + + // Generate install URL from app ID + const installUrl = useMemo(() => { + if (!appId.trim()) return ""; + return `https://api.slack.com/apps/${appId}/install-on-team`; + }, [appId]); + + // Generate app home page URL (for signing secret) + const appHomeUrl = useMemo(() => { + if (!appId.trim()) return ""; + return `https://api.slack.com/apps/${appId}`; + }, [appId]); + + // Validate bot token on blur + const validateBotToken = useCallback(async () => { + if (!botToken.trim()) return; + + setValidatingToken(true); + setBotTokenError(false); + try { + const result = await client.onboarding.validateCredentials({ + type: "slack", + credentials: { botToken }, + }); + + if (result.valid) { + setTokenValidated(true); + setBotTokenError(false); + } else { + toast.error(result.error || "Invalid bot token"); + setTokenValidated(false); + setBotTokenError(true); + } + } catch (error) { + toast.error(error instanceof Error ? error.message : "Validation failed"); + setTokenValidated(false); + setBotTokenError(true); + } finally { + setValidatingToken(false); + } + }, [client, botToken]); + + // Auto-validate when bot token reaches 8+ characters + useEffect(() => { + if (botToken.length < 8) { + autoValidationTriggeredRef.current = false; + return; + } + if ( + !autoValidationTriggeredRef.current && + !validatingToken && + !tokenValidated + ) { + autoValidationTriggeredRef.current = true; + validateBotToken(); + } + }, [botToken, validatingToken, tokenValidated, validateBotToken]); + + // Start verification (called when step 6 becomes active) + const startVerification = useCallback(async () => { + if (verificationStarted || startingVerification) return; + + setStartingVerification(true); + try { + await client.agents.setupSlack.startVerification(agentId, { + signing_secret: signingSecret, + bot_token: botToken, + }); + setVerificationStarted(true); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to start verification" + ); + } finally { + setStartingVerification(false); + } + }, [ + client, + agentId, + signingSecret, + botToken, + verificationStarted, + startingVerification, + ]); + + // Poll for verification status + const pollVerificationStatus = useCallback(async () => { + try { + const status = + await client.agents.setupSlack.getVerificationStatus(agentId); + setVerificationStatus({ + active: status.active, + lastEventAt: status.last_event_at, + dmReceived: status.dm_received, + signatureFailed: status.signature_failed, + }); + return status; + } catch (error) { + console.error("Failed to poll verification status:", error); + return null; + } + }, [client, agentId]); + + // Start verification when step 6 is reached (not if there's a signing secret error) + useEffect(() => { + if ( + currentStep === 6 && + tokenValidated && + !verificationStarted && + !signingSecretError + ) { + startVerification(); + } + }, [ + currentStep, + tokenValidated, + verificationStarted, + signingSecretError, + startVerification, + ]); + + // Start polling when verification is active + useEffect(() => { + if (verificationStarted && !verificationStatus.dmReceived) { + const poll = async () => { + const status = await pollVerificationStatus(); + if (status?.dm_received) { + if (pollingRef.current) { + clearInterval(pollingRef.current); + } + } + // Detect signature failure + if (status?.signature_failed) { + if (pollingRef.current) { + clearInterval(pollingRef.current); + } + setSigningSecretError(true); + setVerificationStarted(false); + toast.error( + "Invalid signing secret. Please check and re-enter your signing secret." + ); + } + }; + poll(); + pollingRef.current = setInterval(poll, 2000); + + return () => { + if (pollingRef.current) { + clearInterval(pollingRef.current); + } + }; + } + }, [ + verificationStarted, + verificationStatus.dmReceived, + pollVerificationStatus, + ]); + + // Complete setup + const completeSetup = async () => { + setCompleting(true); + try { + const result = await client.agents.setupSlack.completeVerification( + agentId, + { + bot_token: botToken, + signing_secret: signingSecret, + } + ); + + if (result.success) { + onComplete({ botToken, signingSecret }); + } else { + toast.error("Failed to complete setup"); + } + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to complete setup" + ); + } finally { + setCompleting(false); + } + }; + + // Cancel verification + const handleCancel = async () => { + try { + await client.agents.setupSlack.cancelVerification(agentId); + } catch { + // Ignore errors when canceling + } + onCancel(); + }; + + // Step indicator component + const StepNumber = ({ + num, + active, + completed, + }: { + num: number; + active: boolean; + completed: boolean; + }) => ( +
+ {completed ? : num} +
+ ); + + return ( + + +
+
+ +
+ Slack App Setup +
+ + Connect your agent to Slack in a few steps. + +
+ + {/* Step 1: App Name */} +
+ +
+ + setAppName(e.target.value)} + /> +
+
+ + {/* Step 2: Create Slack App */} + + + {/* Step 3: App ID */} +
+ 3} + /> +
+ + setAppId(e.target.value)} + disabled={currentStep < 3} + data-1p-ignore + autoComplete="off" + /> +
+
+ + {/* Step 4: Signing Secret */} +
+ {signingSecretError ? ( +
+ +
+ ) : ( + 4} + /> + )} +
+ + { + setSigningSecret(e.target.value); + if (signingSecretError) { + // Clear error and reset verification when user fixes signing secret + setSigningSecretError(false); + setVerificationStarted(false); + setVerificationStatus({ + active: false, + dmReceived: false, + signatureFailed: false, + }); + } + }} + disabled={currentStep < 4} + data-1p-ignore + autoComplete="off" + className={ + signingSecretError + ? "border-yellow-500 focus-visible:ring-yellow-500" + : "" + } + /> + {signingSecretError && ( +

+ Signing secret verification failed. Did you enter it correctly? +

+ )} +
+
+ + {/* Step 5: Bot Token */} +
+ {botTokenError ? ( +
+ +
+ ) : ( + + )} +
+ +
+ { + setBotToken(e.target.value); + setTokenValidated(false); + setBotTokenError(false); + }} + onBlur={validateBotToken} + disabled={currentStep < 5} + data-1p-ignore + autoComplete="off" + className={`${tokenValidated ? "pr-10" : ""} ${botTokenError ? "border-yellow-500 focus-visible:ring-yellow-500" : ""}`} + /> + {validatingToken && ( +
+ +
+ )} + {tokenValidated && !validatingToken && ( +
+ +
+ )} +
+ {botTokenError && ( +

+ Bot token validation failed. Did you enter it correctly? +

+ )} +
+
+ + {/* Step 6: DM Verification */} +
+ +
+

= 6 ? "" : "text-muted-foreground" + }`} + > + DM the app on Slack to verify the connection +

+ {currentStep === 6 && ( +
+ {/* DM Status */} +
+ {verificationStatus.dmReceived && !signingSecretError ? ( + + ) : ( + + )} + + {verificationStatus.dmReceived && !signingSecretError + ? "Message received!" + : signingSecretError + ? "Message received..." + : "Waiting for message..."} + +
+ + {/* Subtitle */} +

+ {signingSecretError + ? `There was a problem with the signing secret. Please fix it and DM ${appName} again.` + : `Find "${appName}" in the Slack search bar and DM it.`} +

+
+ )} +
+
+ + {/* Footer */} +
+ {onBack ? ( + + ) : ( + + )} +
+ {onSkip && ( + + )} + +
+
+
+
+ ); +} diff --git a/packages/site/components/web-search-setup.tsx b/packages/site/components/web-search-setup.tsx new file mode 100644 index 0000000..5118e55 --- /dev/null +++ b/packages/site/components/web-search-setup.tsx @@ -0,0 +1,160 @@ +"use client"; + +import { Check, ExternalLink, Info, Search } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export interface WebSearchResult { + exaApiKey: string; +} + +export interface WebSearchSetupProps { + initialValue?: string; + onComplete: (result: WebSearchResult) => void; + onCancel?: () => void; + completing?: boolean; +} + +export const EXA_ENV_VAR_KEY = "EXA_API_KEY"; + +function StepNumber({ + num, + active, + completed, +}: { + num: number; + active: boolean; + completed: boolean; +}) { + return ( +
+ {completed ? : num} +
+ ); +} + +export function WebSearchSetup({ + initialValue, + onComplete, + onCancel, + completing, +}: WebSearchSetupProps) { + const [exaApiKey, setExaApiKey] = useState(initialValue || ""); + const [hasOpenedKeyPage, setHasOpenedKeyPage] = useState(false); + + const handleComplete = () => { + if (exaApiKey.trim()) { + onComplete({ exaApiKey: exaApiKey.trim() }); + } + }; + + return ( + + +
+
+ +
+ Web Search Setup +
+ + Enable web search capabilities for your agent via Exa. + +
+ +
+ +

+ Exa is a web search provider for AI agents.{" "} + + Learn more + +

+
+ + {/* Step 1: Create API Key */} +
+ +
+

Create an Exa API key

+ +
+
+ + {/* Step 2: Enter API Key */} +
+ +
+ + setExaApiKey(e.target.value)} + disabled={completing} + data-1p-ignore + autoComplete="off" + /> +
+
+ + {/* Footer */} +
+ {onCancel && ( + + )} + +
+
+
+ ); +} diff --git a/packages/site/lib/api-client.ts b/packages/site/lib/api-client.ts index 91f2236..a1f1c90 100644 --- a/packages/site/lib/api-client.ts +++ b/packages/site/lib/api-client.ts @@ -7,7 +7,8 @@ export function useAPIClient() { return useMemo( () => new Client({ - baseURL: typeof window !== "undefined" ? window.location.origin : "", + baseURL: + typeof window !== "undefined" ? window.location.origin : undefined, }), [] ); diff --git a/packages/site/lib/slack-manifest.ts b/packages/site/lib/slack-manifest.ts new file mode 100644 index 0000000..b81e4fa --- /dev/null +++ b/packages/site/lib/slack-manifest.ts @@ -0,0 +1,107 @@ +/** + * Creates a Slack manifest URL for app creation. + */ + +export interface SlackManifest { + display_information: { + name: string; + description?: string; + }; + features?: { + bot_user?: { + display_name: string; + always_online?: boolean; + }; + app_home?: { + home_tab_enabled?: boolean; + messages_tab_enabled?: boolean; + messages_tab_read_only_enabled?: boolean; + }; + }; + oauth_config: { + scopes: { + bot?: string[]; + user?: string[]; + }; + }; + settings?: { + event_subscriptions?: { + request_url: string; + bot_events?: string[]; + }; + interactivity?: { + is_enabled: boolean; + request_url: string; + }; + org_deploy_enabled?: boolean; + socket_mode_enabled?: boolean; + token_rotation_enabled?: boolean; + }; +} + +/** + * Creates a URL to initialize Slack App creation with a manifest. + * @param manifest The Slack App manifest configuration + * @returns URL to create the Slack app with the provided manifest + */ +export function createSlackAppUrl(manifest: SlackManifest): string { + const manifestJson = encodeURIComponent(JSON.stringify(manifest)); + return `https://api.slack.com/apps?new_app=1&manifest_json=${manifestJson}`; +} + +/** + * Creates a default Slack manifest for a Blink agent. + * @param appName The display name for the Slack app + * @param webhookUrl The webhook URL for event subscriptions + * @returns A Slack manifest configured for the agent + */ +export function createAgentSlackManifest( + appName: string, + webhookUrl: string +): SlackManifest { + return { + display_information: { + name: appName, + description: `Chat with ${appName}`, + }, + features: { + bot_user: { + display_name: appName, + always_online: true, + }, + app_home: { + home_tab_enabled: false, + messages_tab_enabled: true, + messages_tab_read_only_enabled: false, + }, + }, + oauth_config: { + scopes: { + bot: [ + "assistant:write", + "chat:write", + "im:history", + "im:read", + "im:write", + ], + }, + }, + settings: { + event_subscriptions: { + request_url: webhookUrl, + bot_events: [ + "assistant_thread_context_changed", + "assistant_thread_started", + "message.im", + ], + }, + interactivity: { + is_enabled: true, + request_url: webhookUrl, + }, + org_deploy_enabled: false, + socket_mode_enabled: false, + token_rotation_enabled: false, + }, + }; +} diff --git a/packages/site/next-env.d.ts b/packages/site/next-env.d.ts index 830fb59..1b3be08 100644 --- a/packages/site/next-env.d.ts +++ b/packages/site/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.