diff --git a/.env.development b/.env.development index ae6fdbb3..463cc1fa 100644 --- a/.env.development +++ b/.env.development @@ -23,6 +23,8 @@ AUTH_URL="http://localhost:3000" # AUTH_GOOGLE_CLIENT_ID="" # AUTH_GOOGLE_CLIENT_SECRET="" +#DATA_CACHE_DIR="" # Path to the sourcebot cache dir (ex. ~/sourcebot/.sourcebot) + # Email # EMAIL_FROM_ADDRESS="" # The from address for transactional emails. # SMTP_CONNECTION_URL="" # The SMTP connection URL for transactional emails. @@ -51,6 +53,16 @@ REDIS_URL="redis://localhost:6379" # STRIPE_WEBHOOK_SECRET: z.string().optional(), # STRIPE_ENABLE_TEST_CLOCKS=false +# Agents + +# GITHUB_APP_ID= +# GITHUB_APP_PRIVATE_KEY_PATH= +# GITHUB_APP_WEBHOOK_SECRET= +# OPENAI_API_KEY= +REVIEW_AGENT_LOGGING_ENABLED=true +REVIEW_AGENT_AUTO_REVIEW_ENABLED=false +REVIEW_AGENT_REVIEW_COMMAND=review + # Misc # Generated using: @@ -78,4 +90,4 @@ SOURCEBOT_TELEMETRY_DISABLED=true # Disables telemetry collection # NODE_ENV= # SOURCEBOT_TENANCY_MODE=single -# NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT= \ No newline at end of file +# NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT= diff --git a/docs/docs.json b/docs/docs.json index a897fd5e..949b99f0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -43,6 +43,13 @@ } ] }, + { + "group": "Agents", + "pages": [ + "docs/agents/overview", + "docs/agents/review-agent" + ] + }, { "group": "More", "pages": [ diff --git a/docs/docs/agents/overview.mdx b/docs/docs/agents/overview.mdx new file mode 100644 index 00000000..86fce12d --- /dev/null +++ b/docs/docs/agents/overview.mdx @@ -0,0 +1,17 @@ +--- +title: "Agents Overview" +sidebarTitle: "Overview" +--- + + +Have an idea for an agent that we haven't built? Submit a [feature request](https://github.com/sourcebot-dev/sourcebot/discussions/categories/feature-requests) on our GitHub + + +Agents are automations that leverage the code indexed on Sourcebot to perform a specific task. Once you've setup Sourcebot, check out the +guides below to configure additional agents. + + + + An AI agent that reviews your PRs to identify issues + + \ No newline at end of file diff --git a/docs/docs/agents/review-agent.mdx b/docs/docs/agents/review-agent.mdx new file mode 100644 index 00000000..ba1ef642 --- /dev/null +++ b/docs/docs/agents/review-agent.mdx @@ -0,0 +1,93 @@ +--- +title: AI Code Review Agent +sidebarTitle: AI Code Review Agent +--- + + +This agent sends data to OpenAI (through an API key you supply) to perform code reviews. This data includes code from the PR being reviewed, as well as additional relevant context from your +codebase that the agent may fetch to perform the review. + + +This agent provides codebase-aware reviews for your PRs. For each diff, this agent fetches relevant context from Sourcebot and feeds it into an LLM for a detailed review of your changes. + +The AI Code Review Agent is open source and packaged in [Sourcebot](https://github.com/sourcebot-dev/sourcebot). To get started using this agent, [deploy Sourcebot](/self-hosting/overview) +and then follow the configuration instructions below. + +![AI Code Review Agent Example](/images/review_agent_example.png) + +# Configure + +This agent currently only supports reviewing GitHub PRs. You configure the agent by creating a GitHub app, installing it into your GitHub organization, and then giving your app info to Sourcebot. + +Before you get started, make sure you have an OpenAPI account that you can create an OpenAPI key with. + + + + Follow the official GitHub guide for [registering a GitHub app](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app) + + - GitHub App name: You can make this whatever you want (ex. Sourcebot Review Agent) + - Homepage URL: You can make this whatever you want (ex. https://www.sourcebot.dev/) + - Webhook URL (**IMPORTANT**): You must set this to point to your Sourcebot deployment at /api/webhook (ex. https://sourcebot.aperture.com/api/webhook). Your Sourcebot deployment must be able to accept requests from GitHub + (either github.com or your self-hosted enterprise server) for this to work. If you're running Sourcebot locally, you can [use smee](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/quickstart#step-2-get-a-webhook-proxy-url) to [forward webhooks](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/quickstart#step-6-start-your-server) to your local deployment. + - Permissions + - Pull requests: Read & Write + - Issues: Read & Write + - Contents: Read + - Events: + - Pull request + - Issue comment + + + Navigate to your new [GitHub app's page](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/quickstart#navigate-to-your-app-settings) and press `Install` + + + Sourcebot requires the following environment variables to begin reviewing PRs through your new GitHub app: + + - `GITHUB_APP_ID`: The client ID of your GitHub app. Can be found in your [app settings](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/quickstart#navigate-to-your-app-settings) + - `GITHUB_APP_WEBHOOK_SECRET`: A random webhook secret that you've set in your [app settings](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/quickstart#navigate-to-your-app-settings). This can be anything (ex. `python -c "import secrets; print(secrets.token_hex(10))"` to generate a random secret) + - `GITHUB_APP_PRIVATE_KEY_PATH`: The path to your app's private key. If you're running Sourcebot from a container, this is the path to this file from within your container + (ex `/data/review-agent-key.pem`). You must copy the private key file into the directory you mount to Sourcebot (similar to the config file). + + You can generate a private key file for your app in the [app settings](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/quickstart#navigate-to-your-app-settings). You must copy this private key file into the + directory that you mount to Sourcebot + ![GitHub App Private Key](/images/github_app_private_key.png) + - `OPENAI_API_KEY`: Your OpenAI API key + - `REVIEW_AGENT_AUTO_REVIEW_ENABLED` (default: `false`): If enabled, the review agent will automatically review any new or updated PR. If disabled, you must invoke it using the command defined by `REVIEW_AGENT_REVIEW_COMMAND` + - `REVIEW_AGENT_REVIEW_COMMAND` (default: `review`): The command that invokes the review agent (ex. `/review`) when a user comments on the PR. Don't include the slash character in this value. + + You can find an example docker compose file below. + - This docker compose file is placed in `~/sourcebot_review_agent_workspace`, and I'm mounting that directory to Sourcebot + - The config and the app private key files are placed in this directory + - The paths to these files are given to Sourcebot relative to `/data` since that's the directory in Sourcebot that I'm mounting to + + ```yaml + services: + sourcebot: + image: ghcr.io/sourcebot-dev/sourcebot:latest + pull_policy: always + container_name: sourcebot + ports: + - "3000:3000" + volumes: + - "/Users/michael/sourcebot_review_agent_workspace:/data" + environment: + CONFIG_PATH: "/data/config.json" + GITHUB_APP_ID: "my-github-app-id" + GITHUB_APP_WEBHOOK_SECRET: "my-github-app-webhook-secret" + GITHUB_APP_PRIVATE_KEY_PATH: "/data/review-agent-key.pem" + OPENAI_API_KEY: "sk-proj-my-open-api-key" + ``` + + + Navigate to the agents page by pressing `Agents` in the Sourcebot nav menu. If you've configured your environment variables correctly you'll see the following: + + ![Review Agent Configured](/images/review_agent_configured.png) + + + +# Using the agent + +The review agent will not automatically review your PRs by default. To enable this feature, set the `REVIEW_AGENT_AUTO_REVIEW_ENABLED` environment variable to true. + +You can invoke the review agent manually by commenting `/review` on the PR you'd like it to review. You can configure the command that triggers the agent by changing +the `REVIEW_AGENT_REVIEW_COMMAND` environment variable. \ No newline at end of file diff --git a/docs/docs/more/mcp-server.mdx b/docs/docs/more/mcp-server.mdx index 39968add..d992f7de 100644 --- a/docs/docs/more/mcp-server.mdx +++ b/docs/docs/more/mcp-server.mdx @@ -7,7 +7,7 @@ sidebarTitle: Sourcebot MCP server This feature is only available when [self-hosting](/self-hosting) with [authentication](/self-hosting/more/authentication) disabled. -The [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) is a open standard for providing context to LLMs. The [@sourcebot/mcp](https://www.npmjs.com/package/@sourcebot/mcp) package is a MCP server that enables LLMs to interface with your Sourcebot instance, enabling MCP clients like Cursor, Vscode, and others to have context over your entire codebase. +The [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) is an open standard for providing context to LLMs. The [@sourcebot/mcp](https://www.npmjs.com/package/@sourcebot/mcp) package is a MCP server that enables LLMs to interface with your Sourcebot instance, enabling MCP clients like Cursor, Vscode, and others to have context over your entire codebase. ## Getting Started diff --git a/docs/images/github_app_private_key.png b/docs/images/github_app_private_key.png new file mode 100644 index 00000000..819fae23 Binary files /dev/null and b/docs/images/github_app_private_key.png differ diff --git a/docs/images/review_agent_configured.png b/docs/images/review_agent_configured.png new file mode 100644 index 00000000..6e824566 Binary files /dev/null and b/docs/images/review_agent_configured.png differ diff --git a/docs/images/review_agent_example.png b/docs/images/review_agent_example.png new file mode 100644 index 00000000..d933a199 Binary files /dev/null and b/docs/images/review_agent_example.png differ diff --git a/package.json b/package.json index e4781586..53e3e363 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "private": true, "workspaces": [ - "packages/*" + "packages/*", + "packages/agents/*" ], "scripts": { "build": "cross-env SKIP_ENV_VALIDATION=1 yarn workspaces foreach -A run build", @@ -11,6 +12,7 @@ "dev:zoekt": "yarn with-env zoekt-webserver -index .sourcebot/index -rpc", "dev:backend": "yarn with-env yarn workspace @sourcebot/backend dev:watch", "dev:web": "yarn with-env yarn workspace @sourcebot/web dev", + "dev:review-agent": "yarn with-env yarn workspace @sourcebot/review-agent dev", "watch:mcp": "yarn workspace @sourcebot/mcp build:watch", "watch:schemas": "yarn workspace @sourcebot/schemas watch", "dev:prisma:migrate:dev": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:dev", diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts index 472bcec2..512c9dae 100644 --- a/packages/backend/src/env.ts +++ b/packages/backend/src/env.ts @@ -27,6 +27,8 @@ export const env = createEnv({ SOURCEBOT_INSTALL_ID: z.string().default("unknown"), NEXT_PUBLIC_SOURCEBOT_VERSION: z.string().default("unknown"), + DATA_CACHE_DIR: z.string(), + NEXT_PUBLIC_POSTHOG_PAPIK: z.string().optional(), FALLBACK_GITHUB_CLOUD_TOKEN: z.string().optional(), diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 702cb0e5..346f2ff9 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -8,6 +8,7 @@ import path from 'path'; import { AppContext } from "./types.js"; import { main } from "./main.js" import { PrismaClient } from "@sourcebot/db"; +import { env } from "./env.js"; // Register handler for normal exit process.on('exit', (code) => { @@ -36,22 +37,9 @@ process.on('unhandledRejection', (reason, promise) => { process.exit(1); }); +console.log(process.cwd()); -const parser = new ArgumentParser({ - description: "Sourcebot backend tool", -}); - -type Arguments = { - cacheDir: string; -} - -parser.add_argument("--cacheDir", { - help: "Path to .sourcebot cache directory", - required: true, -}); -const args = parser.parse_args() as Arguments; - -const cacheDir = args.cacheDir; +const cacheDir = env.DATA_CACHE_DIR; const reposPath = path.join(cacheDir, 'repos'); const indexPath = path.join(cacheDir, 'index'); diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 351ea9c6..4e2e3de1 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -38,4 +38,4 @@ "sourcebot", "code-intelligence" ] -} \ No newline at end of file +} diff --git a/packages/web/package.json b/packages/web/package.json index a652f59e..39aebf65 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -118,6 +118,9 @@ "next-auth": "^5.0.0-beta.25", "next-themes": "^0.3.0", "nodemailer": "^6.10.0", + "octokit": "^4.1.3", + "openai": "^4.98.0", + "parse-diff": "^0.11.1", "posthog-js": "^1.161.5", "pretty-bytes": "^6.1.1", "psl": "^1.15.0", @@ -135,7 +138,8 @@ "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "usehooks-ts": "^3.1.0", - "zod": "^3.24.3" + "zod": "^3.24.3", + "zod-to-json-schema": "^3.24.5" }, "devDependencies": { "@types/micromatch": "^4.0.9", diff --git a/packages/web/src/app/[domain]/agents/page.tsx b/packages/web/src/app/[domain]/agents/page.tsx new file mode 100644 index 00000000..7f283ede --- /dev/null +++ b/packages/web/src/app/[domain]/agents/page.tsx @@ -0,0 +1,69 @@ +import Link from "next/link"; +import { NavigationMenu } from "../components/navigationMenu"; +import { FaCogs } from "react-icons/fa"; +import { env } from "@/env.mjs"; + +const agents = [ + { + id: "review-agent", + name: "Review Agent", + description: "An AI code review agent that reviews your PRs. Uses the code indexed on Sourcebot to provide codebase-wide context.", + requiredEnvVars: ["GITHUB_APP_ID", "GITHUB_APP_WEBHOOK_SECRET", "GITHUB_APP_PRIVATE_KEY_PATH", "OPENAI_API_KEY"], + configureUrl: "https://docs.sourcebot.dev/docs/agents/review-agent" + }, +]; + +export default function AgentsPage({ params: { domain } }: { params: { domain: string } }) { + return ( +
+ +
+
+ {agents.map((agent) => ( +
+ {/* Name and description */} +
+

+ {agent.name} +

+

+ {agent.description} +

+
+ {/* Actions */} +
+ {agent.requiredEnvVars.every(envVar => envVar in env && env[envVar as keyof typeof env] !== undefined) ? ( +
+ Agent is configured and accepting requests on /api/webhook +
+ ) : ( + + Configure + + )} +
+
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/browse/[...path]/page.tsx b/packages/web/src/app/[domain]/browse/[...path]/page.tsx index 89e4e293..afa10999 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/page.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/page.tsx @@ -3,7 +3,8 @@ import { TopBar } from "@/app/[domain]/components/topBar"; import { Separator } from '@/components/ui/separator'; import { getFileSource } from '@/features/search/fileSourceApi'; import { listRepositories } from '@/features/search/listReposApi'; -import { base64Decode, isServiceError } from "@/lib/utils"; +import { isServiceError } from "@/lib/utils"; +import { base64Decode } from "@/lib/utils"; import { CodePreview } from "./codePreview"; import { ErrorCode } from "@/lib/errorCodes"; import { LuFileX2, LuBookX } from "react-icons/lu"; diff --git a/packages/web/src/app/[domain]/components/navigationMenu.tsx b/packages/web/src/app/[domain]/components/navigationMenu.tsx index 75ff020c..ba760d6b 100644 --- a/packages/web/src/app/[domain]/components/navigationMenu.tsx +++ b/packages/web/src/app/[domain]/components/navigationMenu.tsx @@ -59,6 +59,13 @@ export const NavigationMenu = async ({ + + + + Agents + + + diff --git a/packages/web/src/app/api/(server)/source/route.ts b/packages/web/src/app/api/(server)/source/route.ts index 78522f11..dc361a02 100644 --- a/packages/web/src/app/api/(server)/source/route.ts +++ b/packages/web/src/app/api/(server)/source/route.ts @@ -27,7 +27,7 @@ export const POST = async (request: NextRequest) => { } -const postSource = (request: FileSourceRequest, domain: string) => sew(() => +export const postSource = (request: FileSourceRequest, domain: string) => sew(() => withAuth(async (session) => withOrgMembership(session, domain, async ({ orgId }) => { const response = await getFileSource(request, orgId); diff --git a/packages/web/src/app/api/(server)/webhook/route.ts b/packages/web/src/app/api/(server)/webhook/route.ts new file mode 100644 index 00000000..4f980d07 --- /dev/null +++ b/packages/web/src/app/api/(server)/webhook/route.ts @@ -0,0 +1,113 @@ +'use server'; + +import { NextRequest } from "next/server"; +import { App, Octokit } from "octokit"; +import { WebhookEventDefinition} from "@octokit/webhooks/types"; +import { EndpointDefaults } from "@octokit/types"; +import { env } from "@/env.mjs"; +import { processGitHubPullRequest } from "@/features/agents/review-agent/app"; +import { throttling } from "@octokit/plugin-throttling"; +import fs from "fs"; +import { GitHubPullRequest } from "@/features/agents/review-agent/types"; + +let githubApp: App | undefined; +if (env.GITHUB_APP_ID && env.GITHUB_APP_WEBHOOK_SECRET && env.GITHUB_APP_PRIVATE_KEY_PATH) { + try { + const privateKey = fs.readFileSync(env.GITHUB_APP_PRIVATE_KEY_PATH, "utf8"); + + const throttledOctokit = Octokit.plugin(throttling); + githubApp = new App({ + appId: env.GITHUB_APP_ID, + privateKey: privateKey, + webhooks: { + secret: env.GITHUB_APP_WEBHOOK_SECRET, + }, + Octokit: throttledOctokit, + throttle: { + onRateLimit: (retryAfter: number, options: Required, octokit: Octokit, retryCount: number) => { + if (retryCount > 3) { + console.log(`Rate limit exceeded: ${retryAfter} seconds`); + return false; + } + + return true; + }, + } + }); + } catch (error) { + console.error(`Error initializing GitHub app: ${error}`); + } +} + +function isPullRequestEvent(eventHeader: string, payload: unknown): payload is WebhookEventDefinition<"pull-request-opened"> | WebhookEventDefinition<"pull-request-synchronize"> { + return eventHeader === "pull_request" && typeof payload === "object" && payload !== null && "action" in payload && typeof payload.action === "string" && (payload.action === "opened" || payload.action === "synchronize"); +} + +function isIssueCommentEvent(eventHeader: string, payload: unknown): payload is WebhookEventDefinition<"issue-comment-created"> { + return eventHeader === "issue_comment" && typeof payload === "object" && payload !== null && "action" in payload && typeof payload.action === "string" && payload.action === "created"; +} + +export const POST = async (request: NextRequest) => { + const body = await request.json(); + const headers = Object.fromEntries(request.headers.entries()); + + const githubEvent = headers['x-github-event'] || headers['X-GitHub-Event']; + if (githubEvent) { + console.log('GitHub event received:', githubEvent); + + if (!githubApp) { + console.warn('Received GitHub webhook event but GitHub app env vars are not set'); + return Response.json({ status: 'ok' }); + } + + if (isPullRequestEvent(githubEvent, body)) { + if (env.REVIEW_AGENT_AUTO_REVIEW_ENABLED === "false") { + console.log('Review agent auto review (REVIEW_AGENT_AUTO_REVIEW_ENABLED) is disabled, skipping'); + return Response.json({ status: 'ok' }); + } + + if (!body.installation) { + console.error('Received github pull request event but installation is not present'); + return Response.json({ status: 'ok' }); + } + + const installationId = body.installation.id; + const octokit = await githubApp.getInstallationOctokit(installationId); + + const pullRequest = body.pull_request as GitHubPullRequest; + await processGitHubPullRequest(octokit, pullRequest); + } + + if (isIssueCommentEvent(githubEvent, body)) { + const comment = body.comment.body; + if (!comment) { + console.warn('Received issue comment event but comment body is empty'); + return Response.json({ status: 'ok' }); + } + + if (comment === `/${env.REVIEW_AGENT_REVIEW_COMMAND}`) { + console.log('Review agent review command received, processing'); + + if (!body.installation) { + console.error('Received github issue comment event but installation is not present'); + return Response.json({ status: 'ok' }); + } + + const pullRequestNumber = body.issue.number; + const repositoryName = body.repository.name; + const owner = body.repository.owner.login; + + const octokit = await githubApp.getInstallationOctokit(body.installation.id); + const { data: pullRequest } = await octokit.rest.pulls.get({ + owner, + repo: repositoryName, + pull_number: pullRequestNumber, + }); + + await processGitHubPullRequest(octokit, pullRequest); + } + } + } + + return Response.json({ status: 'ok' }); +} \ No newline at end of file diff --git a/packages/web/src/env.mjs b/packages/web/src/env.mjs index e75cc5d6..9002e4c3 100644 --- a/packages/web/src/env.mjs +++ b/packages/web/src/env.mjs @@ -27,6 +27,8 @@ export const env = createEnv({ AUTH_URL: z.string().url(), AUTH_CREDENTIALS_LOGIN_ENABLED: booleanSchema.default('true'), + DATA_CACHE_DIR: z.string(), + // Email SMTP_CONNECTION_URL: z.string().url().optional(), EMAIL_FROM_ADDRESS: z.string().email().optional(), @@ -52,6 +54,15 @@ export const env = createEnv({ // EE License SOURCEBOT_EE_LICENSE_KEY: z.string().optional(), + + // GitHub app for review agent + GITHUB_APP_ID: z.string().optional(), + GITHUB_APP_WEBHOOK_SECRET: z.string().optional(), + GITHUB_APP_PRIVATE_KEY_PATH: z.string().optional(), + OPENAI_API_KEY: z.string().optional(), + REVIEW_AGENT_LOGGING_ENABLED: booleanSchema.default('true'), + REVIEW_AGENT_AUTO_REVIEW_ENABLED: booleanSchema.default('false'), + REVIEW_AGENT_REVIEW_COMMAND: z.string().default('review'), }, // @NOTE: Please make sure of the following: // - Make sure you destructure all client variables in diff --git a/packages/web/src/features/agents/review-agent/app.ts b/packages/web/src/features/agents/review-agent/app.ts new file mode 100644 index 00000000..f62d77a9 --- /dev/null +++ b/packages/web/src/features/agents/review-agent/app.ts @@ -0,0 +1,51 @@ +import { Octokit } from "octokit"; +import { generatePrReviews } from "@/features/agents/review-agent/nodes/generatePrReview"; +import { githubPushPrReviews } from "@/features/agents/review-agent/nodes/githubPushPrReviews"; +import { githubPrParser } from "@/features/agents/review-agent/nodes/githubPrParser"; +import { env } from "@/env.mjs"; +import { GitHubPullRequest } from "@/features/agents/review-agent/types"; +import path from "path"; +import fs from "fs"; + +const rules = [ + "Do NOT provide general feedback, summaries, explanations of changes, or praises for making good additions.", + "Do NOT provide any advice that is not actionable or directly related to the changes.", + "Do NOT provide any comments or reviews on code that you believe is good, correct, or a good addition. Your job is only to identify issues and provide feedback on how to fix them.", + "If a review for a chunk contains different reviews at different line ranges, return a seperate review object for each line range.", + "Focus solely on offering specific, objective insights based on the given context and refrain from making broad comments about potential impacts on the system or question intentions behind the changes.", + "Keep comments concise and to the point. Every comment must highlight a specific issue and provide a clear and actionable solution to the developer.", + "If there are no issues found on a line range, do NOT respond with any comments. This includes comments such as \"No issues found\" or \"LGTM\"." +] + +export async function processGitHubPullRequest(octokit: Octokit, pullRequest: GitHubPullRequest) { + console.log(`Received a pull request event for #${pullRequest.number}`); + + if (!env.OPENAI_API_KEY) { + console.error("OPENAI_API_KEY is not set, skipping review agent"); + return; + } + + let reviewAgentLogPath: string | undefined; + if (env.REVIEW_AGENT_LOGGING_ENABLED) { + const reviewAgentLogDir = path.join(env.DATA_CACHE_DIR, "review-agent"); + if (!fs.existsSync(reviewAgentLogDir)) { + fs.mkdirSync(reviewAgentLogDir, { recursive: true }); + } + + const timestamp = new Date().toLocaleString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }).replace(/(\d+)\/(\d+)\/(\d+), (\d+):(\d+):(\d+)/, '$3_$1_$2_$4_$5_$6'); + reviewAgentLogPath = path.join(reviewAgentLogDir, `review-agent-${pullRequest.number}-${timestamp}.log`); + console.log(`Review agent logging to ${reviewAgentLogPath}`); + } + + const prPayload = await githubPrParser(octokit, pullRequest); + const fileDiffReviews = await generatePrReviews(reviewAgentLogPath, prPayload, rules); + await githubPushPrReviews(octokit, prPayload, fileDiffReviews); +} \ No newline at end of file diff --git a/packages/web/src/features/agents/review-agent/nodes/fetchFileContent.ts b/packages/web/src/features/agents/review-agent/nodes/fetchFileContent.ts new file mode 100644 index 00000000..edd03274 --- /dev/null +++ b/packages/web/src/features/agents/review-agent/nodes/fetchFileContent.ts @@ -0,0 +1,33 @@ +import { sourcebot_context, sourcebot_pr_payload } from "@/features/agents/review-agent/types"; +import { fileSourceResponseSchema } from "@/features/search/schemas"; +import { base64Decode } from "@/lib/utils"; +import { postSource } from "@/app/api/(server)/source/route"; +import { isServiceError } from "@/lib/utils"; + +export const fetchFileContent = async (pr_payload: sourcebot_pr_payload, filename: string): Promise => { + console.log("Executing fetch_file_content"); + + const repoPath = pr_payload.hostDomain + "/" + pr_payload.owner + "/" + pr_payload.repo; + const fileSourceRequest = { + fileName: filename, + repository: repoPath, + } + console.log(JSON.stringify(fileSourceRequest, null, 2)); + + const response = await postSource(fileSourceRequest, "~"); + if (isServiceError(response)) { + throw new Error(`Failed to fetch file content for ${filename} from ${repoPath}: ${response.message}`); + } + + const fileSourceResponse = fileSourceResponseSchema.parse(response); + const fileContent = base64Decode(fileSourceResponse.source); + + const fileContentContext: sourcebot_context = { + type: "file_content", + description: `The content of the file ${filename}`, + context: fileContent, + } + + console.log("Completed fetch_file_content"); + return fileContentContext; +} \ No newline at end of file diff --git a/packages/web/src/features/agents/review-agent/nodes/generateDiffReviewPrompt.ts b/packages/web/src/features/agents/review-agent/nodes/generateDiffReviewPrompt.ts new file mode 100644 index 00000000..13150226 --- /dev/null +++ b/packages/web/src/features/agents/review-agent/nodes/generateDiffReviewPrompt.ts @@ -0,0 +1,44 @@ +import { sourcebot_diff, sourcebot_context, sourcebot_file_diff_review_schema } from "@/features/agents/review-agent/types"; +import { zodToJsonSchema } from "zod-to-json-schema"; + +export const generateDiffReviewPrompt = async (diff: sourcebot_diff, context: sourcebot_context[], rules: string[]) => { + console.log("Executing generate_diff_review_prompt"); + + const prompt = ` + You are an expert software engineer that excells at reviewing code changes. Given the input, additional context, and rules defined below, review the code changes and provide a detailed review. The review you provide + must conform to all of the rules defined below. The output format of your review must conform to the output format defined below. + + # Input + + The input is the old and new code snippets, which represent a single hunk from a git diff. The old code snippet is the code before the changes were made, and the new code snippet is the code after the changes were made. Each code snippet + is a sequence of lines each with a line number. + + ## Old Code Snippet + + \`\`\` + ${diff.oldSnippet} + \`\`\` + + ## New Code Snippet + + \`\`\` + ${diff.newSnippet} + \`\`\` + + # Additional Context + + ${context.map(c => `${c.type}: ${c.description}\n\n${c.context}`).join("\n\n----------------------\n\n")} + + # Rules + + - ${rules.join("\n- ")} + + # Output Format (JSON Schema) + The output must be a valid JSON object that conforms to the following JSON schema. Do NOT respond with anything other than the JSON object. Do NOT respond with + the JSON object in a markdown code block. + ${JSON.stringify(zodToJsonSchema(sourcebot_file_diff_review_schema), null, 2)} + `; + + console.log("Completed generate_diff_review_prompt"); + return prompt; +} \ No newline at end of file diff --git a/packages/web/src/features/agents/review-agent/nodes/generatePrReview.ts b/packages/web/src/features/agents/review-agent/nodes/generatePrReview.ts new file mode 100644 index 00000000..c7ad81a3 --- /dev/null +++ b/packages/web/src/features/agents/review-agent/nodes/generatePrReview.ts @@ -0,0 +1,49 @@ +import { sourcebot_pr_payload, sourcebot_diff_review, sourcebot_file_diff_review, sourcebot_context } from "@/features/agents/review-agent/types"; +import { generateDiffReviewPrompt } from "@/features/agents/review-agent/nodes/generateDiffReviewPrompt"; +import { invokeDiffReviewLlm } from "@/features/agents/review-agent/nodes/invokeDiffReviewLlm"; +import { fetchFileContent } from "@/features/agents/review-agent/nodes/fetchFileContent"; + +export const generatePrReviews = async (reviewAgentLogPath: string | undefined, pr_payload: sourcebot_pr_payload, rules: string[]): Promise => { + console.log("Executing generate_pr_reviews"); + + const file_diff_reviews: sourcebot_file_diff_review[] = []; + for (const file_diff of pr_payload.file_diffs) { + const reviews: sourcebot_diff_review[] = []; + + for (const diff of file_diff.diffs) { + try { + const fileContentContext = await fetchFileContent(pr_payload, file_diff.to); + const context: sourcebot_context[] = [ + { + type: "pr_title", + description: "The title of the pull request", + context: pr_payload.title, + }, + { + type: "pr_description", + description: "The description of the pull request", + context: pr_payload.description, + }, + fileContentContext, + ]; + + const prompt = await generateDiffReviewPrompt(diff, context, rules); + + const diffReview = await invokeDiffReviewLlm(reviewAgentLogPath, prompt); + reviews.push(...diffReview.reviews); + } catch (error) { + console.error(`Error generating review for ${file_diff.to}: ${error}`); + } + } + + if (reviews.length > 0) { + file_diff_reviews.push({ + filename: file_diff.to, + reviews: reviews, + }); + } + } + + console.log("Completed generate_pr_reviews"); + return file_diff_reviews; +} \ No newline at end of file diff --git a/packages/web/src/features/agents/review-agent/nodes/githubPrParser.ts b/packages/web/src/features/agents/review-agent/nodes/githubPrParser.ts new file mode 100644 index 00000000..59ea158b --- /dev/null +++ b/packages/web/src/features/agents/review-agent/nodes/githubPrParser.ts @@ -0,0 +1,64 @@ +import { sourcebot_pr_payload, sourcebot_file_diff, sourcebot_diff } from "@/features/agents/review-agent/types"; +import parse from "parse-diff"; +import { Octokit } from "octokit"; +import { GitHubPullRequest } from "@/features/agents/review-agent/types"; + +export const githubPrParser = async (octokit: Octokit, pullRequest: GitHubPullRequest): Promise => { + console.log("Executing github_pr_parser"); + + let parsedDiff: parse.File[] = []; + try { + const diff = await octokit.request(pullRequest.diff_url); + parsedDiff = parse(diff.data); + } catch (error) { + console.error("Error fetching diff: ", error); + throw error; + } + + const sourcebotFileDiffs: (sourcebot_file_diff | null)[] = parsedDiff.map((file) => { + if (!file.from || !file.to) { + console.log(`Skipping file due to missing from (${file.from}) or to (${file.to})`) + return null; + } + + const diffs: sourcebot_diff[] = file.chunks.map((chunk) => { + let oldSnippet = `@@ -${chunk.oldStart},${chunk.oldLines} +${chunk.newStart},${chunk.newLines} @@\n`; + let newSnippet = `@@ -${chunk.oldStart},${chunk.oldLines} +${chunk.newStart},${chunk.newLines} @@\n`; + + for (const change of chunk.changes) { + if (change.type === "normal") { + oldSnippet += change.ln1 + ":" + change.content + "\n"; + newSnippet += change.ln2 + ":" + change.content + "\n"; + } else if (change.type === "add") { + newSnippet += change.ln + ":" + change.content + "\n"; + } else if (change.type === "del") { + oldSnippet += change.ln + ":" + change.content + "\n"; + } + } + + return { + oldSnippet: oldSnippet, + newSnippet: newSnippet, + } + }); + + return { + from: file.from, + to: file.to, + diffs: diffs, + } + }); + const filteredSourcebotFileDiffs: sourcebot_file_diff[] = sourcebotFileDiffs.filter((file) => file !== null) as sourcebot_file_diff[]; + + console.log("Completed github_pr_parser"); + return { + title: pullRequest.title, + description: pullRequest.body ?? "", + hostDomain: "github.com", + owner: pullRequest.base.repo.owner.login, + repo: pullRequest.base.repo.name, + file_diffs: filteredSourcebotFileDiffs, + number: pullRequest.number, + head_sha: pullRequest.head.sha + } +} \ No newline at end of file diff --git a/packages/web/src/features/agents/review-agent/nodes/githubPushPrReviews.ts b/packages/web/src/features/agents/review-agent/nodes/githubPushPrReviews.ts new file mode 100644 index 00000000..70ecd043 --- /dev/null +++ b/packages/web/src/features/agents/review-agent/nodes/githubPushPrReviews.ts @@ -0,0 +1,37 @@ +import { Octokit } from "octokit"; +import { sourcebot_pr_payload, sourcebot_file_diff_review } from "@/features/agents/review-agent/types"; + +export const githubPushPrReviews = async (octokit: Octokit, pr_payload: sourcebot_pr_payload, file_diff_reviews: sourcebot_file_diff_review[]) => { + console.log("Executing github_push_pr_reviews"); + + try { + for (const file_diff_review of file_diff_reviews) { + for (const review of file_diff_review.reviews) { + try { + await octokit.rest.pulls.createReviewComment({ + owner: pr_payload.owner, + repo: pr_payload.repo, + pull_number: pr_payload.number, + body: review.review, + path: file_diff_review.filename, + commit_id: pr_payload.head_sha, + side: "RIGHT", + ...(review.line_start === review.line_end + ? { line: review.line_start } + : { + start_line: review.line_start, + line: review.line_end, + start_side: "RIGHT", + }), + }); + } catch (error) { + console.error(`Error pushing pr reviews for ${file_diff_review.filename}: ${error}`); + } + } + } + } catch (error) { + console.error(`Error pushing pr reviews: ${error}`); + } + + console.log("Completed github_push_pr_reviews"); +} \ No newline at end of file diff --git a/packages/web/src/features/agents/review-agent/nodes/invokeDiffReviewLlm.ts b/packages/web/src/features/agents/review-agent/nodes/invokeDiffReviewLlm.ts new file mode 100644 index 00000000..2a703804 --- /dev/null +++ b/packages/web/src/features/agents/review-agent/nodes/invokeDiffReviewLlm.ts @@ -0,0 +1,48 @@ +import OpenAI from "openai"; +import { sourcebot_file_diff_review, sourcebot_file_diff_review_schema } from "@/features/agents/review-agent/types"; +import { env } from "@/env.mjs"; +import fs from "fs"; + +export const invokeDiffReviewLlm = async (reviewAgentLogPath: string | undefined, prompt: string): Promise => { + console.log("Executing invoke_diff_review_llm"); + + if (!env.OPENAI_API_KEY) { + console.error("OPENAI_API_KEY is not set, skipping review agent"); + throw new Error("OPENAI_API_KEY is not set, skipping review agent"); + } + + const openai = new OpenAI({ + apiKey: env.OPENAI_API_KEY, + }); + + if (reviewAgentLogPath) { + fs.appendFileSync(reviewAgentLogPath, `\n\nPrompt:\n${prompt}`); + } + + try { + const completion = await openai.chat.completions.create({ + model: "gpt-4.1-mini", + messages: [ + { role: "system", content: prompt } + ] + }); + + const openaiResponse = completion.choices[0].message.content; + if (reviewAgentLogPath) { + fs.appendFileSync(reviewAgentLogPath, `\n\nResponse:\n${openaiResponse}`); + } + + const diffReviewJson = JSON.parse(openaiResponse || '{}'); + const diffReview = sourcebot_file_diff_review_schema.safeParse(diffReviewJson); + + if (!diffReview.success) { + throw new Error(`Invalid diff review format: ${diffReview.error}`); + } + + console.log("Completed invoke_diff_review_llm"); + return diffReview.data; + } catch (error) { + console.error('Error calling OpenAI:', error); + throw error; + } +} \ No newline at end of file diff --git a/packages/web/src/features/agents/review-agent/types.ts b/packages/web/src/features/agents/review-agent/types.ts new file mode 100644 index 00000000..d3264fc1 --- /dev/null +++ b/packages/web/src/features/agents/review-agent/types.ts @@ -0,0 +1,50 @@ +import { components } from "@octokit/openapi-types"; +import { z } from "zod"; + +export type GitHubPullRequest = components["schemas"]["pull-request"]; + +export const sourcebot_diff_schema = z.object({ + oldSnippet: z.string(), + newSnippet: z.string(), +}); +export type sourcebot_diff = z.infer; + +export const sourcebot_file_diff_schema = z.object({ + from: z.string(), + to: z.string(), + diffs: z.array(sourcebot_diff_schema), +}); +export type sourcebot_file_diff = z.infer; + +export const sourcebot_pr_payload_schema = z.object({ + title: z.string(), + description: z.string(), + hostDomain: z.string(), + owner: z.string(), + repo: z.string(), + file_diffs: z.array(sourcebot_file_diff_schema), + number: z.number(), + head_sha: z.string() +}); +export type sourcebot_pr_payload = z.infer; + +export const sourcebot_context_schema = z.object({ + type: z.enum(["pr_title", "pr_description", "pr_summary", "comment_chains", "file_content"]), + description: z.string().optional(), + context: z.string(), +}); +export type sourcebot_context = z.infer; + +export const sourcebot_diff_review_schema = z.object({ + line_start: z.number(), + line_end: z.number(), + review: z.string(), +}); +export type sourcebot_diff_review = z.infer; + +export const sourcebot_file_diff_review_schema = z.object({ + filename: z.string(), + reviews: z.array(sourcebot_diff_review_schema), +}); +export type sourcebot_file_diff_review = z.infer; + diff --git a/staging/fly.toml b/staging/fly.toml deleted file mode 100644 index 6d3f2896..00000000 --- a/staging/fly.toml +++ /dev/null @@ -1,28 +0,0 @@ -# fly.toml app configuration file generated for sourcebot-staging on 2025-01-29T15:57:18-08:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# - -app = 'sourcebot-staging' -primary_region = 'sea' - -[build] - image = 'ghcr.io/sourcebot-dev/sourcebot:staging' - -[[mounts]] - source = 'sourcebot_staging_data' - destination = '/data' - initial_size = "60GB" - -[http_service] - internal_port = 3000 - force_https = true - auto_stop_machines = true - auto_start_machines = true - min_machines_running = 0 - processes = ['app'] - -[[vm]] - memory = '4gb' - cpu_kind = 'performance' - cpus = 2 diff --git a/supervisord.conf b/supervisord.conf index fee47fdd..0873b0cf 100644 --- a/supervisord.conf +++ b/supervisord.conf @@ -22,7 +22,7 @@ stdout_logfile_maxbytes=0 redirect_stderr=true [program:backend] -command=./prefix-output.sh node packages/backend/dist/index.js --cacheDir %(ENV_DATA_CACHE_DIR)s +command=./prefix-output.sh node packages/backend/dist/index.js autostart=true autorestart=true startretries=3 diff --git a/yarn.lock b/yarn.lock index 992e8954..6a6acbdd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2253,6 +2253,75 @@ __metadata: languageName: node linkType: hard +"@octokit/app@npm:^15.1.6": + version: 15.1.6 + resolution: "@octokit/app@npm:15.1.6" + dependencies: + "@octokit/auth-app": "npm:^7.2.1" + "@octokit/auth-unauthenticated": "npm:^6.1.3" + "@octokit/core": "npm:^6.1.5" + "@octokit/oauth-app": "npm:^7.1.6" + "@octokit/plugin-paginate-rest": "npm:^12.0.0" + "@octokit/types": "npm:^14.0.0" + "@octokit/webhooks": "npm:^13.6.1" + checksum: 10c0/865098127d85cc58adda78c9b86cae58c502410a0d9c23d6eeabe633bc6c5e7d5f8901f243c519a0ce72342a8d08d73f120b32a864a8f6e4eeb3ad33326b6825 + languageName: node + linkType: hard + +"@octokit/auth-app@npm:^7.2.1": + version: 7.2.1 + resolution: "@octokit/auth-app@npm:7.2.1" + dependencies: + "@octokit/auth-oauth-app": "npm:^8.1.4" + "@octokit/auth-oauth-user": "npm:^5.1.4" + "@octokit/request": "npm:^9.2.3" + "@octokit/request-error": "npm:^6.1.8" + "@octokit/types": "npm:^14.0.0" + toad-cache: "npm:^3.7.0" + universal-github-app-jwt: "npm:^2.2.0" + universal-user-agent: "npm:^7.0.0" + checksum: 10c0/da8890ad8ae554697a4d58427d7633c8d4d6f7acbfc14e98345b85035cca6773c393f5db5767b455dfc8de9bc5bac4da2d24e443cd48e701de11dd52fa128385 + languageName: node + linkType: hard + +"@octokit/auth-oauth-app@npm:^8.1.3, @octokit/auth-oauth-app@npm:^8.1.4": + version: 8.1.4 + resolution: "@octokit/auth-oauth-app@npm:8.1.4" + dependencies: + "@octokit/auth-oauth-device": "npm:^7.1.5" + "@octokit/auth-oauth-user": "npm:^5.1.4" + "@octokit/request": "npm:^9.2.3" + "@octokit/types": "npm:^14.0.0" + universal-user-agent: "npm:^7.0.0" + checksum: 10c0/2e50711be9ee6c3bf85cf1d58a1d8ae56c36eb894dcfdacabda92b9454b2c49bccc56fb1a989d3e56625885f1ef36278cc01f65c01cde6e3b505b363eac66cc6 + languageName: node + linkType: hard + +"@octokit/auth-oauth-device@npm:^7.1.5": + version: 7.1.5 + resolution: "@octokit/auth-oauth-device@npm:7.1.5" + dependencies: + "@octokit/oauth-methods": "npm:^5.1.5" + "@octokit/request": "npm:^9.2.3" + "@octokit/types": "npm:^14.0.0" + universal-user-agent: "npm:^7.0.0" + checksum: 10c0/f93a67473c58e8b855af15c7300a0937c9d3c6ea898e5e810f2f9762100c6cee22ae52e18c7f926b0e4c6c52573751d4c4ab6294c44116bed2d9fdde5cbdd35d + languageName: node + linkType: hard + +"@octokit/auth-oauth-user@npm:^5.1.3, @octokit/auth-oauth-user@npm:^5.1.4": + version: 5.1.4 + resolution: "@octokit/auth-oauth-user@npm:5.1.4" + dependencies: + "@octokit/auth-oauth-device": "npm:^7.1.5" + "@octokit/oauth-methods": "npm:^5.1.5" + "@octokit/request": "npm:^9.2.3" + "@octokit/types": "npm:^14.0.0" + universal-user-agent: "npm:^7.0.0" + checksum: 10c0/2bb597b2e50fbd5c03bee3a276efa72b03105aff1d9e243c18a71b72d9fe4ad4b1cee52c2df3e022c4169c7c1d37ab14d05455d5de982528914af965e14b763a + languageName: node + linkType: hard + "@octokit/auth-token@npm:^5.0.0": version: 5.1.2 resolution: "@octokit/auth-token@npm:5.1.2" @@ -2260,6 +2329,16 @@ __metadata: languageName: node linkType: hard +"@octokit/auth-unauthenticated@npm:^6.1.2, @octokit/auth-unauthenticated@npm:^6.1.3": + version: 6.1.3 + resolution: "@octokit/auth-unauthenticated@npm:6.1.3" + dependencies: + "@octokit/request-error": "npm:^6.1.8" + "@octokit/types": "npm:^14.0.0" + checksum: 10c0/60639987fa30e68c4ecd98dd9f1604b3f4368384979cdfa2ab9ea2d0b04c50b42a990e67167cf2a0098205a994b3a80d76b23bb0de4c8c956a898b977f5ce7d0 + languageName: node + linkType: hard + "@octokit/core@npm:^6.1.4": version: 6.1.4 resolution: "@octokit/core@npm:6.1.4" @@ -2275,6 +2354,21 @@ __metadata: languageName: node linkType: hard +"@octokit/core@npm:^6.1.5": + version: 6.1.5 + resolution: "@octokit/core@npm:6.1.5" + dependencies: + "@octokit/auth-token": "npm:^5.0.0" + "@octokit/graphql": "npm:^8.2.2" + "@octokit/request": "npm:^9.2.3" + "@octokit/request-error": "npm:^6.1.8" + "@octokit/types": "npm:^14.0.0" + before-after-hook: "npm:^3.0.2" + universal-user-agent: "npm:^7.0.0" + checksum: 10c0/c89ea754cc33da740fdd69fadb971b4b65c89971bba4e8ad545d3ea7aba79759ee3e195c3b72e7df78f14b8b1d392bddc56e7c385d48b5272319ea6a0246ac7c + languageName: node + linkType: hard + "@octokit/endpoint@npm:^10.1.3": version: 10.1.3 resolution: "@octokit/endpoint@npm:10.1.3" @@ -2285,6 +2379,16 @@ __metadata: languageName: node linkType: hard +"@octokit/endpoint@npm:^10.1.4": + version: 10.1.4 + resolution: "@octokit/endpoint@npm:10.1.4" + dependencies: + "@octokit/types": "npm:^14.0.0" + universal-user-agent: "npm:^7.0.2" + checksum: 10c0/bf7cca71a05dc4751df658588e32642e59c98768e7509521226b997ea4837e2d16efd35c391231c76d888226f4daf80e6a9f347dee01a69f490253654dada581 + languageName: node + linkType: hard + "@octokit/graphql@npm:^8.1.2": version: 8.2.1 resolution: "@octokit/graphql@npm:8.2.1" @@ -2296,6 +2400,52 @@ __metadata: languageName: node linkType: hard +"@octokit/graphql@npm:^8.2.2": + version: 8.2.2 + resolution: "@octokit/graphql@npm:8.2.2" + dependencies: + "@octokit/request": "npm:^9.2.3" + "@octokit/types": "npm:^14.0.0" + universal-user-agent: "npm:^7.0.0" + checksum: 10c0/29cd5af5ed04e416d28798a11907ab861dc73c0af47f8c9c3f4183d81d2e77d88228643825538acc81e7015f78d891f84107929019a673b06a6b38ccea6a24e0 + languageName: node + linkType: hard + +"@octokit/oauth-app@npm:^7.1.6": + version: 7.1.6 + resolution: "@octokit/oauth-app@npm:7.1.6" + dependencies: + "@octokit/auth-oauth-app": "npm:^8.1.3" + "@octokit/auth-oauth-user": "npm:^5.1.3" + "@octokit/auth-unauthenticated": "npm:^6.1.2" + "@octokit/core": "npm:^6.1.4" + "@octokit/oauth-authorization-url": "npm:^7.1.1" + "@octokit/oauth-methods": "npm:^5.1.4" + "@types/aws-lambda": "npm:^8.10.83" + universal-user-agent: "npm:^7.0.0" + checksum: 10c0/75264f6cb9336baf815bb85ea450d69c5d33eb70754065c9c5307b10d0efec2d76bb17c7788346dfb8b5e4ad0205e6df841b9c709eaf077390bba793787841f5 + languageName: node + linkType: hard + +"@octokit/oauth-authorization-url@npm:^7.0.0, @octokit/oauth-authorization-url@npm:^7.1.1": + version: 7.1.1 + resolution: "@octokit/oauth-authorization-url@npm:7.1.1" + checksum: 10c0/a2723dde503693d4b0793bc43988d7445bdbd1a4e4994f4e94e635816c87b12a3058609eb5982d91783ddf6cdf663ccdb954b5d05efdc59cb66bc0456d7ba370 + languageName: node + linkType: hard + +"@octokit/oauth-methods@npm:^5.1.4, @octokit/oauth-methods@npm:^5.1.5": + version: 5.1.5 + resolution: "@octokit/oauth-methods@npm:5.1.5" + dependencies: + "@octokit/oauth-authorization-url": "npm:^7.0.0" + "@octokit/request": "npm:^9.2.3" + "@octokit/request-error": "npm:^6.1.8" + "@octokit/types": "npm:^14.0.0" + checksum: 10c0/2b1c5f9ef0fcdb2f19fd864303a67bdda296fcb2cc058efc51bf3fbb3cb582475a5532eab05bd7a57277607730fa4a7796431564693fffdb9c4cb2b471484a5c + languageName: node + linkType: hard + "@octokit/openapi-types@npm:^24.2.0": version: 24.2.0 resolution: "@octokit/openapi-types@npm:24.2.0" @@ -2303,6 +2453,29 @@ __metadata: languageName: node linkType: hard +"@octokit/openapi-types@npm:^25.0.0": + version: 25.0.0 + resolution: "@octokit/openapi-types@npm:25.0.0" + checksum: 10c0/59c9e5998e08cecec155b776c93d8f6f88ab1a812add61cc65f3de8f3744201565545eac308083d18c9fa330a4381a27bcd771a311ac0348d3590a00f333f233 + languageName: node + linkType: hard + +"@octokit/openapi-webhooks-types@npm:11.0.0": + version: 11.0.0 + resolution: "@octokit/openapi-webhooks-types@npm:11.0.0" + checksum: 10c0/cea59ba6b976a242fa4e6bedfab7e6fc3437381557d2c1876ca3aea5f6d4231559a378f9bc35e09593cb2d466afb4a2415be66b960f3e0a38b65b8b9f22ff90e + languageName: node + linkType: hard + +"@octokit/plugin-paginate-graphql@npm:^5.2.4": + version: 5.2.4 + resolution: "@octokit/plugin-paginate-graphql@npm:5.2.4" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10c0/30601cf11d78e5683098d68fbff071a3d20b1e23758f40b1f43fbc93f69b3d0e07f2c9aaaaee113e586af7f604e809ba93702d932b1a4ea65998c7ab39a40a54 + languageName: node + linkType: hard + "@octokit/plugin-paginate-rest@npm:^11.4.2": version: 11.6.0 resolution: "@octokit/plugin-paginate-rest@npm:11.6.0" @@ -2314,6 +2487,17 @@ __metadata: languageName: node linkType: hard +"@octokit/plugin-paginate-rest@npm:^12.0.0": + version: 12.0.0 + resolution: "@octokit/plugin-paginate-rest@npm:12.0.0" + dependencies: + "@octokit/types": "npm:^14.0.0" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10c0/4ed04e8c111ac8cfb0692f917fe09ff6484b7436f0605c661e8051c6fb281c0260c9b067c1422529b948e53b22221b0f7664e2d10a28dcd9db14465c02c7182f + languageName: node + linkType: hard + "@octokit/plugin-request-log@npm:^5.3.1": version: 5.3.1 resolution: "@octokit/plugin-request-log@npm:5.3.1" @@ -2334,6 +2518,42 @@ __metadata: languageName: node linkType: hard +"@octokit/plugin-rest-endpoint-methods@npm:^14.0.0": + version: 14.0.0 + resolution: "@octokit/plugin-rest-endpoint-methods@npm:14.0.0" + dependencies: + "@octokit/types": "npm:^14.0.0" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10c0/c3f26c5277d4aa0c898d8fdbf84326943ea80496e6f60ae34834415384ab629e1e3702d1ed82d40c31a7370edfcb5fa9fe434b0357b302b3be309879bad1d4e6 + languageName: node + linkType: hard + +"@octokit/plugin-retry@npm:^7.2.1": + version: 7.2.1 + resolution: "@octokit/plugin-retry@npm:7.2.1" + dependencies: + "@octokit/request-error": "npm:^6.1.8" + "@octokit/types": "npm:^14.0.0" + bottleneck: "npm:^2.15.3" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10c0/0cac8aa32bfe9612cbaba36f51382c69c882ef7927e1deb4b166fe664959887e952205adbf8b9ff461e0128ac3e1bd807ca58e38041a55ddb3214397584f0406 + languageName: node + linkType: hard + +"@octokit/plugin-throttling@npm:^10.0.0": + version: 10.0.0 + resolution: "@octokit/plugin-throttling@npm:10.0.0" + dependencies: + "@octokit/types": "npm:^14.0.0" + bottleneck: "npm:^2.15.3" + peerDependencies: + "@octokit/core": ^6.1.3 + checksum: 10c0/60294f89bcc03b034fce557dffcb0dba1b02d1fa1834b3b4f542f8635750df4bd23fc143f7c71f802a22a1aaed089c5da77979e6803abe11908b1c91aa1ba419 + languageName: node + linkType: hard + "@octokit/request-error@npm:^6.1.7": version: 6.1.7 resolution: "@octokit/request-error@npm:6.1.7" @@ -2343,6 +2563,15 @@ __metadata: languageName: node linkType: hard +"@octokit/request-error@npm:^6.1.8": + version: 6.1.8 + resolution: "@octokit/request-error@npm:6.1.8" + dependencies: + "@octokit/types": "npm:^14.0.0" + checksum: 10c0/02aa5bfebb5b1b9e152558b4a6f4f7dcb149b41538778ffe0fce3395fd0da5c0862311a78e94723435667581b2a58a7cefa458cf7aa19ae2948ae419276f7ee1 + languageName: node + linkType: hard + "@octokit/request@npm:^9.2.1, @octokit/request@npm:^9.2.2": version: 9.2.2 resolution: "@octokit/request@npm:9.2.2" @@ -2356,6 +2585,19 @@ __metadata: languageName: node linkType: hard +"@octokit/request@npm:^9.2.3": + version: 9.2.3 + resolution: "@octokit/request@npm:9.2.3" + dependencies: + "@octokit/endpoint": "npm:^10.1.4" + "@octokit/request-error": "npm:^6.1.8" + "@octokit/types": "npm:^14.0.0" + fast-content-type-parse: "npm:^2.0.0" + universal-user-agent: "npm:^7.0.2" + checksum: 10c0/96067fc9a5f30f2faa00f08015309930561c3ea0536b543e1c91c475f965eabc81c48fc27d401681ebdb3f6c1acc5d46eaa69163ab98b0faa08be1c02ea5b684 + languageName: node + linkType: hard + "@octokit/rest@npm:^21.0.2": version: 21.1.1 resolution: "@octokit/rest@npm:21.1.1" @@ -2377,6 +2619,33 @@ __metadata: languageName: node linkType: hard +"@octokit/types@npm:^14.0.0": + version: 14.0.0 + resolution: "@octokit/types@npm:14.0.0" + dependencies: + "@octokit/openapi-types": "npm:^25.0.0" + checksum: 10c0/c82da635fe99f265dbef7bf954d45a23ca7ce9c6fc9a8478c247b5435799e5d0eab3ff42f085785ee0882b2de293cab0ab831b379c66f41d00b78412df850ba4 + languageName: node + linkType: hard + +"@octokit/webhooks-methods@npm:^5.1.1": + version: 5.1.1 + resolution: "@octokit/webhooks-methods@npm:5.1.1" + checksum: 10c0/837aa49dbc7f8bc01448f4eaea32cfb0c1cbfa0b4ac570f7bcda385c71f43e4be05e91a3fb63708448410b27e246570fde42056b6b87d755154d84eff5a6500c + languageName: node + linkType: hard + +"@octokit/webhooks@npm:^13.6.1": + version: 13.8.2 + resolution: "@octokit/webhooks@npm:13.8.2" + dependencies: + "@octokit/openapi-webhooks-types": "npm:11.0.0" + "@octokit/request-error": "npm:^6.1.7" + "@octokit/webhooks-methods": "npm:^5.1.1" + checksum: 10c0/5053ab9cff8afe4cd6e7d7a41af3f1b0c0ccc5058482577aa82bb8e5003f3002896c68bd59d7da363204c2d6baa4cca73ed684bb7a5a9286ca4b43172cd1fe9e + languageName: node + linkType: hard + "@opentelemetry/api-logs@npm:0.57.2": version: 0.57.2 resolution: "@opentelemetry/api-logs@npm:0.57.2" @@ -5401,6 +5670,9 @@ __metadata: next-themes: "npm:^0.3.0" nodemailer: "npm:^6.10.0" npm-run-all: "npm:^4.1.5" + octokit: "npm:^4.1.3" + openai: "npm:^4.98.0" + parse-diff: "npm:^0.11.1" postcss: "npm:^8" posthog-js: "npm:^1.161.5" pretty-bytes: "npm:^6.1.1" @@ -5426,6 +5698,7 @@ __metadata: vite-tsconfig-paths: "npm:^5.1.3" vitest: "npm:^2.1.5" zod: "npm:^3.24.3" + zod-to-json-schema: "npm:^3.24.5" languageName: unknown linkType: soft @@ -5606,6 +5879,13 @@ __metadata: languageName: node linkType: hard +"@types/aws-lambda@npm:^8.10.83": + version: 8.10.149 + resolution: "@types/aws-lambda@npm:8.10.149" + checksum: 10c0/9ef41214a1b69fa30d89cab575429793f1ccc49cfe3a7aa75228aef31a202bb6d2c306a5b33def61ef29e0d0a1a324412bf6a4fa5a615e61117685584a394047 + languageName: node + linkType: hard + "@types/body-parser@npm:*": version: 1.19.5 resolution: "@types/body-parser@npm:1.19.5" @@ -5749,6 +6029,16 @@ __metadata: languageName: node linkType: hard +"@types/node-fetch@npm:^2.6.4": + version: 2.6.12 + resolution: "@types/node-fetch@npm:2.6.12" + dependencies: + "@types/node": "npm:*" + form-data: "npm:^4.0.0" + checksum: 10c0/7693acad5499b7df2d1727d46cff092a63896dc04645f36b973dd6dd754a59a7faba76fcb777bdaa35d80625c6a9dd7257cca9c401a4bab03b04480cda7fd1af + languageName: node + linkType: hard + "@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=8.1.0, @types/node@npm:^22.7.5": version: 22.13.11 resolution: "@types/node@npm:22.13.11" @@ -5758,6 +6048,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^18.11.18": + version: 18.19.100 + resolution: "@types/node@npm:18.19.100" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10c0/5524303171eee6788df45d736f5783b5bea27803a596b9cd5669f45487a619e5d8d41d56dd55b8c85c677ffd7c045edd8daea8c4b37e70290bee2a482fc605f6 + languageName: node + linkType: hard + "@types/node@npm:^20": version: 20.17.25 resolution: "@types/node@npm:20.17.25" @@ -6364,6 +6663,15 @@ __metadata: languageName: node linkType: hard +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: "npm:^5.0.0" + checksum: 10c0/90ccc50f010250152509a344eb2e71977fbf8db0ab8f1061197e3275ddf6c61a41a6edfd7b9409c664513131dd96e962065415325ef23efa5db931b382d24ca5 + languageName: node + linkType: hard + "accepts@npm:^2.0.0": version: 2.0.0 resolution: "accepts@npm:2.0.0" @@ -6427,6 +6735,15 @@ __metadata: languageName: node linkType: hard +"agentkeepalive@npm:^4.2.1": + version: 4.6.0 + resolution: "agentkeepalive@npm:4.6.0" + dependencies: + humanize-ms: "npm:^1.2.1" + checksum: 10c0/235c182432f75046835b05f239708107138a40103deee23b6a08caee5136873709155753b394ec212e49e60e94a378189562cb01347765515cff61b692c69187 + languageName: node + linkType: hard + "ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" @@ -6831,6 +7148,13 @@ __metadata: languageName: node linkType: hard +"bottleneck@npm:^2.15.3": + version: 2.19.5 + resolution: "bottleneck@npm:2.19.5" + checksum: 10c0/b0f72e45b2e0f56a21ba720183f16bef8e693452fb0495d997fa354e42904353a94bd8fd429868e6751bc85e54b6755190519eed5a0ae0a94a5185209ae7c6d0 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -8982,6 +9306,13 @@ __metadata: languageName: node linkType: hard +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 10c0/0255d9f936215fd206156fd4caa9e8d35e62075d720dc7d847e89b417e5e62cf1ce6c9b4e0a1633a9256de0efefaf9f8d26924b1f3c8620cffb9db78e7d3076b + languageName: node + linkType: hard + "eventsource-parser@npm:^3.0.1": version: 3.0.1 resolution: "eventsource-parser@npm:3.0.1" @@ -9303,6 +9634,13 @@ __metadata: languageName: node linkType: hard +"form-data-encoder@npm:1.7.2": + version: 1.7.2 + resolution: "form-data-encoder@npm:1.7.2" + checksum: 10c0/56553768037b6d55d9de524f97fe70555f0e415e781cb56fc457a68263de3d40fadea2304d4beef2d40b1a851269bd7854e42c362107071892cb5238debe9464 + languageName: node + linkType: hard + "form-data@npm:^4.0.0": version: 4.0.2 resolution: "form-data@npm:4.0.2" @@ -9315,6 +9653,16 @@ __metadata: languageName: node linkType: hard +"formdata-node@npm:^4.3.2": + version: 4.4.1 + resolution: "formdata-node@npm:4.4.1" + dependencies: + node-domexception: "npm:1.0.0" + web-streams-polyfill: "npm:4.0.0-beta.3" + checksum: 10c0/74151e7b228ffb33b565cec69182694ad07cc3fdd9126a8240468bb70a8ba66e97e097072b60bcb08729b24c7ce3fd3e0bd7f1f80df6f9f662b9656786e76f6a + languageName: node + linkType: hard + "forwarded-parse@npm:2.1.2": version: 2.1.2 resolution: "forwarded-parse@npm:2.1.2" @@ -9891,6 +10239,15 @@ __metadata: languageName: node linkType: hard +"humanize-ms@npm:^1.2.1": + version: 1.2.1 + resolution: "humanize-ms@npm:1.2.1" + dependencies: + ms: "npm:^2.0.0" + checksum: 10c0/f34a2c20161d02303c2807badec2f3b49cbfbbb409abd4f95a07377ae01cfe6b59e3d15ac609cffcd8f2521f0eb37b7e1091acf65da99aa2a4f1ad63c21e7e7a + languageName: node + linkType: hard + "iconv-lite@npm:0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" @@ -11295,7 +11652,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.1.1, ms@npm:^2.1.3": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 @@ -11581,6 +11938,13 @@ __metadata: languageName: node linkType: hard +"node-domexception@npm:1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10c0/5e5d63cda29856402df9472335af4bb13875e1927ad3be861dc5ebde38917aecbf9ae337923777af52a48c426b70148815e890a5d72760f1b4d758cc671b1a2b + languageName: node + linkType: hard + "node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -11830,6 +12194,24 @@ __metadata: languageName: node linkType: hard +"octokit@npm:^4.1.3": + version: 4.1.3 + resolution: "octokit@npm:4.1.3" + dependencies: + "@octokit/app": "npm:^15.1.6" + "@octokit/core": "npm:^6.1.5" + "@octokit/oauth-app": "npm:^7.1.6" + "@octokit/plugin-paginate-graphql": "npm:^5.2.4" + "@octokit/plugin-paginate-rest": "npm:^12.0.0" + "@octokit/plugin-rest-endpoint-methods": "npm:^14.0.0" + "@octokit/plugin-retry": "npm:^7.2.1" + "@octokit/plugin-throttling": "npm:^10.0.0" + "@octokit/request-error": "npm:^6.1.8" + "@octokit/types": "npm:^14.0.0" + checksum: 10c0/a35352dff1d7bacf8123e491489650c29b830a765f78113064edd6bd0aad0e58e8933874394892972fb8b0e43369b6f7a7b354426c61f4d9fc37408891fde582 + languageName: node + linkType: hard + "on-finished@npm:2.4.1, on-finished@npm:^2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -11877,6 +12259,31 @@ __metadata: languageName: node linkType: hard +"openai@npm:^4.98.0": + version: 4.98.0 + resolution: "openai@npm:4.98.0" + dependencies: + "@types/node": "npm:^18.11.18" + "@types/node-fetch": "npm:^2.6.4" + abort-controller: "npm:^3.0.0" + agentkeepalive: "npm:^4.2.1" + form-data-encoder: "npm:1.7.2" + formdata-node: "npm:^4.3.2" + node-fetch: "npm:^2.6.7" + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + bin: + openai: bin/cli + checksum: 10c0/399f07cd04c47d05be89cbcf3edb98650177ca09322ae664ade347bd56830cc0423834b4635341950bd9af59fdf203d3fad1de7927f4d8d2449b08c642c0ee3e + languageName: node + linkType: hard + "openapi-fetch@npm:^0.13.4": version: 0.13.5 resolution: "openapi-fetch@npm:0.13.5" @@ -11976,6 +12383,13 @@ __metadata: languageName: node linkType: hard +"parse-diff@npm:^0.11.1": + version: 0.11.1 + resolution: "parse-diff@npm:0.11.1" + checksum: 10c0/b8a488039f535e0ddaf1cfd4a13c2adc6d142f4e6263de6dc603f9762b59da89703a8f0dc8aacb7c7c66cdbdd2d075a57c63813fb74f66c9b8f5fe8ad2e89689 + languageName: node + linkType: hard + "parse-json@npm:^4.0.0": version: 4.0.0 resolution: "parse-json@npm:4.0.0" @@ -14587,6 +15001,13 @@ __metadata: languageName: node linkType: hard +"toad-cache@npm:^3.7.0": + version: 3.7.0 + resolution: "toad-cache@npm:3.7.0" + checksum: 10c0/7dae2782ee20b22c9798bb8b71dec7ec6a0091021d2ea9dd6e8afccab6b65b358fdba49a02209fac574499702e2c000660721516c87c2538d1b2c0ba03e8c0c3 + languageName: node + linkType: hard + "toidentifier@npm:1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" @@ -14920,6 +15341,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 10c0/bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501 + languageName: node + linkType: hard + "undici-types@npm:~6.19.2": version: 6.19.8 resolution: "undici-types@npm:6.19.8" @@ -15000,6 +15428,13 @@ __metadata: languageName: node linkType: hard +"universal-github-app-jwt@npm:^2.2.0": + version: 2.2.2 + resolution: "universal-github-app-jwt@npm:2.2.2" + checksum: 10c0/7ae5f031fb89c01a4407459b764c5e6341d725d436e1ceec161f9b754dd4883d9704cc8de53d5b6314b7e1bef8dbc7561799fc23001e706f213d468c17026fb6 + languageName: node + linkType: hard + "universal-user-agent@npm:^7.0.0, universal-user-agent@npm:^7.0.2": version: 7.0.2 resolution: "universal-user-agent@npm:7.0.2" @@ -15314,6 +15749,13 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:4.0.0-beta.3": + version: 4.0.0-beta.3 + resolution: "web-streams-polyfill@npm:4.0.0-beta.3" + checksum: 10c0/a9596779db2766990117ed3a158e0b0e9f69b887a6d6ba0779940259e95f99dc3922e534acc3e5a117b5f5905300f527d6fbf8a9f0957faf1d8e585ce3452e8e + languageName: node + linkType: hard + "web-vitals@npm:^4.2.4": version: 4.2.4 resolution: "web-vitals@npm:4.2.4" @@ -15652,7 +16094,7 @@ __metadata: languageName: node linkType: hard -"zod-to-json-schema@npm:^3.24.1": +"zod-to-json-schema@npm:^3.24.1, zod-to-json-schema@npm:^3.24.5": version: 3.24.5 resolution: "zod-to-json-schema@npm:3.24.5" peerDependencies: