diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml new file mode 100644 index 00000000..42f44dd7 --- /dev/null +++ b/.github/workflows/ai-review.yml @@ -0,0 +1,37 @@ +name: AI Code Reviewer + +on: + pull_request: + types: [opened, synchronize] + +permissions: + pull-requests: write + contents: read + +jobs: + review: + name: Gemini AI Review + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run AI Review + env: + # Use user's PAT to count towards their GitHub contribution graph, fallback to standard bot token + GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} + AGENT_API_KEY: ${{ secrets.AGENT_API_KEY }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: bun run scripts/ai-review.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8d05af2..f7c5ec45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -347,9 +347,11 @@ jobs: DATABASE_DRIVER: postgres - name: Start server and run perf check run: | - bun run dev & + HOST=127.0.0.1 PORT=4321 node ./dist/server/entry.mjs & SERVER_PID=$! - sleep 8 + sleep 5 + curl -s http://127.0.0.1:4321/api/health > /dev/null || true + curl -s http://127.0.0.1:4321/api/metrics > /dev/null || true bun run perf:baseline kill $SERVER_PID 2>/dev/null || true env: @@ -360,6 +362,7 @@ jobs: INTERNAL_HOOK_SECRET: "ci-perf-hook" CSRF_SKIP_DEV: "true" RATE_LIMIT_SKIP_DEV: "true" + RATE_LIMIT_ENABLED: "false" SKIP_REDIS_CHECK: "1" PERF_BASE_URL: http://127.0.0.1:4321 PERF_PATHS: /api/health,/api/metrics @@ -460,6 +463,9 @@ jobs: runs-on: ubuntu-latest needs: [build, e2e] if: github.event_name == 'push' && github.ref == 'refs/heads/main' + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 @@ -500,7 +506,7 @@ jobs: quality-gate: name: Quality Gate runs-on: ubuntu-latest - needs: [lint, docs-parity, security, test, e2e, container-scan, sast, perf, build] + needs: [lint, docs-parity, security, test, e2e, container-scan, sast, performance, build] if: always() steps: - name: Check all gates @@ -515,13 +521,13 @@ jobs: echo "Docs Parity: ${{ needs.docs-parity.result }}" echo "Security Gates: ${{ needs.security.result }}" - echo "Perf Gate: ${{ needs.perf.result }}" + echo "Perf Gate: ${{ needs.performance.result }}" if [[ "${{ needs.lint.result }}" != "success" ]] || \ [[ "${{ needs.docs-parity.result }}" != "success" ]] || \ [[ "${{ needs.security.result }}" != "success" ]] || \ [[ "${{ needs.test.result }}" != "success" ]] || \ - [[ "${{ needs.perf.result }}" != "success" ]] || \ + [[ "${{ needs.performance.result }}" != "success" ]] || \ [[ "${{ needs.build.result }}" != "success" ]]; then echo "::error::Quality gate FAILED — required checks did not pass" exit 1 diff --git a/bun.lock b/bun.lock index 5cb35682..00ac56d6 100644 --- a/bun.lock +++ b/bun.lock @@ -16,7 +16,9 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@google/genai": "^2.11.0", "@libsql/client": "^0.15.15", + "@mistralai/mistralai": "^2.4.1", "@monaco-editor/react": "^4.6.0", "@opentelemetry/auto-instrumentations-node": "^0.76.0", "@opentelemetry/exporter-trace-otlp-http": "^0.218.0", @@ -60,6 +62,7 @@ "cobe": "^0.6.5", "date-fns": "^4.1.0", "dockerode": "^4.0.12", + "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", "framer-motion": "^11.18.2", "github-markdown-css": "^5.8.1", @@ -468,6 +471,8 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@google/genai": ["@google/genai@2.11.0", "https://registry.npmmirror.com/@google/genai/-/genai-2.11.0.tgz", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-d2Csf29vS0GfHc52H0MG25ccY4FKvvbDgqDlEovLrPLF8sPegWr/GGO+LMOy85/1SnX0iV0zDAW7R8SsvWg8Vg=="], + "@graphql-tools/executor": ["@graphql-tools/executor@1.5.3", "https://registry.npmmirror.com/@graphql-tools/executor/-/executor-1.5.3.tgz", { "dependencies": { "@graphql-tools/utils": "^11.1.0", "@graphql-typed-document-node/core": "^3.2.0", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mgBFC0bsrZPZLu9EnydpMnAuQ8Iiq0CEbUcsmvXsm2/iYektGHDN/+bmb7hicA6dWZtdPfklYJmr21WD0GnOfA=="], "@graphql-tools/merge": ["@graphql-tools/merge@9.1.9", "https://registry.npmmirror.com/@graphql-tools/merge/-/merge-9.1.9.tgz", { "dependencies": { "@graphql-tools/utils": "^11.1.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iHUWNjRHeQRYdgIMIuChThOwoKzA9vrzYeslgfBo5eUYEyHGZCoDPjAavssoYXLwstYt1dZj2J22jSzc2DrN0Q=="], @@ -584,6 +589,8 @@ "@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.3", "https://registry.npmmirror.com/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg=="], + "@mistralai/mistralai": ["@mistralai/mistralai@2.4.1", "https://registry.npmmirror.com/@mistralai/mistralai/-/mistralai-2.4.1.tgz", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-vNpR2GR76aW7fK8Dcu1NRx3fdSPO1QruRDo4GbfXfkro2mvp194xIAvJ2BP4ZykzyWtJoDmBe+Qsk9D443aqlg=="], + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "https://registry.npmmirror.com/@monaco-editor/loader/-/loader-1.7.0.tgz", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "https://registry.npmmirror.com/@monaco-editor/react/-/react-4.7.0.tgz", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], @@ -1166,6 +1173,8 @@ "@types/react-dom": ["@types/react-dom@18.3.7", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], + "@types/retry": ["@types/retry@0.12.0", "https://registry.npmmirror.com/@types/retry/-/retry-0.12.0.tgz", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], + "@types/ssh2": ["@types/ssh2@1.15.5", "https://registry.npmmirror.com/@types/ssh2/-/ssh2-1.15.5.tgz", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], "@types/stats.js": ["@types/stats.js@0.17.4", "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.4.tgz", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], @@ -1368,6 +1377,8 @@ "buffer": ["buffer@5.6.0", "https://registry.npmmirror.com/buffer/-/buffer-5.6.0.tgz", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "buffer-from": ["buffer-from@1.1.2", "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "buildcheck": ["buildcheck@0.0.7", "https://registry.npmmirror.com/buildcheck/-/buildcheck-0.0.7.tgz", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="], @@ -1590,6 +1601,8 @@ "dot-prop": ["dot-prop@8.0.2", "https://registry.npmmirror.com/dot-prop/-/dot-prop-8.0.2.tgz", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="], + "dotenv": ["dotenv@17.4.2", "https://registry.npmmirror.com/dotenv/-/dotenv-17.4.2.tgz", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + "drizzle-kit": ["drizzle-kit@0.31.10", "https://registry.npmmirror.com/drizzle-kit/-/drizzle-kit-0.31.10.tgz", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], "drizzle-orm": ["drizzle-orm@0.45.2", "https://registry.npmmirror.com/drizzle-orm/-/drizzle-orm-0.45.2.tgz", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], @@ -1602,6 +1615,8 @@ "eastasianwidth": ["eastasianwidth@0.2.0", "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "ee-first": ["ee-first@1.1.1", "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "electron-to-chromium": ["electron-to-chromium@1.5.366", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.366.tgz", {}, "sha512-OlRuhb688YTCzzU3gXPLn6nGyd+F+53INE1qaKKlu6kETErE8FYsyDh0XqXEU+uBRn0MpCzz2vfNwORhkap8qg=="], @@ -1770,6 +1785,8 @@ "globe.gl": ["globe.gl@2.46.1", "https://registry.npmmirror.com/globe.gl/-/globe.gl-2.46.1.tgz", { "dependencies": { "@tweenjs/tween.js": "18 - 25", "accessor-fn": "1", "kapsule": "^1.16", "three": ">=0.179 <1", "three-globe": "^2.45", "three-render-objects": "^1.41" } }, "sha512-h+OvX52EBIPLtM0/2JkM+JZ9gPAhPJ4y3+hxUwD5Ey/O0Zk2ockuTiJ71bZbnNBGmNiIZzA5Vr3TMT0b3d35IQ=="], + "google-auth-library": ["google-auth-library@10.9.0", "https://registry.npmmirror.com/google-auth-library/-/google-auth-library-10.9.0.tgz", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg=="], + "google-logging-utils": ["google-logging-utils@1.1.3", "https://registry.npmmirror.com/google-logging-utils/-/google-logging-utils-1.1.3.tgz", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], "gopd": ["gopd@1.2.0", "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -1952,6 +1969,10 @@ "jsonfile": ["jsonfile@4.0.0", "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + "jwa": ["jwa@2.0.1", "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "kapsule": ["kapsule@1.16.3", "https://registry.npmmirror.com/kapsule/-/kapsule-1.16.3.tgz", { "dependencies": { "lodash-es": "4" } }, "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg=="], "keyv": ["keyv@5.6.0", "https://registry.npmmirror.com/keyv/-/keyv-5.6.0.tgz", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="], @@ -2260,6 +2281,8 @@ "p-queue": ["p-queue@8.1.1", "https://registry.npmmirror.com/p-queue/-/p-queue-8.1.1.tgz", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="], + "p-retry": ["p-retry@4.6.2", "https://registry.npmmirror.com/p-retry/-/p-retry-4.6.2.tgz", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + "p-timeout": ["p-timeout@6.1.4", "https://registry.npmmirror.com/p-timeout/-/p-timeout-6.1.4.tgz", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], "p-try": ["p-try@2.2.0", "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], @@ -2504,7 +2527,7 @@ "retext-stringify": ["retext-stringify@4.0.0", "https://registry.npmmirror.com/retext-stringify/-/retext-stringify-4.0.0.tgz", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], - "retry": ["retry@0.12.0", "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "retry": ["retry@0.13.1", "https://registry.npmmirror.com/retry/-/retry-0.13.1.tgz", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], "reusify": ["reusify@1.1.0", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], @@ -3114,6 +3137,8 @@ "prebuild-install/detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "promise-retry/retry": ["retry@0.12.0", "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "prompts/kleur": ["kleur@3.0.3", "https://registry.npmmirror.com/kleur/-/kleur-3.0.3.tgz", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "qrcode/yargs": ["yargs@15.4.1", "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], diff --git a/features.md b/features.md index 79d3de0b..b42a4404 100644 --- a/features.md +++ b/features.md @@ -5,7 +5,7 @@ Canonical source: `doc/feature_audit.md` ## Recently completed - Local free-model AI support (`local` provider, Ollama/OpenAI-compatible) with configurable `localBaseUrl` and key. -- External-agent (CodeRabbit-like) callback and async AI review flow documented and supported. +- External-agent (Greptile-like) callback and async AI review flow documented and supported. - PR dependency APIs: - `GET /api/repos/{owner}/{repo}/pulls/dependencies` - `POST /api/repos/{owner}/{repo}/pulls/stack-order` diff --git a/package.json b/package.json index 27e70420..be0270fc 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,9 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@google/genai": "^2.11.0", "@libsql/client": "^0.15.15", + "@mistralai/mistralai": "^2.4.1", "@monaco-editor/react": "^4.6.0", "@opentelemetry/auto-instrumentations-node": "^0.76.0", "@opentelemetry/exporter-trace-otlp-http": "^0.218.0", @@ -99,6 +101,7 @@ "cobe": "^0.6.5", "date-fns": "^4.1.0", "dockerode": "^4.0.12", + "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", "framer-motion": "^11.18.2", "github-markdown-css": "^5.8.1", @@ -137,6 +140,7 @@ "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", + "shiki": "^1.29.0", "simple-git": "^3.36.0", "sonner": "^2.0.7", "ssh2": "^1.15.0", @@ -145,7 +149,6 @@ "three": "^0.182.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", - "shiki": "^1.29.0", "yaml": "^2.8.0", "zod": "^3.23.8", "zustand": "^4.5.4" diff --git a/scripts/ai-review.ts b/scripts/ai-review.ts new file mode 100644 index 00000000..1bc851be --- /dev/null +++ b/scripts/ai-review.ts @@ -0,0 +1,172 @@ +import { execSync } from 'child_process'; +import { Mistral } from '@mistralai/mistralai'; +import { GoogleGenAI } from '@google/genai'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const GEMINI_API_KEY = process.env.GEMINI_KEY || process.env.GEMINI_API_KEY; +const AGENT_API_KEY = process.env.AGENT_API_KEY; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const PR_NUMBER = process.env.PR_NUMBER; +const REPO = process.env.REPO; +const BASE_SHA = process.env.BASE_SHA; +const HEAD_SHA = process.env.HEAD_SHA; + +if (!GEMINI_API_KEY && !AGENT_API_KEY) { + console.log("No AI provider API key found (GEMINI_KEY or AGENT_API_KEY). Skipping AI review."); + process.exit(0); +} + +if (!GITHUB_TOKEN) { + console.log("GITHUB_TOKEN is not set. Skipping AI review."); + process.exit(0); +} + +async function run() { + try { + // 1. Get the diff + console.log(`Getting diff between ${BASE_SHA} and ${HEAD_SHA}`); + const diff = execSync(`git diff ${BASE_SHA} ${HEAD_SHA}`).toString(); + + if (!diff.trim()) { + console.log("No diff found. Skipping review."); + return; + } + + if (diff.length > 50000) { + console.log("Diff is too large for AI review (max 50,000 characters). Skipping."); + return; + } + + const prompt = `You are a senior software engineer conducting a code review on a pull request. +Review the following git diff and provide constructive, human-like, and professional feedback. +Focus on identifying logic errors, security issues, performance bottlenecks, and best practice violations. +If the code looks perfect, say so in a friendly way. + +Important constraints: +- Do NOT output any internal reasoning, thoughts, or tags. +- Output ONLY the final review in markdown format. +- Be concise but thorough. + +Diff: +\`\`\`diff +${diff} +\`\`\` +`; + + let reviewComment = ""; + + if (AGENT_API_KEY) { + console.log("Requesting review from Mistral Agent..."); + const client = new Mistral({ apiKey: AGENT_API_KEY }); + const messages = [{ "role": "user" as const, "content": prompt }]; + + const response = await client.beta.conversations.start({ + agentId: 'ag_019f4ae0eb2e764aaa3f1dec318fc748', + agentVersion: 0, + inputs: messages as any, + }); + + reviewComment = (response as any)?.choices?.[0]?.message?.content || (response as any)?.message?.content || ""; + + if (!reviewComment) { + console.log("Could not find standard content in response. Fallback to full response."); + process.exit(1); + } + reviewComment += "\n\n— *AI Code Review (Powered by Mistral Agent)*"; + + } else if (GEMINI_API_KEY) { + console.log("Requesting review from Gemini 3.5 Flash..."); + const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY }); + const tools: any[] = [{ type: 'google_search' }]; + + // Fixed config: Using camelCase and nested thinkingConfig + const generationConfig = { + maxOutputTokens: 65536, + thinkingConfig: { thinkingBudget: 8192 }, + }; + + const interaction = await ai.interactions.create({ + model: 'models/gemini-3.5-flash', + input: prompt, + tools: tools, + generation_config: generationConfig as any, // the sdk still expects the property to be named generation_config + } as any); + + const step = interaction.steps?.at(-1); + reviewComment = (step as any)?.text || (step as any)?.parts?.[0]?.text || ""; + + if (!reviewComment) { + console.error("No content received from Gemini."); + process.exit(1); + } + reviewComment += "\n\n— *AI Code Review (Powered by Gemini)*"; + } + + // 3. Post to GitHub (and handle deduplication) + console.log("Fetching existing reviews..."); + const reviewsResponse = await fetch(`https://api.github.com/repos/${REPO}/pulls/${PR_NUMBER}/reviews`, { + headers: { + 'Authorization': `Bearer ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + + if (reviewsResponse.ok) { + const reviews = await reviewsResponse.json(); + // Look for a previous review made by our bot + const previousReview = reviews.find((r: any) => r.body && r.body.includes('AI Code Review (Powered by')); + + if (previousReview) { + console.log(`Updating existing review #${previousReview.id}...`); + const updateResponse = await fetch(`https://api.github.com/repos/${REPO}/pulls/${PR_NUMBER}/reviews/${previousReview.id}`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'X-GitHub-Api-Version': '2022-11-28' + }, + body: JSON.stringify({ body: reviewComment }) + }); + + if (!updateResponse.ok) { + const ghError = await updateResponse.text(); + console.error(`GitHub API Error on update: ${updateResponse.status} - ${ghError}`); + process.exit(1); + } + console.log("Review successfully updated!"); + return; + } + } + + console.log("Posting new review to GitHub PR..."); + const ghResponse = await fetch(`https://api.github.com/repos/${REPO}/pulls/${PR_NUMBER}/reviews`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'X-GitHub-Api-Version': '2022-11-28' + }, + body: JSON.stringify({ + body: reviewComment, + event: 'COMMENT' + }) + }); + + if (!ghResponse.ok) { + const ghError = await ghResponse.text(); + console.error(`GitHub API Error on post: ${ghResponse.status} - ${ghError}`); + process.exit(1); + } + + console.log("Review successfully posted!"); + + } catch (error) { + console.error("Error during AI review:", error); + process.exit(1); + } +} + +run(); diff --git a/scripts/reset-database.cjs b/scripts/reset-database.cjs index 3533224b..b0c5cb66 100644 --- a/scripts/reset-database.cjs +++ b/scripts/reset-database.cjs @@ -30,6 +30,7 @@ async function resetDatabase() { const sql = postgres(connectionString, { max: 1 }); const db = drizzle(sql); + void db; try { // Get all tables diff --git a/scripts/security/audit.mjs b/scripts/security/audit.mjs index 2dfce7c8..7f428e8d 100755 --- a/scripts/security/audit.mjs +++ b/scripts/security/audit.mjs @@ -15,7 +15,7 @@ * 1 — fail (disallowed high/critical vulnerability present) * 2 — script error */ -import { existsSync, readFileSync, statSync, copyFileSync, mkdtempSync, rmSync, copyFileSync as copyFileSyncEsm } from "node:fs"; +import { existsSync, readFileSync, statSync, copyFileSync, mkdtempSync, rmSync } from "node:fs"; import { resolve, join } from "node:path"; import { tmpdir } from "node:os"; import { spawnSync } from "node:child_process"; diff --git a/scripts/test-storage-config.ts b/scripts/test-storage-config.ts index cba3a4ee..7215cc57 100644 --- a/scripts/test-storage-config.ts +++ b/scripts/test-storage-config.ts @@ -1,5 +1,5 @@ -import { getStorage, getStorageConfig, resetStorage } from "../src/lib/storage"; +import { getStorage, getStorageConfig } from "../src/lib/storage"; async function main() { console.log("--- Testing Storage Configuration ---"); diff --git a/scripts/worker.ts b/scripts/worker.ts index 4c9b4a8e..9fa48ffd 100644 --- a/scripts/worker.ts +++ b/scripts/worker.ts @@ -5,7 +5,7 @@ import { logger } from "@/lib/logger"; import { syncAllMirrors } from "@/lib/mirror-sync"; import { queueWorker } from "@/lib/queue-worker"; import { runDueDigests } from "@/lib/chat-notifications"; -import { and, eq, lt } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { createServer } from "http"; // ── Configuration ───────────────────────────────────────────────────────────── diff --git a/src/lib/openapi.ts b/src/lib/openapi.ts index 4495abb2..276cace2 100644 --- a/src/lib/openapi.ts +++ b/src/lib/openapi.ts @@ -496,7 +496,7 @@ export const openApiSpec = { post: { tags: ["AI Review"], summary: "Receive external AI agent callback", - description: "Authenticated callback endpoint for external agents (CodeRabbit-like) to finalize async AI reviews.", + description: "Authenticated callback endpoint for external agents (Greptile-like) to finalize async AI reviews.", parameters: [ { name: "owner", in: "path", required: true, schema: { type: "string" } }, { name: "repo", in: "path", required: true, schema: { type: "string" } }, diff --git a/src/middleware/rate-limit.ts b/src/middleware/rate-limit.ts index 65cc77a7..b2c20b6b 100644 --- a/src/middleware/rate-limit.ts +++ b/src/middleware/rate-limit.ts @@ -61,6 +61,11 @@ export function createRateLimitMiddleware( const config = customLimit || RATE_LIMITS[tier]; return async (request: Request, context?: any): Promise => { + // Skip if globally disabled + if (process.env.RATE_LIMIT_ENABLED === "false" || process.env.RATE_LIMIT_ENABLED === "0") { + return null; + } + // Skip rate limiting in development if configured if ( process.env.NODE_ENV === "development" && diff --git a/src/pages/api/health.ts b/src/pages/api/health.ts index 3290c574..7fdcbda6 100644 --- a/src/pages/api/health.ts +++ b/src/pages/api/health.ts @@ -1,4 +1,5 @@ -import { getDb } from "@/db/adapter"; +import { getDatabase } from "@/db"; +import { sql } from "drizzle-orm"; import type { APIRoute } from "astro"; import { withErrorHandler } from "@/lib/errors"; import { isDistributedLocking } from "@/lib/distributed-lock"; @@ -14,8 +15,12 @@ export const GET: APIRoute = withErrorHandler(async () => { // Check database const dbStart = Date.now(); try { - const db = getDb(); - await db.rawQuery("SELECT 1"); + const db = getDatabase() as any; + if (db.execute) { + await db.execute(sql`SELECT 1`); + } else { + await db.run(sql`SELECT 1`); + } checks.database = { status: "ok", latency: Date.now() - dbStart }; } catch (error) { checks.database = { @@ -42,6 +47,7 @@ export const GET: APIRoute = withErrorHandler(async () => { try { const fs = await import("fs/promises"); const storagePath = process.env.STORAGE_PATH || "./data/storage"; + await fs.mkdir(storagePath, { recursive: true }).catch(() => {}); await fs.access(storagePath); checks.storage = { status: "ok" }; } catch (error) { @@ -58,17 +64,19 @@ export const GET: APIRoute = withErrorHandler(async () => { const production = process.env.NODE_ENV === "production"; const scalingIssues: string[] = []; - if (production && !redisConfigured) { - scalingIssues.push("REDIS_URL is required in production for distributed coordination"); - } - if (production && !isDistributedLocking) { - scalingIssues.push("Distributed locking is not active"); - } - if (production && !isDistributedRateLimit) { - scalingIssues.push("Distributed rate limiting is not active"); - } - if (production && !queueWorker.multiInstanceSafe) { - scalingIssues.push("Queue worker is not multi-instance safe"); + if (production && process.env.SKIP_REDIS_CHECK !== "1") { + if (!redisConfigured) { + scalingIssues.push("REDIS_URL is required in production for distributed coordination"); + } + if (!isDistributedLocking) { + scalingIssues.push("Distributed locking is not active"); + } + if (!isDistributedRateLimit) { + scalingIssues.push("Distributed rate limiting is not active"); + } + if (!queueWorker.multiInstanceSafe) { + scalingIssues.push("Queue worker is not multi-instance safe"); + } } checks.scaling = { diff --git a/src/pages/settings/ai-review-rules.astro b/src/pages/settings/ai-review-rules.astro index a6e62ffe..e284cce0 100644 --- a/src/pages/settings/ai-review-rules.astro +++ b/src/pages/settings/ai-review-rules.astro @@ -1,7 +1,7 @@ --- import BaseLayout from "@/layouts/BaseLayout.astro"; import { getDatabase, schema } from "@/db"; -import { eq, and, desc } from "drizzle-orm"; +import { eq, desc } from "drizzle-orm"; import { Bot, Plus, diff --git a/src/pages/settings/ai.astro b/src/pages/settings/ai.astro index f3e6cec7..14c1010a 100644 --- a/src/pages/settings/ai.astro +++ b/src/pages/settings/ai.astro @@ -153,7 +153,7 @@ const sideNav = [ value="external_agent" selected={aiConfig.provider === "external_agent"} - >External Agent (CodeRabbit-like)External Agent (Greptile-like)