diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0197c0ff..631b3004 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,4 @@ contact_links: - - name: 💡 Feature Request - url: https://github.com/sourcebot-dev/sourcebot/discussions/new?category=ideas - about: Suggest any ideas you have using our discussion forums. - - name: 🛟 Get Help - url: https://github.com/sourcebot-dev/sourcebot/discussions/new?category=support - about: If you can't get something to work the way you expect, open a question in our discussion forums. \ No newline at end of file + - name: 👾 Discord + url: https://discord.gg/f4Cbf3HT + about: Something else? Join the Discord! diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..4832e46d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,12 @@ +--- +name: "💡 Feature Request" +about: Suggest an idea for this project +title: "[FR] " +labels: enhancement +assignees: '' + +--- + + + + diff --git a/.github/ISSUE_TEMPLATE/get_help.md b/.github/ISSUE_TEMPLATE/get_help.md new file mode 100644 index 00000000..95693738 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/get_help.md @@ -0,0 +1,12 @@ +--- +name: "🛟 Get Help" +about: Something isn't working the way you expect +title: "" +labels: help wanted +assignees: '' + +--- + + + + diff --git a/.github/workflows/_gcp-deploy.yml b/.github/workflows/_gcp-deploy.yml index cee812a4..15fde89b 100644 --- a/.github/workflows/_gcp-deploy.yml +++ b/.github/workflows/_gcp-deploy.yml @@ -60,6 +60,8 @@ jobs: NEXT_PUBLIC_SENTRY_ENVIRONMENT=${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} NEXT_PUBLIC_SENTRY_WEBAPP_DSN=${{ vars.NEXT_PUBLIC_SENTRY_WEBAPP_DSN }} NEXT_PUBLIC_SENTRY_BACKEND_DSN=${{ vars.NEXT_PUBLIC_SENTRY_BACKEND_DSN }} + NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY=${{ vars.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY }} + NEXT_PUBLIC_LANGFUSE_BASE_URL=${{ vars.NEXT_PUBLIC_LANGFUSE_BASE_URL }} SENTRY_SMUAT=${{ secrets.SENTRY_SMUAT }} SENTRY_ORG=${{ vars.SENTRY_ORG }} SENTRY_WEBAPP_PROJECT=${{ vars.SENTRY_WEBAPP_PROJECT }} diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml index 50884bef..71bc9ef0 100644 --- a/.github/workflows/deploy-demo.yml +++ b/.github/workflows/deploy-demo.yml @@ -2,7 +2,7 @@ name: Deploy Demo on: push: - tags: ["v*.*.*"] + branches: ["main"] workflow_dispatch: jobs: diff --git a/.github/workflows/update-roadmap-released.yml b/.github/workflows/update-roadmap-released.yml new file mode 100644 index 00000000..b40955a1 --- /dev/null +++ b/.github/workflows/update-roadmap-released.yml @@ -0,0 +1,76 @@ +name: Update Roadmap Released + +on: + pull_request: + types: [closed] + workflow_dispatch: + schedule: + - cron: "0 */6 * * *" + +permissions: + pull-requests: read + contents: read + issues: write + +jobs: + update: + runs-on: ubuntu-latest + steps: + - name: Update "Released" section with last 10 merged PRs + uses: actions/github-script@v7 + env: + ROADMAP_ISSUE_NUMBER: "459" + with: + script: | + const issue_number = parseInt(process.env.ROADMAP_ISSUE_NUMBER, 10); + const {owner, repo} = context.repo; + + // Fetch more than 10, then sort by closed_at to be precise + const batchSize = 50; + const { data: prBatch } = await github.rest.pulls.list({ + owner, + repo, + state: "closed", + per_page: batchSize, + sort: "updated", + direction: "desc" + }); + + const last10 = prBatch + .filter(pr => pr.merged_at) // only merged PRs + .sort((a, b) => new Date(b.merged_at) - new Date(a.merged_at)) + .slice(0, 10); + + const list = last10.map(pr => `- #${pr.number}`).join("\n"); + + const start = ""; + const end = ""; + + const mergedUrl = `https://github.com/${owner}/${repo}/pulls?q=is%3Apr+is%3Amerged`; + const replacementBlock = [ + start, + "", + `10 most recent [merged PRs](${mergedUrl}):`, + "", + list, + "", + end + ].join("\n"); + + const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number }); + let body = issue.body || ""; + + if (body.includes(start) && body.includes(end)) { + const pattern = new RegExp(`${start}[\\s\\S]*?${end}`); + body = body.replace(pattern, replacementBlock); + } else { + core.setFailed('Missing RELEASED markers in roadmap issue body. Please add and to the issue.'); + return; + } + + await github.rest.issues.update({ + owner, + repo, + issue_number, + body + }); \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 169b2e6a..89530b9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,144 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Added support for passing db connection url as seperate `DATABASE_HOST`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `DATABASE_NAME`, and `DATABASE_ARGS` env vars. [#545](https://github.com/sourcebot-dev/sourcebot/pull/545) + +## [4.7.3] - 2025-09-29 + +### Fixed +- Manually pass auth token for ado server deployments. [#543](https://github.com/sourcebot-dev/sourcebot/pull/543) + +## [4.7.2] - 2025-09-22 + +### Fixed +- Fix support email. [#529](https://github.com/sourcebot-dev/sourcebot/pull/529) + +### Added +- [Experimental][Sourcebot EE] Added permission syncing repository Access Control Lists (ACLs) between Sourcebot and GitHub. [#508](https://github.com/sourcebot-dev/sourcebot/pull/508) + +### Changed +- Improved repository query performance by adding db indices. [#526](https://github.com/sourcebot-dev/sourcebot/pull/526) +- Improved repository query performance by removing JOIN on `Connection` table. [#527](https://github.com/sourcebot-dev/sourcebot/pull/527) +- Changed repo carousel and repo list links to redirect to the file browser. [#528](https://github.com/sourcebot-dev/sourcebot/pull/528) +- Changed file headers, files/directories in file tree, and reference list buttons into links. [#532](https://github.com/sourcebot-dev/sourcebot/pull/532) + +## [4.7.1] - 2025-09-19 + +### Fixed +- Fixed sourcebot not pulling github forked repos [#499](https://github.com/sourcebot-dev/sourcebot/pull/499) +- Fixed azure devop cloud pat issue [#524](https://github.com/sourcebot-dev/sourcebot/pull/524) + +## [4.7.0] - 2025-09-17 + +### Added +- Added fallback to default the Node.JS AWS SDK's `fromNodeProviderChain` when no credentials are provided for a bedrock config. [#513](https://github.com/sourcebot-dev/sourcebot/pull/513) +- Added support for Azure Devops support. [#514](https://github.com/sourcebot-dev/sourcebot/pull/514) + +### Fixed +- Fixed "At least one project, user, or group must be specified" for GitLab configs with `all` in web configurator. [#512](https://github.com/sourcebot-dev/sourcebot/pull/512) +- Fixed zoekt indexing failing with pipe in branch/tag names [#506](https://github.com/sourcebot-dev/sourcebot/pull/506) +- Removed deprecated connection creation/edit UI [#515](https://github.com/sourcebot-dev/sourcebot/pull/515) + +## [4.6.8] - 2025-09-15 + +### Fixed +- Fixed Bitbucket Cloud pagination not working beyond first page. [#295](https://github.com/sourcebot-dev/sourcebot/issues/295) +- Fixed search bar line wrapping. [#501](https://github.com/sourcebot-dev/sourcebot/pull/501) +- Fixed carousel perf issues. [#507](https://github.com/sourcebot-dev/sourcebot/pull/507) + +## [4.6.7] - 2025-09-08 + +### Added +- Added `exclude.userOwnedProjects` setting to GitLab configs. [#498](https://github.com/sourcebot-dev/sourcebot/pull/498) + +### Fixed +- Fixed "couldn't find remote ref HEAD" errors when re-indexing certain repositories. [#497](https://github.com/sourcebot-dev/sourcebot/pull/497) + +### Changed +- Disable page scroll when using arrow keys on search suggestions box. [#493](https://github.com/sourcebot-dev/sourcebot/pull/493) + +## [4.6.6] - 2025-09-04 + +### Added +- Added support for specifying query params for openai compatible language models. [#490](https://github.com/sourcebot-dev/sourcebot/pull/490) + +### Fixed +- Fix issue where zoekt was failing to index repositories due to `HEAD` pointing to a branch that does not exist. [#488](https://github.com/sourcebot-dev/sourcebot/pull/488) + +## [4.6.5] - 2025-09-02 + +### Fixed +- Remove setting `remote.origin.url` for remote git repositories. [#483](https://github.com/sourcebot-dev/sourcebot/pull/483) +- Fix error when navigating to paths with percentage symbols. [#485](https://github.com/sourcebot-dev/sourcebot/pull/485) + +### Changed +- Updated NextJS to version 15. [#477](https://github.com/sourcebot-dev/sourcebot/pull/477) +- Add `sessionToken` as optional Bedrock configuration parameter. [#478](https://github.com/sourcebot-dev/sourcebot/pull/478) + +## [4.6.4] - 2025-08-11 + +### Added +- Added multi-branch indexing support for Gerrit. [#433](https://github.com/sourcebot-dev/sourcebot/pull/433) +- [ask sb] Added `reasoningEffort` option to OpenAI provider. [#446](https://github.com/sourcebot-dev/sourcebot/pull/446) +- [ask db] Added `headers` option to all providers. [#449](https://github.com/sourcebot-dev/sourcebot/pull/449) + +### Fixed +- Removed prefix from structured log output. [#443](https://github.com/sourcebot-dev/sourcebot/pull/443) +- [ask sb] Fixed long generation times for first message in a chat thread. [#447](https://github.com/sourcebot-dev/sourcebot/pull/447) + +### Changed +- Bumped AI SDK and associated packages version. [#444](https://github.com/sourcebot-dev/sourcebot/pull/444) + +## [4.6.3] - 2025-08-04 + +### Fixed +- Fixed issue where `users` specified in a GitHub config were not getting picked up when a `token` is also specified. [#428](https://github.com/sourcebot-dev/sourcebot/pull/428) + +### Added +- [ask sb] Added OpenAI Compatible Language Provider. [#424](https://github.com/sourcebot-dev/sourcebot/pull/424) + +## [4.6.2] - 2025-07-31 + +### Changed +- Bumped AI SDK and associated packages version. [#417](https://github.com/sourcebot-dev/sourcebot/pull/417) + +### Fixed +- [ask sb] Fixed "413 content too large" error when starting a new chat with many repos selected. [#416](https://github.com/sourcebot-dev/sourcebot/pull/416) + +### Added +- [ask sb] PostHog telemetry for chat thread creation. [#418](https://github.com/sourcebot-dev/sourcebot/pull/418) + +## [4.6.1] - 2025-07-29 + +### Added +- Add search context to ask sourcebot context selector. [#397](https://github.com/sourcebot-dev/sourcebot/pull/397) +- Add ability to include/exclude connection in search context. [#399](https://github.com/sourcebot-dev/sourcebot/pull/399) +- Search context refactor to search scope and demo card UI changes. [#405](https://github.com/sourcebot-dev/sourcebot/pull/405) +- Add GitHub star toast. [#409](https://github.com/sourcebot-dev/sourcebot/pull/409) +- Added a onboarding modal when first visiting the homepage when `ask` mode is selected. [#408](https://github.com/sourcebot-dev/sourcebot/pull/408) +- [ask sb] Added `searchReposTool` and `listAllReposTool`. [#400](https://github.com/sourcebot-dev/sourcebot/pull/400) + +### Fixed +- Fixed multiple writes race condition on config file watcher. [#398](https://github.com/sourcebot-dev/sourcebot/pull/398) + +### Changed +- Bumped AI SDK and associated packages version. [#404](https://github.com/sourcebot-dev/sourcebot/pull/404) +- Bumped form-data package version. [#407](https://github.com/sourcebot-dev/sourcebot/pull/407) +- Bumped next version. [#406](https://github.com/sourcebot-dev/sourcebot/pull/406) +- [ask sb] Improved search code tool with filter options. [#400](https://github.com/sourcebot-dev/sourcebot/pull/400) +- [ask sb] Removed search scope constraint. [#400](https://github.com/sourcebot-dev/sourcebot/pull/400) +- Update README with new features and videos. [#410](https://github.com/sourcebot-dev/sourcebot/pull/410) +- [ask sb] Add back search scope requirement and other UI changes. [#411](https://github.com/sourcebot-dev/sourcebot/pull/411) + +## [4.6.0] - 2025-07-25 + +### Added +- Introducing Ask Sourcebot - ask natural langauge about your codebase. Get back comprehensive Markdown responses with inline citations back to the code. Bring your own LLM api key. [#392](https://github.com/sourcebot-dev/sourcebot/pull/392) + +### Fixed +- Fixed onboarding infinite loop when GCP IAP Auth is enabled. [#381](https://github.com/sourcebot-dev/sourcebot/pull/381) + ## [4.5.3] - 2025-07-20 ### Changed @@ -223,7 +361,7 @@ Sourcebot v3 is here and brings a number of structural changes to the tool's fou ### Removed - [**Breaking Change**] Removed `db.json` in favour of a Postgres database for transactional workloads. See the [architecture overview](https://docs.sourcebot.dev/self-hosting/overview#architecture). -- [**Breaking Change**] Removed local folder & arbitrary .git repo support. If your deployment depended on these features, please [open a discussion](https://github.com/sourcebot-dev/sourcebot/discussions/categories/support) and let us know. +- [**Breaking Change**] Removed local folder & arbitrary .git repo support. If your deployment depended on these features, please [open a issue](https://github.com/sourcebot-dev/sourcebot/issues/new?template=get_help.md) and let us know. - [**Breaking Chnage**] Removed ability to specify a `token` as a string literal from the schema. - [**Breaking Change**] Removed support for `DOMAIN_SUB_PATH` configuration. diff --git a/Dockerfile b/Dockerfile index 24f542b1..010f5940 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,8 @@ ARG NEXT_PUBLIC_SENTRY_ENVIRONMENT ARG NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT ARG NEXT_PUBLIC_SENTRY_WEBAPP_DSN ARG NEXT_PUBLIC_SENTRY_BACKEND_DSN +ARG NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY +ARG NEXT_PUBLIC_LANGFUSE_BASE_URL FROM node:20-alpine3.19 AS node-alpine FROM golang:1.23.4-alpine3.19 AS go-alpine @@ -67,6 +69,10 @@ ARG NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT ENV NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT=$NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT ARG NEXT_PUBLIC_SENTRY_WEBAPP_DSN ENV NEXT_PUBLIC_SENTRY_WEBAPP_DSN=$NEXT_PUBLIC_SENTRY_WEBAPP_DSN +ARG NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY +ENV NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY=$NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY +ARG NEXT_PUBLIC_LANGFUSE_BASE_URL +ENV NEXT_PUBLIC_LANGFUSE_BASE_URL=$NEXT_PUBLIC_LANGFUSE_BASE_URL # To upload source maps to Sentry, we need to set the following build-time args. # It's important that we don't set these for oss builds, otherwise the Sentry @@ -164,6 +170,10 @@ ARG NEXT_PUBLIC_SENTRY_WEBAPP_DSN ENV NEXT_PUBLIC_SENTRY_WEBAPP_DSN=$NEXT_PUBLIC_SENTRY_WEBAPP_DSN ARG NEXT_PUBLIC_SENTRY_BACKEND_DSN ENV NEXT_PUBLIC_SENTRY_BACKEND_DSN=$NEXT_PUBLIC_SENTRY_BACKEND_DSN +ARG NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY +ENV NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY=$NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY +ARG NEXT_PUBLIC_LANGFUSE_BASE_URL +ENV NEXT_PUBLIC_LANGFUSE_BASE_URL=$NEXT_PUBLIC_LANGFUSE_BASE_URL # ----------- RUN echo "Sourcebot Version: $NEXT_PUBLIC_SOURCEBOT_VERSION" @@ -175,7 +185,6 @@ ENV DATA_DIR=/data ENV DATA_CACHE_DIR=$DATA_DIR/.sourcebot ENV DATABASE_DATA_DIR=$DATA_CACHE_DIR/db ENV REDIS_DATA_DIR=$DATA_CACHE_DIR/redis -ENV DATABASE_URL="postgresql://postgres@localhost:5432/sourcebot" ENV REDIS_URL="redis://localhost:6379" ENV SRC_TENANT_ENFORCEMENT_MODE=strict ENV SOURCEBOT_PUBLIC_KEY_PATH=/app/public.pem diff --git a/LICENSE.md b/LICENSE.md index 93c14254..315bde81 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,7 +2,7 @@ Copyright (c) 2025 Taqla Inc. Portions of this software are licensed as follows: -- All content that resides under the "ee/", "packages/web/src/ee/", and "packages/shared/src/ee/" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE". +- All content that resides under the "ee/", "packages/web/src/ee/", "packages/backend/src/ee/", and "packages/shared/src/ee/" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE". - All third party components incorporated into the Sourcebot Software are licensed under the original license provided by the owner of the applicable component. - Content outside of the above mentioned directories or restrictions above is available under the "Functional Source License" as defined below. diff --git a/README.md b/README.md index 40193362..de9b664f 100644 --- a/README.md +++ b/README.md @@ -12,22 +12,19 @@ Self Host · - Demo + Public Demo
Docs · - Report Bug · - Feature Request · - Changelog · - Contact · + Roadmap · + Report Bug · + Feature Request · + Changelog

- Sourcebot uses Github Discussions for Support and Feature Requests. -
-
@@ -37,30 +34,45 @@

-

- -

-# About +Sourcebot is a self-hosted tool that helps you understand your codebase. -Sourcebot lets you index all your repos and branches across multiple code hosts (GitHub, GitLab, Bitbucket, Gitea, or Gerrit) and search through them using a blazingly fast interface. +- **Ask Sourcebot:** Ask questions about your codebase and have Sourcebot provide detailed answers grounded with inline citations. +- **Code search:** Search and navigate across all your repos and branches, no matter where they’re hosted. -https://github.com/user-attachments/assets/ced355f3-967e-4f37-ae6e-74ab8c06b9ec +Try it out in our [public demo](https://demo.sourcebot.dev)! +https://github.com/user-attachments/assets/ed66a622-e38f-4947-a531-86df1e1e0218 -## Features -- 💻 **One-command deployment**: Get started instantly using Docker on your own machine. -- 🔍 **Multi-repo search**: Index and search through multiple public and private repositories and branches on GitHub, GitLab, Bitbucket, Gitea, or Gerrit. -- ⚡**Lightning fast performance**: Built on top of the powerful [Zoekt](https://github.com/sourcegraph/zoekt) search engine. -- 🎨 **Modern web app**: Enjoy a sleek interface with features like syntax highlighting, light/dark mode, and vim-style navigation -- 📂 **Full file visualization**: Instantly view the entire file when selecting any search result. +# Features +![Sourcebot Features](https://github.com/user-attachments/assets/3aed7348-7aeb-4af3-89da-b617c3db2e02) -You can try out our public hosted demo [here](https://demo.sourcebot.dev)! +## Ask Sourcebot +Ask Sourcebot gives you the ability to ask complex questions about your codebase in natural language. + +It uses Sourcebot's existing code search and navigation tools to allow reasoning models to search your code, follow code nav references, and provide an answer that's rich with inline citations and navigable code snippets. + +https://github.com/user-attachments/assets/8212cd16-683f-468f-8ea5-67455c0931e2 + +## Code Search +Search across all your repos/branches across any code host platform. Blazingly fast, and supports regular expressions, repo/language search filters, boolean logic, and more. + +https://github.com/user-attachments/assets/3b381452-d329-4949-b6f2-2fc38952e481 + +## Code Navigation +IDE-level code navigation (goto definition and find references) across all your repos. + +https://github.com/user-attachments/assets/e2da2829-71cc-40af-98b4-7ba52e945530 + +## Built-in File Explorer +Explore every file across all of your repos. Modern UI with syntax highlighting, file tree, code navigation, etc. + +https://github.com/user-attachments/assets/31ec0669-707d-4e03-b511-1bc33d44197a # Deploy Sourcebot -Sourcebot can be deployed in seconds using our official docker image. Visit our [docs](https://docs.sourcebot.dev/self-hosting/overview) for more information. +Sourcebot can be deployed in seconds using our official docker image. Visit our [docs](https://docs.sourcebot.dev/docs/deployment-guide) for more information. 1. Create a config ```sh @@ -102,10 +114,10 @@ docker run \
-3. Start searching at `http://localhost:3000` +3. Visit `http://localhost:3000` to start using Sourcebot
-To learn how to configure Sourcebot to index your own repos, please refer to our [docs](https://docs.sourcebot.dev/self-hosting/overview). +To configure Sourcebot (index your own repos, connect your LLMs, etc), check out our [docs](https://docs.sourcebot.dev/docs/configuration/config-file). > [!NOTE] > Sourcebot collects anonymous usage data by default to help us improve the product. No sensitive data is collected, but if you'd like to disable this you can do so by setting the `SOURCEBOT_TELEMETRY_DISABLED` environment diff --git a/docs/docs.json b/docs/docs.json index e0834f28..3a2cb43f 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -28,16 +28,25 @@ "group": "Features", "pages": [ { - "group": "Search", + "group": "Code Search", "pages": [ + "docs/features/search/overview", "docs/features/search/syntax-reference", "docs/features/search/multi-branch-indexing", "docs/features/search/search-contexts" ] }, + { + "group": "Ask Sourcebot", + "pages": [ + "docs/features/ask/overview", + "docs/features/ask/add-model-providers" + ] + }, "docs/features/code-navigation", "docs/features/analytics", "docs/features/mcp-server", + "docs/features/permission-syncing", { "group": "Agents", "tag": "experimental", @@ -51,6 +60,7 @@ { "group": "Configuration", "pages": [ + "docs/configuration/config-file", { "group": "Indexing your code", "pages": [ @@ -59,6 +69,8 @@ "docs/connections/gitlab", "docs/connections/bitbucket-cloud", "docs/connections/bitbucket-data-center", + "docs/connections/ado-cloud", + "docs/connections/ado-server", "docs/connections/gitea", "docs/connections/gerrit", "docs/connections/generic-git-host", @@ -66,8 +78,7 @@ "docs/connections/request-new" ] }, - "docs/license-key", - "docs/configuration/environment-variables", + "docs/configuration/language-model-providers", { "group": "Authentication", "pages": [ @@ -78,6 +89,8 @@ "docs/configuration/auth/faq" ] }, + "docs/configuration/environment-variables", + "docs/license-key", "docs/configuration/transactional-emails", "docs/configuration/structured-logging", "docs/configuration/audit-logs" @@ -97,9 +110,14 @@ "href": "https://sourcebot.dev/changelog", "icon": "list-check" }, + { + "anchor": "Roadmap", + "href": "https://github.com/sourcebot-dev/sourcebot/issues/459", + "icon": "map" + }, { "anchor": "Support", - "href": "https://github.com/sourcebot-dev/sourcebot/discussions/categories/support", + "href": "https://github.com/sourcebot-dev/sourcebot/issues/new?template=get_help.md", "icon": "life-ring" } ] diff --git a/docs/docs/configuration/auth/faq.mdx b/docs/docs/configuration/auth/faq.mdx index 5d37bc66..f7192f4f 100644 --- a/docs/docs/configuration/auth/faq.mdx +++ b/docs/docs/configuration/auth/faq.mdx @@ -41,6 +41,4 @@ This page covers a range of frequently asked questions about Sourcebot's built-i - -Have a question that's not answered here? Submit it on our [GitHub discussions](https://github.com/sourcebot-dev/sourcebot/discussions) -page and we'll get back to you as soon as we can! \ No newline at end of file +Have a question that's not answered here? Submit an issue on [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose) and we'll get back to you as soon as we can. \ No newline at end of file diff --git a/docs/docs/configuration/auth/overview.mdx b/docs/docs/configuration/auth/overview.mdx index 732fef35..2544c013 100644 --- a/docs/docs/configuration/auth/overview.mdx +++ b/docs/docs/configuration/auth/overview.mdx @@ -25,4 +25,4 @@ Sourcebot's built-in authentication system gates your deployment, and allows adm # Troubleshooting - If you experience issues logging in, logging out, or accessing an organization you should have access to, try clearing your cookies & performing a full page refresh (`Cmd/Ctrl + Shift + R` on most browsers). -- Still not working? Reach out to us on our [discord](https://discord.com/invite/6Fhp27x7Pb) or [github discussions](https://github.com/sourcebot-dev/sourcebot/discussions) \ No newline at end of file +- Still not working? Reach out to us on our [discord](https://discord.com/invite/6Fhp27x7Pb) or [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose) \ No newline at end of file diff --git a/docs/docs/configuration/config-file.mdx b/docs/docs/configuration/config-file.mdx new file mode 100644 index 00000000..58d7a1b1 --- /dev/null +++ b/docs/docs/configuration/config-file.mdx @@ -0,0 +1,51 @@ +--- +title: Config File +sidebarTitle: Config file +--- + +When self-hosting Sourcebot, you **must** provide it a config file. This is done by defining a config file in a volume that's mounted to Sourcebot, and providing the path to this +file in the `CONFIG_PATH` environment variable. For example: + +```bash icon="terminal" Passing in a CONFIG_PATH to Sourcebot +docker run \ + -v $(pwd)/config.json:/data/config.json \ + -e CONFIG_PATH=/data/config.json \ + ... \ # other options + ghcr.io/sourcebot-dev/sourcebot:latest +``` + +The config file tells Sourcebot which repos to index, what language models to use, and various other settings as defined in the [schema](#config-file-schema). + +# Config File Schema + +The config file you provide Sourcebot must follow the [schema](https://github.com/sourcebot-dev/sourcebot/blob/main/schemas/v3/index.json). This schema consists of the following properties: + +- [Connections](/docs/connections/overview) (`connections`): Defines a set of connections that tell Sourcebot which repos to index and from where +- [Language Models](/docs/configuration/language-model-providers) (`models`): Defines a set of language model providers for use with [Ask Sourcebot](/docs/features/ask) +- [Settings](#settings) (`settings`): Additional settings to tweak your Sourcebot deployment +- [Search Contexts](/docs/features/search/search-contexts) (`contexts`): Groupings of repos that you can search against + +# Config File Syncing + +Sourcebot syncs the config file on startup, and automatically whenever a change is detected. + +# Settings + +The following are settings that can be provided in your config file to modify Sourcebot's behavior + +| Setting | Type | Default | Minimum | Description / Notes | +|-------------------------------------------------|---------|------------|---------|----------------------------------------------------------------------------------------| +| `maxFileSize` | number | 2 MB | 1 | Maximum size (bytes) of a file to index. Files exceeding this are skipped. | +| `maxTrigramCount` | number | 20 000 | 1 | Maximum trigrams per document. Larger files are skipped. | +| `reindexIntervalMs` | number | 1 hour | 1 | Interval at which all repositories are re‑indexed. | +| `resyncConnectionIntervalMs` | number | 24 hours | 1 | Interval for checking connections that need re‑syncing. | +| `resyncConnectionPollingIntervalMs` | number | 1 second | 1 | DB polling rate for connections that need re‑syncing. | +| `reindexRepoPollingIntervalMs` | number | 1 second | 1 | DB polling rate for repos that should be re‑indexed. | +| `maxConnectionSyncJobConcurrency` | number | 8 | 1 | Concurrent connection‑sync jobs. | +| `maxRepoIndexingJobConcurrency` | number | 8 | 1 | Concurrent repo‑indexing jobs. | +| `maxRepoGarbageCollectionJobConcurrency` | number | 8 | 1 | Concurrent repo‑garbage‑collection jobs. | +| `repoGarbageCollectionGracePeriodMs` | number | 10 seconds | 1 | Grace period to avoid deleting shards while loading. | +| `repoIndexTimeoutMs` | number | 2 hours | 1 | Timeout for a single repo‑indexing run. | +| `enablePublicAccess` **(deprecated)** | boolean | false | — | Use the `FORCE_ENABLE_ANONYMOUS_ACCESS` environment variable instead. | +| `experiment_repoDrivenPermissionSyncIntervalMs` | number | 24 hours | 1 | Interval at which the repo permission syncer should run. | +| `experiment_userDrivenPermissionSyncIntervalMs` | number | 24 hours | 1 | Interval at which the user permission syncer should run. | diff --git a/docs/docs/configuration/environment-variables.mdx b/docs/docs/configuration/environment-variables.mdx index d6aab9eb..d49073fd 100644 --- a/docs/docs/configuration/environment-variables.mdx +++ b/docs/docs/configuration/environment-variables.mdx @@ -19,7 +19,7 @@ The following environment variables allow you to configure your Sourcebot deploy | `DATA_CACHE_DIR` | `$DATA_DIR/.sourcebot` |

The root data directory in which all data written to disk by Sourcebot will be located.

| | `DATA_DIR` | `/data` |

The directory within the container to store all persistent data. Typically, this directory will be volume mapped such that data is persisted across container restarts (e.g., `docker run -v $(pwd):/data`)

| | `DATABASE_DATA_DIR` | `$DATA_CACHE_DIR/db` |

The data directory for the default Postgres database.

| -| `DATABASE_URL` | `postgresql://postgres@ localhost:5432/sourcebot` |

Connection string of your Postgres database. By default, a Postgres database is automatically provisioned at startup within the container.

If you'd like to use a non-default schema, you can provide it as a parameter in the database url

| +| `DATABASE_URL` | `postgresql://postgres@ localhost:5432/sourcebot` |

Connection string of your Postgres database. By default, a Postgres database is automatically provisioned at startup within the container.

If you'd like to use a non-default schema, you can provide it as a parameter in the database url.

You can also use `DATABASE_HOST`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `DATABASE_NAME`, and `DATABASE_ARGS` to construct the database url.

| | `EMAIL_FROM_ADDRESS` | `-` |

The email address that transactional emails will be sent from. See [this doc](/docs/configuration/transactional-emails) for more info.

| | `FORCE_ENABLE_ANONYMOUS_ACCESS` | `false` |

When enabled, [anonymous access](/docs/configuration/auth/access-settings#anonymous-access) to the organization will always be enabled

| `REDIS_DATA_DIR` | `$DATA_CACHE_DIR/redis` |

The data directory for the default Redis instance.

| @@ -59,6 +59,7 @@ The following environment variables allow you to configure your Sourcebot deploy | `AUTH_EE_OKTA_ISSUER` | `-` |

The issuer URL for Okta SSO authentication.

| | `AUTH_EE_GCP_IAP_ENABLED` | `false` |

When enabled, allows Sourcebot to automatically register/login from a successful GCP IAP redirect

| | `AUTH_EE_GCP_IAP_AUDIENCE` | - |

The GCP IAP audience to use when verifying JWT tokens. Must be set to enable GCP IAP JIT provisioning

| +| `EXPERIMENT_EE_PERMISSION_SYNC_ENABLED` | `false` |

Enables [permission syncing](/docs/features/permission-syncing).

| ### Review Agent Environment Variables diff --git a/docs/docs/configuration/language-model-providers.mdx b/docs/docs/configuration/language-model-providers.mdx new file mode 100644 index 00000000..606f4d31 --- /dev/null +++ b/docs/docs/configuration/language-model-providers.mdx @@ -0,0 +1,373 @@ +--- +title: Language Model Providers +sidebarTitle: Language model providers +--- + +import LanguageModelSchema from '/snippets/schemas/v3/languageModel.schema.mdx' + + +Looking to self-host your own model? Check out the [OpenAI Compatible](#openai-compatible) provider. + + +To use [Ask Sourcebot](/docs/features/ask) you must define at least one Language Model Provider. These providers are defined within the [config file](/docs/configuration/config-file) you +provide Sourcebot. + + +```json wrap icon="code" Example config with language model provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + // 1. Google Vertex config for Gemini 2.5 Pro + { + "provider": "google-vertex", + "model": "gemini-2.5-pro", + "displayName": "Gemini 2.5 Pro", + "project": "sourcebot", + "credentials": { + "env": "GOOGLE_APPLICATION_CREDENTIALS" + } + }, + // 2. OpenAI config for o3 + { + "provider": "openai", + "model": "o3", + "displayName": "o3", + "token": { + "env": "OPENAI_API_KEY" + } + } + ] +} +``` + +# Supported Providers + +Sourcebot uses the [Vercel AI SDK](https://ai-sdk.dev/docs/introduction), so it can integrate with any provider the SDK supports. If you don't see your provider below please submit +a [feature request](https://github.com/sourcebot-dev/sourcebot/issues/new?template=feature_request.md). + +For a detailed description of all the providers, please refer to the [schema](https://github.com/sourcebot-dev/sourcebot/blob/main/schemas/v3/languageModel.json). + +Any parameter defined using `env` will read the value from the corresponding environment variable you provide Sourcebot + +### Amazon Bedrock + +[Vercel AI SDK Amazon Bedrock Docs](https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock) + +```json wrap icon="code" Example config with Amazon Bedrock provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + "provider": "amazon-bedrock", + "model": "YOUR_MODEL_HERE", + "displayName": "OPTIONAL_DISPLAY_NAME", + "accessKeyId": { + "env": "AWS_ACCESS_KEY_ID" + }, + "accessKeySecret": { + "env": "AWS_SECRET_ACCESS_KEY" + }, + "sessionToken": { + "env": "AWS_SESSION_TOKEN" + }, + "region": "YOUR_REGION_HERE", // defaults to the AWS_REGION env var if not set + "baseUrl": "OPTIONAL_BASE_URL" + } + ] +} +``` + +### Anthropic + +[Vercel AI SDK Anthropic Docs](https://ai-sdk.dev/providers/ai-sdk-providers/anthropic) + +```json wrap icon="code" Example config with Anthropic provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + "provider": "anthropic", + "model": "YOUR_MODEL_HERE", + "displayName": "OPTIONAL_DISPLAY_NAME", + "token": { + "env": "ANTHROPIC_API_KEY" + }, + "baseUrl": "OPTIONAL_BASE_URL" + } + ] +} +``` + +### Azure OpenAI + +[Vercel AI SDK Azure OpenAI Docs](https://ai-sdk.dev/providers/ai-sdk-providers/azure) + +```json wrap icon="code" Example config with Azure AI provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + "provider": "azure", + "model": "YOUR_MODEL_HERE", + "displayName": "OPTIONAL_DISPLAY_NAME", + "resourceName": "YOUR_RESOURCE_NAME", // defaults to the AZURE_RESOURCE_NAME env var if not set + "apiVersion": "OPTIONAL_API_VERSION", // defailts to 'preview' if not set + "token": { + "env": "AZURE_API_KEY" + }, + "baseUrl": "OPTIONAL_BASE_URL" + } + ] +} +``` + +### Deepseek + +[Vercel AI SDK Deepseek Docs](https://ai-sdk.dev/providers/ai-sdk-providers/deepseek) + +```json wrap icon="code" Example config with Deepseek provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + "provider": "deepseek", + "model": "YOUR_MODEL_HERE", + "displayName": "OPTIONAL_DISPLAY_NAME", + "token": { + "env": "DEEPSEEK_API_KEY" + }, + "baseUrl": "OPTIONAL_BASE_URL" + } + ] +} +``` + +### Google Generative AI + +[Vercel AI SDK Google Generative AI Docs](https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai) + +```json wrap icon="code" Example config with Google Generative AI provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + "provider": "google-generative-ai", + "model": "YOUR_MODEL_HERE", + "displayName": "OPTIONAL_DISPLAY_NAME", + "token": { + "env": "GOOGLE_GENERATIVE_AI_API_KEY" + }, + "baseUrl": "OPTIONAL_BASE_URL" + } + ] +} +``` + +### Google Vertex + +If you're using an Anthropic model on Google Vertex, you must define a [Google Vertex Anthropic](#google-vertex-anthropic) provider instead +The `credentials` paramater here expects a **path** to a [credentials](https://console.cloud.google.com/apis/credentials) file. This file **must be in a volume mounted by Sourcebot** for it to be readable. + +[Vercel AI SDK Google Vertex AI Docs](https://ai-sdk.dev/providers/ai-sdk-providers/google-vertex) + +```json wrap icon="code" Example config with Google Vertex provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + "provider": "google-vertex", + "model": "YOUR_MODEL_HERE", + "displayName": "OPTIONAL_DISPLAY_NAME", + "project": "YOUR_PROJECT_ID", // defaults to the GOOGLE_VERTEX_PROJECT env var if not set + "region": "YOUR_REGION_HERE", // defaults to the GOOGLE_VERTEX_REGION env var if not set + "credentials": { + "env": "GOOGLE_APPLICATION_CREDENTIALS" + }, + "baseUrl": "OPTIONAL_BASE_URL" + } + ] +} +``` + +### Google Vertex Anthropic + +The `credentials` paramater here expects a **path** to a [credentials](https://console.cloud.google.com/apis/credentials) file. This file **must be in a volume mounted by Sourcebot** for it to be readable. + + +[Vercel AI SDK Google Vertex Anthropic Docs](https://ai-sdk.dev/providers/ai-sdk-providers/google-vertex#google-vertex-anthropic-provider-usage) + +```json wrap icon="code" Example config with Google Vertex Anthropic provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + "provider": "google-vertex-anthropic", + "model": "YOUR_MODEL_HERE", + "displayName": "OPTIONAL_DISPLAY_NAME", + "project": "YOUR_PROJECT_ID", // defaults to the GOOGLE_VERTEX_PROJECT env var if not set + "region": "YOUR_REGION_HERE", // defaults to the GOOGLE_VERTEX_REGION env var if not set + "credentials": { + "env": "GOOGLE_APPLICATION_CREDENTIALS" + }, + "baseUrl": "OPTIONAL_BASE_URL" + } + ] +} +``` + +### Mistral + +[Vercel AI SDK Mistral Docs](https://ai-sdk.dev/providers/ai-sdk-providers/mistral) + +```json wrap icon="code" Example config with Mistral provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + "provider": "mistral", + "model": "YOUR_MODEL_HERE", + "displayName": "OPTIONAL_DISPLAY_NAME", + "token": { + "env": "MISTRAL_API_KEY" + }, + "baseUrl": "OPTIONAL_BASE_URL" + } + ] +} +``` + +### OpenAI + +[Vercel AI SDK OpenAI Docs](https://ai-sdk.dev/providers/ai-sdk-providers/openai) + +```json wrap icon="code" Example config with OpenAI provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + "provider": "openai", + "model": "YOUR_MODEL_HERE", + "displayName": "OPTIONAL_DISPLAY_NAME", + "token": { + "env": "OPENAI_API_KEY" + }, + "baseUrl": "OPTIONAL_BASE_URL", + "reasoningEffort": "OPTIONAL_REASONING_EFFORT" // defaults to "medium" + } + ] +} +``` + +### OpenAI Compatible + +[Vercel AI SDK OpenAI Compatible Docs](https://ai-sdk.dev/providers/openai-compatible-providers) + +The OpenAI compatible provider allows you to use any model that is compatible with the OpenAI [Chat Completions API](https://github.com/ollama/ollama/blob/main/docs/openai.md). This includes self-hosted tools like [Ollama](https://ollama.ai/) and [llama.cpp](https://github.com/ggerganov/llama.cpp). + +```json wrap icon="code" Example config with OpenAI Compatible provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + "provider": "openai-compatible", + "baseUrl": "BASE_URL_HERE", + "model": "YOUR_MODEL_HERE", + "displayName": "OPTIONAL_DISPLAY_NAME", + "token": { + "env": "OPTIONAL_API_KEY" + }, + // Optional query parameters can be passed in the request url as: + "queryParams": { + // raw string values + "optional-query-param": "foo", + // or as environment variables + "optional-query-param-secret": { + "env": "MY_SECRET_ENV_VAR" + } + } + } + ] +} +``` + + +- When using [llama.cpp](https://github.com/ggml-org/llama.cpp), if you hit "Failed after 3 attempts. Last error: tools param requires --jinja flag", add the `--jinja` flag to your `llama-server` command. + + +### OpenRouter + +[Vercel AI SDK OpenRouter Docs](https://ai-sdk.dev/providers/community-providers/openrouter) + +```json wrap icon="code" Example config with OpenRouter provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + "provider": "openai", + "model": "YOUR_MODEL_HERE", + "displayName": "OPTIONAL_DISPLAY_NAME", + "token": { + "env": "OPENROUTER_API_KEY" + }, + "baseUrl": "OPTIONAL_BASE_URL" + } + ] +} +``` + +### xAI + +[Vercel AI SDK xAI Docs](https://ai-sdk.dev/providers/ai-sdk-providers/xai) + +```json wrap icon="code" Example config with xAI provider +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + "provider": "xai", + "model": "YOUR_MODEL_HERE", + "displayName": "OPTIONAL_DISPLAY_NAME", + "token": { + "env": "XAI_API_KEY" + }, + "baseUrl": "OPTIONAL_BASE_URL" + } + ] +} +``` + +# Custom headers + +You can pass custom headers to the language model provider by using the `headers` parameter. Header values can either be a string or a environment variable. Headers are supported for all providers. + +```json wrap icon="code" Example config with custom headers +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json", + "models": [ + { + // ... provider, model, displayName, etc... + + // Key-value pairs of headers + "headers": { + // Header values can be passed as a environment variable... + "my-secret-header": { + "env": "MY_SECRET_HEADER_ENV_VAR" + }, + + // ... or directly as a string. + "my-non-secret-header": "plaintextvalue" + } + } + ] +} +``` + + +# Schema reference + + +[schemas/v3/languageModel.json](https://github.com/sourcebot-dev/sourcebot/blob/main/schemas/v3/languageModel.json) + + + + \ No newline at end of file diff --git a/docs/docs/connections/ado-cloud.mdx b/docs/docs/connections/ado-cloud.mdx new file mode 100644 index 00000000..cc20c745 --- /dev/null +++ b/docs/docs/connections/ado-cloud.mdx @@ -0,0 +1,147 @@ +--- +title: Linking code from Azure Devops Cloud +sidebarTitle: Azure Devops Cloud +icon: https://www.svgrepo.com/show/448307/azure-devops.svg +--- + +import AzureDevopsSchema from '/snippets/schemas/v3/azuredevops.schema.mdx' + +If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first. + +## Examples + + + + ```json + { + "type": "azuredevops", + "deploymentType": "cloud", + "repos": [ + "organizationName/projectName/repoName", + "organizationName/projectName/repoName2 + ] + } + ``` + + + ```json + { + "type": "azuredevops", + "deploymentType": "cloud", + "orgs": [ + "organizationName", + "organizationName2 + ] + } + ``` + + + ```json + { + "type": "azuredevops", + "deploymentType": "cloud", + "projects": [ + "organizationName/projectName", + "organizationName/projectName2" + ] + } + ``` + + + ```json + { + "type": "azuredevops", + "deploymentType": "cloud", + // Include all repos in my-org... + "orgs": [ + "my-org" + ], + // ...except: + "exclude": { + // repos that are disabled + "disabled": true, + // repos that match these glob patterns + "repos": [ + "reposToExclude*" + ], + // projects that match these glob patterns + "projects": [ + "projectstoExclude*" + ] + // repos less than the defined min OR larger than the defined max + "size": { + // repos that are less than 1MB (in bytes)... + "min": 1048576, + // or repos greater than 100MB (in bytes) + "max": 104857600 + } + } + } + ``` + + + +## Authenticating with Azure Devops Cloud + +Azure Devops Cloud requires you to provide a PAT in order to index your repositories. To learn how to create PAT, check out the [Azure Devops docs](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows). +Sourcebot needs the `Read` access for the `Code` scope in order to find and clone your repos. + +Next, provide the access token via the `token` property, either as an environment variable or a secret: + + + + + 1. Add the `token` property to your connection config: + ```json + { + "type": "azuredevops", + "deploymentType": "cloud", + "token": { + // note: this env var can be named anything. It + // doesn't need to be `ADO_TOKEN`. + "env": "ADO_TOKEN" + } + // .. rest of config .. + } + ``` + + 2. Pass this environment variable each time you run Sourcebot: + ```bash + docker run \ + -e ADO_TOKEN= \ + /* additional args */ \ + ghcr.io/sourcebot-dev/sourcebot:latest + ``` + + + + Secrets are only supported when [authentication](/docs/configuration/auth/overview) is enabled. + + 1. Navigate to **Secrets** in settings and create a new secret with your PAT: + + ![](/images/secrets_list.png) + + 2. Add the `token` property to your connection config: + + ```json + { + "type": "azuredevops", + "deploymentType": "cloud", + "token": { + "secret": "mysecret" + } + // .. rest of config .. + } + ``` + + + + +## Schema reference + + +[schemas/v3/azuredevops.json](https://github.com/sourcebot-dev/sourcebot/blob/main/schemas/v3/azuredevops.json) + + + + \ No newline at end of file diff --git a/docs/docs/connections/ado-server.mdx b/docs/docs/connections/ado-server.mdx new file mode 100644 index 00000000..1cfc0252 --- /dev/null +++ b/docs/docs/connections/ado-server.mdx @@ -0,0 +1,161 @@ +--- +title: Linking code from Azure Devops Server +sidebarTitle: Azure Devops Server +icon: https://www.svgrepo.com/show/448307/azure-devops.svg +--- + +import AzureDevopsSchema from '/snippets/schemas/v3/azuredevops.schema.mdx' + +If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first. + +## Examples + + + + This is required if you're using an older version of ADO Server which has `/tfs` in the repo paths. + ```json + { + "type": "azuredevops", + "deploymentType": "server", + "useTfsPath": true, + "repos": [ + "organizationName/projectName/repoName", + "organizationName/projectName/repoName2 + ] + } + ``` + + + ```json + { + "type": "azuredevops", + "deploymentType": "server", + "repos": [ + "organizationName/projectName/repoName", + "organizationName/projectName/repoName2 + ] + } + ``` + + + ```json + { + "type": "azuredevops", + "deploymentType": "server", + "orgs": [ + "collectionName", + "collectionName2" + ] + } + ``` + + + ```json + { + "type": "azuredevops", + "deploymentType": "server", + "projects": [ + "collectionName/projectName", + "collectionName/projectName2" + ] + } + ``` + + + ```json + { + "type": "azuredevops", + "deploymentType": "server", + // Include all repos in my-org... + "orgs": [ + "my-org" + ], + // ...except: + "exclude": { + // repos that are disabled + "disabled": true, + // repos that match these glob patterns + "repos": [ + "reposToExclude*" + ], + // projects that match these glob patterns + "projects": [ + "projectstoExclude*" + ] + // repos less than the defined min OR larger than the defined max + "size": { + // repos that are less than 1MB (in bytes)... + "min": 1048576, + // or repos greater than 100MB (in bytes) + "max": 104857600 + } + } + } + ``` + + + +## Authenticating with Azure Devops Server + +Azure Devops Server requires you to provide a PAT in order to index your repositories. To learn how to create PAT, check out the [Azure Devops docs](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows). +Sourcebot needs the `Read` access for the `Code` scope in order to find and clone your repos. + +Next, provide the access token via the `token` property, either as an environment variable or a secret: + + + + + 1. Add the `token` property to your connection config: + ```json + { + "type": "azuredevops", + "deploymentType": "server", + "token": { + // note: this env var can be named anything. It + // doesn't need to be `ADO_TOKEN`. + "env": "ADO_TOKEN" + } + // .. rest of config .. + } + ``` + + 2. Pass this environment variable each time you run Sourcebot: + ```bash + docker run \ + -e ADO_TOKEN= \ + /* additional args */ \ + ghcr.io/sourcebot-dev/sourcebot:latest + ``` + + + + Secrets are only supported when [authentication](/docs/configuration/auth/overview) is enabled. + + 1. Navigate to **Secrets** in settings and create a new secret with your PAT: + + ![](/images/secrets_list.png) + + 2. Add the `token` property to your connection config: + + ```json + { + "type": "azuredevops", + "deploymentType": "server", + "token": { + "secret": "mysecret" + } + // .. rest of config .. + } + ``` + + + + +## Schema reference + + +[schemas/v3/azuredevops.json](https://github.com/sourcebot-dev/sourcebot/blob/main/schemas/v3/azuredevops.json) + + + + \ No newline at end of file diff --git a/docs/docs/connections/bitbucket-cloud.mdx b/docs/docs/connections/bitbucket-cloud.mdx index 0c431ce3..bde7a665 100644 --- a/docs/docs/connections/bitbucket-cloud.mdx +++ b/docs/docs/connections/bitbucket-cloud.mdx @@ -12,6 +12,8 @@ import BitbucketSchema from '/snippets/schemas/v3/bitbucket.schema.mdx' Looking for docs on Bitbucket Data Center? See [this doc](/docs/connections/bitbucket-data-center). +If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first. + ## Examples diff --git a/docs/docs/connections/bitbucket-data-center.mdx b/docs/docs/connections/bitbucket-data-center.mdx index c76ca383..77479e46 100644 --- a/docs/docs/connections/bitbucket-data-center.mdx +++ b/docs/docs/connections/bitbucket-data-center.mdx @@ -12,6 +12,8 @@ import BitbucketSchema from '/snippets/schemas/v3/bitbucket.schema.mdx' Looking for docs on Bitbucket Cloud? See [this doc](/docs/connections/bitbucket-cloud). +If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first. + ## Examples diff --git a/docs/docs/connections/generic-git-host.mdx b/docs/docs/connections/generic-git-host.mdx index df6202b2..c960f248 100644 --- a/docs/docs/connections/generic-git-host.mdx +++ b/docs/docs/connections/generic-git-host.mdx @@ -7,6 +7,8 @@ import GenericGitHost from '/snippets/schemas/v3/genericGitHost.schema.mdx' Sourcebot can sync code from any Git host (by clone url). This is helpful when you want to search code that not in a [supported code host](/docs/connections/overview#supported-code-hosts). +If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first. + ## Getting Started To connect to a Git host, create a new [connection](/docs/connections/overview) with type `git` and specify the clone url in the `url` property. For example: diff --git a/docs/docs/connections/gerrit.mdx b/docs/docs/connections/gerrit.mdx index b7253259..4b312b36 100644 --- a/docs/docs/connections/gerrit.mdx +++ b/docs/docs/connections/gerrit.mdx @@ -6,10 +6,12 @@ icon: crow import GerritSchema from '/snippets/schemas/v3/gerrit.schema.mdx' -Authenticating with Gerrit is currently not supported. If you need this capability, please raise a [feature request](https://github.com/sourcebot-dev/sourcebot/discussions/categories/ideas). +Authenticating with Gerrit is currently not supported. If you need this capability, please raise a [feature request](https://github.com/sourcebot-dev/sourcebot/issues/new?template=feature_request.md). Sourcebot can sync code from self-hosted gerrit instances. +If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first. + ## Connecting to a Gerrit instance To connect to a gerrit instance, provide the `url` property to your config: diff --git a/docs/docs/connections/gitea.mdx b/docs/docs/connections/gitea.mdx index 8869ebff..1d439c76 100644 --- a/docs/docs/connections/gitea.mdx +++ b/docs/docs/connections/gitea.mdx @@ -8,6 +8,8 @@ import GiteaSchema from '/snippets/schemas/v3/gitea.schema.mdx' Sourcebot can sync code from Gitea Cloud, and self-hosted. +If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first. + ## Examples @@ -83,7 +85,6 @@ Next, provide the access token via the `token` property, either as an environmen - Environment variables are only supported in a [declarative config](/docs/configuration/declarative-config) and cannot be used in the web UI. 1. Add the `token` property to your connection config: ```json diff --git a/docs/docs/connections/github.mdx b/docs/docs/connections/github.mdx index 2f31994e..98fc5b50 100644 --- a/docs/docs/connections/github.mdx +++ b/docs/docs/connections/github.mdx @@ -8,6 +8,8 @@ import GitHubSchema from '/snippets/schemas/v3/github.schema.mdx' Sourcebot can sync code from GitHub.com, GitHub Enterprise Server, and GitHub Enterprise Cloud. +If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first. + ## Examples @@ -130,7 +132,6 @@ Next, provide the access token via the `token` property, either as an environmen - Environment variables are only supported in a [declarative config](/docs/configuration/declarative-config) and cannot be used in the web UI. 1. Add the `token` property to your connection config: ```json @@ -195,4 +196,8 @@ To connect to a GitHub host other than `github.com`, provide the `url` property - \ No newline at end of file + + +## See also + +- [Syncing GitHub Access permissions to Sourcebot](/docs/features/permission-syncing#github) diff --git a/docs/docs/connections/gitlab.mdx b/docs/docs/connections/gitlab.mdx index 2677a67a..2680064b 100644 --- a/docs/docs/connections/gitlab.mdx +++ b/docs/docs/connections/gitlab.mdx @@ -8,6 +8,7 @@ import GitLabSchema from '/snippets/schemas/v3/gitlab.schema.mdx' Sourcebot can sync code from GitLab.com, Self Managed (CE & EE), and Dedicated. +If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first. ## Examples @@ -89,6 +90,8 @@ Sourcebot can sync code from GitLab.com, Self Managed (CE & EE), and Dedicated. "archived": true, // projects that are forks "forks": true, + // projects that are owned by users (not groups) + "userOwnedProjects": true, // projects that match these glob patterns "projects": [ "my-group/foo/**", @@ -117,7 +120,6 @@ Next, provide the PAT via the `token` property, either as an environment variabl - Environment variables are only supported in a [declarative config](/docs/configuration/declarative-config) and cannot be used in the web UI. 1. Add the `token` property to your connection config: ```json diff --git a/docs/docs/connections/local-repos.mdx b/docs/docs/connections/local-repos.mdx index 0791b8f0..114c3c8c 100644 --- a/docs/docs/connections/local-repos.mdx +++ b/docs/docs/connections/local-repos.mdx @@ -7,6 +7,8 @@ import GenericGitHost from '/snippets/schemas/v3/genericGitHost.schema.mdx' Sourcebot can sync code from generic git repositories stored in a local directory. This can be helpful in scenarios where you already have a large number of repos already checked out. Local repositories are treated as **read-only**, meaning Sourcebot will **not** `git fetch` new revisions. +If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first. + ## Getting Started diff --git a/docs/docs/connections/overview.mdx b/docs/docs/connections/overview.mdx index 5165a2aa..ab9f8ffc 100644 --- a/docs/docs/connections/overview.mdx +++ b/docs/docs/connections/overview.mdx @@ -6,20 +6,7 @@ sidebarTitle: Overview import SupportedPlatforms from '/snippets/platform-support.mdx' import ConfigSchema from '/snippets/schemas/v3/index.schema.mdx' -To index your code with Sourcebot, you must provide a configuration file. When running Sourcebot, this file must be mounted in a volume that is accessible to the container, with its -path specified in the `CONFIG_PATH` environment variable. For example: - -```bash icon="terminal" Passing in a CONFIG_PATH to Sourcebot -docker run \ - -v $(pwd)/config.json:/data/config.json \ - -e CONFIG_PATH=/data/config.json \ - ... \ # other config - ghcr.io/sourcebot-dev/sourcebot:latest -``` - -## Config Schema - -The configuration file defines a set of **connections**. A connection in Sourcebot represents a link to a code host (such as GitHub, GitLab, Bitbucket, etc.). +A **connection** represents Sourcebot's link to a code host platform (GitHub, GitLab, etc). Connections are defined within the [config file](/docs/configuration/config-file) you provide Sourcebot. Each connection defines how Sourcebot should authenticate and interact with a particular host, and which repositories to sync and index from that host. Connections are uniquely identified by their name. @@ -55,10 +42,11 @@ Each connection defines how Sourcebot should authenticate and interact with a pa Configuration files must conform to the [JSON schema](#schema-reference). -## Config Syncing -Sourcebot performs syncing in the background. Syncing consists of two steps: -1. Fetch the latest changes from `HEAD` (and any [additional branches](/docs/features/search/multi-branch-indexing)) from the code host. -2. Re-indexes the repository. +## Connection Syncing + +When a connection is first discovered, or the `resyncConnectionIntervalMs` [setting](/docs/configuration/config-file#settings) has exceeded, the connection will be synced. This consists of: +1. Fetching the latest changes from `HEAD` (and any [additional branches](/docs/features/search/multi-branch-indexing)) from the code host. +2. Re-indexing the repository. This is processed in a [job queue](/docs/overview#architecture), and is parallelized across multiple worker processes. Jobs will take longer to complete the first time a repository is synced, or when a diff is large. @@ -79,7 +67,7 @@ To learn more about how to create a connection for a specific code host, check o -Missing your code host? [Submit a feature request on GitHub](https://github.com/sourcebot-dev/sourcebot/discussions/categories/ideas). +Missing your code host? [Submit a feature request on GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new?template=feature_request.md). ## Schema reference diff --git a/docs/docs/connections/request-new.mdx b/docs/docs/connections/request-new.mdx index 5308736e..774e6be7 100644 --- a/docs/docs/connections/request-new.mdx +++ b/docs/docs/connections/request-new.mdx @@ -1,8 +1,8 @@ --- sidebarTitle: Request another host -url: https://github.com/sourcebot-dev/sourcebot/discussions/categories/ideas +url: https://github.com/sourcebot-dev/sourcebot/issues/new?template=feature_request.md title: Request another code host icon: plus --- -Is your code host not supported? Please open a [feature request](https://github.com/sourcebot-dev/sourcebot/discussions/categories/ideas). \ No newline at end of file +Is your code host not supported? Please open a [feature request](https://github.com/sourcebot-dev/sourcebot/issues/new?template=feature_request.md). \ No newline at end of file diff --git a/docs/docs/deployment-guide.mdx b/docs/docs/deployment-guide.mdx index 7e785486..7a9725e7 100644 --- a/docs/docs/deployment-guide.mdx +++ b/docs/docs/deployment-guide.mdx @@ -7,7 +7,7 @@ import SupportedPlatforms from '/snippets/platform-support.mdx' The following guide will walk you through the steps to deploy Sourcebot on your own infrastructure. Sourcebot is distributed as a [single docker container](/docs/overview#architecture) that can be deployed to a k8s cluster, a VM, or any platform that supports docker. -Hit an issue? Please let us know on [GitHub discussions](https://github.com/sourcebot-dev/sourcebot/discussions/categories/support) or by [emailing us](mailto:team@sourcebot.dev). +Hit an issue? Please let us know on [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose) or by [emailing us](mailto:team@sourcebot.dev). @@ -32,7 +32,7 @@ The following guide will walk you through the steps to deploy Sourcebot on your }' > config.json ``` - This config creates a single GitHub connection named `starter-connection` that specifies [Sourcebot](https://github.com/sourcebot-dev/sourcebot) as a repo to sync. [Learn more about the config file](/docs/connections/overview). + This config creates a single GitHub connection named `starter-connection` that specifies [Sourcebot](https://github.com/sourcebot-dev/sourcebot) as a repo to sync. [Learn more about the config file](/docs/configuration/config-file). @@ -65,25 +65,24 @@ The following guide will walk you through the steps to deploy Sourcebot on your Navigate to `http://localhost:3000` and complete the onboarding flow. - - - By default, only email / password authentication is enabled. [Learn more about authentication](/docs/configuration/auth/overview). - - You're all set! You can now start searching - checkout the [syntax guide](/docs/features/search/syntax-reference) to learn more about how to search. + You're all set! If you'd like to setup [Ask Sourcebot](/docs/features/ask/overview), configure a language model [provider](/docs/configuration/language-model-providers). ## Next steps --- - - - Learn more about how to connect your code to Sourcebot. + + + Learn how to index your code using Sourcebot - + + Learn how to configure language model providers to start using [Ask Sourcebot](/docs/features/ask/overview) + + Learn more about how to setup SSO, email codes, and other authentication providers. \ No newline at end of file diff --git a/docs/docs/features/agents/overview.mdx b/docs/docs/features/agents/overview.mdx index 3f5a82c6..5b3bea6f 100644 --- a/docs/docs/features/agents/overview.mdx +++ b/docs/docs/features/agents/overview.mdx @@ -3,9 +3,9 @@ title: "Agents Overview" sidebarTitle: "Overview" --- - -Agents are currently a experimental feature. 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. - +import ExperimentalFeatureWarning from '/snippets/experimental-feature-warning.mdx' + + 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. diff --git a/docs/docs/features/agents/review-agent.mdx b/docs/docs/features/agents/review-agent.mdx index 74582c53..743611d0 100644 --- a/docs/docs/features/agents/review-agent.mdx +++ b/docs/docs/features/agents/review-agent.mdx @@ -10,7 +10,7 @@ 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](https://github.com/sourcebot-dev/sourcebot/tree/main/packages/web/src/features/agents/review-agent) and packaged in [Sourcebot](https://github.com/sourcebot-dev/sourcebot). To get started using this agent, [deploy Sourcebot](/docs/deployment-guide) +The AI Code Review Agent is [fair source](https://github.com/sourcebot-dev/sourcebot/tree/main/packages/web/src/features/agents/review-agent) and packaged in [Sourcebot](https://github.com/sourcebot-dev/sourcebot). To get started using this agent, [deploy Sourcebot](/docs/deployment-guide) and then follow the configuration instructions below. ![AI Code Review Agent Example](/images/review_agent_example.png) diff --git a/docs/docs/features/ask/add-model-providers.mdx b/docs/docs/features/ask/add-model-providers.mdx new file mode 100644 index 00000000..19f03add --- /dev/null +++ b/docs/docs/features/ask/add-model-providers.mdx @@ -0,0 +1,5 @@ +--- +sidebarTitle: Configure language models +url: /docs/configuration/language-model-providers +title: Configure Language Models +--- \ No newline at end of file diff --git a/docs/docs/features/ask/overview.mdx b/docs/docs/features/ask/overview.mdx new file mode 100644 index 00000000..cd48b8c9 --- /dev/null +++ b/docs/docs/features/ask/overview.mdx @@ -0,0 +1,56 @@ +--- +title: Overview +--- + +Ask Sourcebot gives you the ability to ask complex questions about your codebase in natural language. + +It uses Sourcebot’s existing [code search](/docs/features/search/overview) and [navigation](/docs/features/code-navigation) tools to allow reasoning models to search your code, +follow code nav references, and provide an answer that’s rich with inline citations and navigable code snippets. + + + + Learn how to connect your language model to Sourcebot + + + Learn how to index your repos so you can ask questions about them + + + Learn how to self-host Sourcebot in a few simple steps. + + + Try Ask Sourcebot on our public demo instance. + + + + + +# Why do we need another AI dev tool? + +Existing AI dev tools (Cursor, Claude Code, Copilot) are great at generating code. However, we believe one of the hardest parts of being +a software engineer is **understanding code**. + +In this domain, these tools fall short: +- You can only ask questions about the code you have checked out locally +- You get a wall of text that's difficult to parse, requiring you to go back and forth through different code snippets in the response +- The richness of the explanation is limited by the fact that you're in your IDE + +We built Ask Sourcebot to address these problems. With Ask Sourcebot, you can: +- Ask questions about your teams entire codebase (even on repos you don't have locally) +- Easily parse the response with side-by-side citations and code navigation +- Share answers with your team to spread the knowledge + +Being a web app is less convenient than being in your IDE, but it allows Sourcebot to provide responses in a richer UI that isn't constrained by the IDE. + +We believe this experience of understanding your codebase is superior, and we hope you find it useful. We'd love to know what you think! Feel free to join the discussion on our +[GitHub](https://github.com/sourcebot-dev/sourcebot/discussions). + +# Troubleshooting + +- **Network timeouts**: If you are hitting generic "network error" message while the answer is streaming when Sourcebot is deployed in a production environment, it may be due to your load balancer or proxy not being configured to handle long-lived connections. The timeout should be configured to a sufficiently large value (e.g., 5 minutes). \ No newline at end of file diff --git a/docs/docs/features/permission-syncing.mdx b/docs/docs/features/permission-syncing.mdx new file mode 100644 index 00000000..527b81f0 --- /dev/null +++ b/docs/docs/features/permission-syncing.mdx @@ -0,0 +1,72 @@ +--- +title: "Permission syncing" +sidebarTitle: "Permission syncing" +tag: "experimental" +--- + +import LicenseKeyRequired from '/snippets/license-key-required.mdx' +import ExperimentalFeatureWarning from '/snippets/experimental-feature-warning.mdx' + + + + +# Overview + +Permission syncing allows you to sync Access Permission Lists (ACLs) from a code host to Sourcebot. When configured, users signed into Sourcebot (via the code host's OAuth provider) will only be able to access repositories that they have access to on the code host. Practically, this means: + +- Code Search results will only include repositories that the user has access to. +- Code navigation results will only include repositories that the user has access to. +- Ask Sourcebot (and the underlying LLM) will only have access to repositories that the user has access to. +- File browsing is scoped to the repositories that the user has access to. + +Permission syncing can be enabled by setting the `EXPERIMENT_EE_PERMISSION_SYNC_ENABLED` environment variable to `true`. + +```bash +docker run \ + -e EXPERIMENT_EE_PERMISSION_SYNC_ENABLED=true \ + /* additional args */ \ + ghcr.io/sourcebot-dev/sourcebot:latest +``` + +## Platform support + +We are actively working on supporting more code hosts. If you'd like to see a specific code host supported, please [reach out](https://www.sourcebot.dev/contact). + +| Platform | Permission syncing | +|:----------|------------------------------| +| [GitHub (GHEC & GHEC Server)](/docs/features/permission-syncing#github) | ✅ | +| GitLab | 🛑 | +| Bitbucket Cloud | 🛑 | +| Bitbucket Data Center | 🛑 | +| Gitea | 🛑 | +| Gerrit | 🛑 | +| Generic git host | 🛑 | + +# Getting started + +## GitHub + +Prerequisite: [Add GitHub as an OAuth provider](/docs/configuration/auth/providers#github). + +Permission syncing works with **GitHub.com**, **GitHub Enterprise Cloud**, and **GitHub Enterprise Server**. For organization-owned repositories, users that have **read-only** access (or above) via the following methods will have their access synced to Sourcebot: +- Outside collaborators +- Organization members that are direct collaborators +- Organization members with access through team memberships +- Organization members with access through default organization permissions +- Organization owners. + +**Notes:** +- A GitHub OAuth provider must be configured to (1) correlate a Sourcebot user with a GitHub user, and (2) to list repositories that the user has access to for [User driven syncing](/docs/features/permission-syncing#how-it-works). +- OAuth tokens must assume the `repo` scope in order to use the [List repositories for the authenticated user API](https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repositories-for-the-authenticated-user) during [User driven syncing](/docs/features/permission-syncing#how-it-works). Sourcebot **will only** use this token for **reads**. + +# How it works + +Permission syncing works by periodically syncing ACLs from the code host(s) to Sourcebot to build an internal mapping between Users and Repositories. This mapping is hydrated in two directions: +- **User driven** : fetches the list of all repositories that a given user has access to. +- **Repo driven** : fetches the list of all users that have access to a given repository. + +User driven and repo driven syncing occurs every 24 hours by default. These intervals can be configured using the following settings in the [config file](/docs/configuration/config-file): +| Setting | Type | Default | Minimum | +|-------------------------------------------------|---------|------------|---------| +| `experiment_repoDrivenPermissionSyncIntervalMs` | number | 24 hours | 1 | +| `experiment_userDrivenPermissionSyncIntervalMs` | number | 24 hours | 1 | \ No newline at end of file diff --git a/docs/docs/features/search/multi-branch-indexing.mdx b/docs/docs/features/search/multi-branch-indexing.mdx index dd64e279..aad65591 100644 --- a/docs/docs/features/search/multi-branch-indexing.mdx +++ b/docs/docs/features/search/multi-branch-indexing.mdx @@ -12,7 +12,7 @@ By default, only the default branch of a repository is indexed and can be search ## Configuration -Multi-branch indexing is currently limited to 64 branches and tags. If this limitation impacts your use-case, please [open a discussion](https://github.com/sourcebot-dev/sourcebot/discussions/categories/support). +Multi-branch indexing is currently limited to 64 branches and tags. Please see [this issue](https://github.com/sourcebot-dev/sourcebot/issues/461) for more details. Multi-branch indexing is configured in the [connection](/docs/connections/overview) using the `revisions.branches` and `revisions.tags` arrays. Glob patterns are supported. For example: @@ -89,6 +89,6 @@ Additional info: | Bitbucket Cloud | ✅ | | Bitbucket Data Center | ✅ | | Gitea | ✅ | -| Gerrit | ❌ | +| Gerrit | ✅ | | Generic git host | ✅ | diff --git a/docs/docs/features/search/overview.mdx b/docs/docs/features/search/overview.mdx new file mode 100644 index 00000000..5f703a3c --- /dev/null +++ b/docs/docs/features/search/overview.mdx @@ -0,0 +1,40 @@ +--- +title: Overview +--- + +Search across all your repos/branches across any code host platform. Blazingly fast, and supports regular expressions, repo/language search filters, boolean logic, and more. + + +- **Regex support:** Use regular expressions to find code with precision. +- **Query language:** Scope searches to specific files, repos, languages, symbol definitions and more using a rich [query language](/docs/features/search/syntax-reference). +- **Branch search:** Specify a list of branches to search across ([docs](/docs/features/search/multi-branch-indexing)). +- **Fast & scalable:** Sourcebot uses [trigram indexing](https://en.wikipedia.org/wiki/Trigram_search), allowing it to scale to massive codebases. +- **Syntax highlighting:** Syntax highlighting support for over [100+ languages](https://github.com/sourcebot-dev/sourcebot/blob/57724689303f351c279d37f45b6406f1d5d5d5ab/packages/web/src/lib/codemirrorLanguage.ts#L125). +- **Multi-repository:** Search across all of your repositories in a single search. +- **Search suggestions:** Get search suggestions as you craft your query. +- **Filter panel:** Filter results by repository or by language. + + + + + Learn how to index your repos so you can ask questions about them + + + Learn how to index and search through your branches + + + Learn how to self-host Sourcebot in a few simple steps. + + + Try Sourcebot's code search on our public demo instance. + + + + \ No newline at end of file diff --git a/docs/docs/license-key.mdx b/docs/docs/license-key.mdx index 64b04f4e..272eab62 100644 --- a/docs/docs/license-key.mdx +++ b/docs/docs/license-key.mdx @@ -7,7 +7,7 @@ sidebarTitle: License key If you'd like a trial license, [reach out](https://www.sourcebot.dev/contact) and we'll send one over within 24 hours -All core Sourcebot features are available in Sourcebot OSS (MIT Licensed) without any limits. Some additional features require a license key. See the [pricing page](https://www.sourcebot.dev/pricing) for more details. +All core Sourcebot features are available [FSL licensed](https://github.com/sourcebot-dev/sourcebot/blob/main/LICENSE.md#functional-source-license-version-11-alv2-future-license) without any limits. Some additional features require a license key. See the [pricing page](https://www.sourcebot.dev/pricing) for more details. ## Activating a license key diff --git a/docs/docs/overview.mdx b/docs/docs/overview.mdx index 35bf8be3..96d9bc85 100644 --- a/docs/docs/overview.mdx +++ b/docs/docs/overview.mdx @@ -2,7 +2,10 @@ title: "Overview" --- -[Sourcebot]((https://github.com/sourcebot-dev/sourcebot)) is an open-source, self-hosted code search tool. It allows you to search and navigate across millions of lines of code across several code host platforms. +[Sourcebot](https://github.com/sourcebot-dev/sourcebot) is a self-hosted tool that helps you understand your codebase. + +- [Code search](/docs/features/search/overview): Search and navigate across all your repos and branches, no matter where they’re hosted +- [Ask Sourcebot](/docs/features/ask): Ask questions about your codebase and have Sourcebot provide detailed answers grounded with inline citations @@ -19,7 +22,7 @@ title: "Overview" - **Self-hosted:** Deploy it in minutes using our official [docker container](https://github.com/sourcebot-dev/sourcebot/pkgs/container/sourcebot). All of your data stays on your machine. - **Modern design:** Light/Dark mode, vim keybindings, keyboard shortcuts, syntax highlighting, etc. - **Scalable:** Scales to millions of lines of code. - - **Open-source:** Core features are MIT licensed. + - **Fair-source:** Core features are [FSL licensed](https://github.com/sourcebot-dev/sourcebot/blob/main/LICENSE.md#functional-source-license-version-11-alv2-future-license). @@ -30,9 +33,28 @@ title: "Overview" Find an overview of all Sourcebot features below. For details, see the individual documentation pages. -### Fast indexed based search +### Ask Sourcebot -Search across millions of lines of code instantly using Sourcebot's blazingly fast indexed search. Find exactly what you are looking for with regular expressions, search filters, boolean logic, and more. +[Ask Sourcebot](/docs/features/ask) gives you the ability to ask complex questions about your codebase, and have Sourcebot provide detailed answers with inline citations. + + +- **Bring your own model:** [Configure](/docs/configuration/language-model-providers) to any language model you'd like +- **Inline citations:** Every answer Sourcebot provides is grounded with inline citations directly into your codebase +- **Mutli-repo:** Ask questions about any repository you have indexed on Sourcebot + + + + +### Code Search + +Search across all your repos/branches across any code host platform. Blazingly fast, and supports regular expressions, repo/language search filters, boolean logic, and more. - **Regex support:** Use regular expressions to find code with precision. @@ -71,7 +93,7 @@ Search across millions of lines of code instantly using Sourcebot's blazingly fa loop playsInline className="w-full aspect-video" - src="https://framerusercontent.com/assets/B9ZxrlsUeO9NJyzkKyvVV2KSU4.mp4" + src="/images/code_nav.mp4" > @@ -174,7 +196,7 @@ Sourcebot does not support horizontal scaling at this time, but it is on our roa ## License key --- -Sourcebot's core features are available under an [MIT license](https://github.com/sourcebot-dev/sourcebot/blob/HEAD/LICENSE) without any limits. Some [additional features](/docs/license-key#feature-availability) such as SSO and code navigation require a [license key](/docs/license-key). +Sourcebot's core features are available under an [FSL licensed](https://github.com/sourcebot-dev/sourcebot/blob/main/LICENSE.md#functional-source-license-version-11-alv2-future-license) without any limits. Some [additional features](/docs/license-key#feature-availability) such as SSO and code navigation require a [license key](/docs/license-key). diff --git a/docs/docs/upgrade/v2-to-v3-guide.mdx b/docs/docs/upgrade/v2-to-v3-guide.mdx index 898c7af2..04e64a1f 100644 --- a/docs/docs/upgrade/v2-to-v3-guide.mdx +++ b/docs/docs/upgrade/v2-to-v3-guide.mdx @@ -78,7 +78,7 @@ If your deployment is dependent on these features, please [reach out](https://gi After updating your configuration file, restart your Sourcebot deployment to pick up the new changes. - Congrats, you've successfully migrated to v3! Please let us know what you think of the new features by reaching out on our [discord](https://discord.gg/6Fhp27x7Pb) or [GitHub discussion](https://github.com/sourcebot-dev/sourcebot/discussions/categories/support) + Congrats, you've successfully migrated to v3! Please let us know what you think of the new features by reaching out on our [discord](https://discord.gg/6Fhp27x7Pb) or on [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose). @@ -90,4 +90,4 @@ Some things to check: - Make sure you have a name for each `connection`, and that the name only contains letters, digits, hyphens, or underscores - Make sure each `connection` has a `type` field with a valid value (`gitlab`, `github`, `gitea`, `gerrit`) -Having troubles migrating from v2 to v3? Reach out to us on [discord](https://discord.gg/6Fhp27x7Pb) or [GitHub discussion](https://github.com/sourcebot-dev/sourcebot/discussions/categories/support) and we'll try our best to help \ No newline at end of file +Having troubles migrating from v2 to v3? Reach out to us on [discord](https://discord.gg/6Fhp27x7Pb) or [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose) and we'll try our best to help \ No newline at end of file diff --git a/docs/docs/upgrade/v3-to-v4-guide.mdx b/docs/docs/upgrade/v3-to-v4-guide.mdx index 93477157..b40f0c23 100644 --- a/docs/docs/upgrade/v3-to-v4-guide.mdx +++ b/docs/docs/upgrade/v3-to-v4-guide.mdx @@ -40,7 +40,7 @@ Please note that the following features are no longer supported in v4: - Congrats, you've successfully migrated to v4! Please let us know what you think of the new features by reaching out on our [discord](https://discord.gg/6Fhp27x7Pb) or [GitHub discussion](https://github.com/sourcebot-dev/sourcebot/discussions/categories/support) + Congrats, you've successfully migrated to v4! Please let us know what you think of the new features by reaching out on our [discord](https://discord.gg/6Fhp27x7Pb) or [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose) @@ -58,4 +58,4 @@ to finish upgrading to v4 in single-tenant mode. - If you're hitting issues with signing into your Sourcebot instance, make sure you're setting `AUTH_URL` correctly to your deployment domain (ex. `https://sourcebot.yourcompany.com`) -Having troubles migrating from v3 to v4? Reach out to us on [discord](https://discord.gg/6Fhp27x7Pb) or [GitHub discussion](https://github.com/sourcebot-dev/sourcebot/discussions/categories/support) and we'll try our best to help \ No newline at end of file +Having troubles migrating from v3 to v4? Reach out to us on [discord](https://discord.gg/6Fhp27x7Pb) or [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose) and we'll try our best to help \ No newline at end of file diff --git a/docs/images/ado.svg b/docs/images/ado.svg new file mode 100644 index 00000000..e5668e9b --- /dev/null +++ b/docs/images/ado.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/images/ask_sourcebot_low_res.mp4 b/docs/images/ask_sourcebot_low_res.mp4 new file mode 100644 index 00000000..25495568 Binary files /dev/null and b/docs/images/ask_sourcebot_low_res.mp4 differ diff --git a/docs/images/code_nav.mp4 b/docs/images/code_nav.mp4 new file mode 100644 index 00000000..754b79ff Binary files /dev/null and b/docs/images/code_nav.mp4 differ diff --git a/docs/snippets/bitbucket-app-password.mdx b/docs/snippets/bitbucket-app-password.mdx index 1f52e79c..0a5fdac3 100644 --- a/docs/snippets/bitbucket-app-password.mdx +++ b/docs/snippets/bitbucket-app-password.mdx @@ -1,6 +1,5 @@ - Environment variables are only supported in a [declarative config](/docs/configuration/declarative-config) and cannot be used in the web UI. 1. Add the `token` and `user` (username associated with the app password you created) properties to your connection config: ```json diff --git a/docs/snippets/bitbucket-token.mdx b/docs/snippets/bitbucket-token.mdx index 8b7e1db6..94197e01 100644 --- a/docs/snippets/bitbucket-token.mdx +++ b/docs/snippets/bitbucket-token.mdx @@ -1,6 +1,5 @@ - Environment variables are only supported in a [declarative config](/docs/configuration/declarative-config) and cannot be used in the web UI. 1. Add the `token` property to your connection config: ```json diff --git a/docs/snippets/experimental-feature-warning.mdx b/docs/snippets/experimental-feature-warning.mdx new file mode 100644 index 00000000..cdae892c --- /dev/null +++ b/docs/snippets/experimental-feature-warning.mdx @@ -0,0 +1,4 @@ + + +This is an experimental feature. Certain functionality may be incomplete and breaking changes may ship in non-major releases. Have feedback? Submit a [issue](https://github.com/sourcebot-dev/sourcebot/issues) on GitHub. + diff --git a/docs/snippets/platform-support.mdx b/docs/snippets/platform-support.mdx index 38266b2d..1687d5db 100644 --- a/docs/snippets/platform-support.mdx +++ b/docs/snippets/platform-support.mdx @@ -3,6 +3,43 @@ + {/* Mintlify has a bug where linking to a file for the logo renders it with a white background, so we have to embed it directly */} + + + + } + /> + + + + } + /> diff --git a/docs/snippets/schemas/v3/azuredevops.schema.mdx b/docs/snippets/schemas/v3/azuredevops.schema.mdx new file mode 100644 index 00000000..fab796de --- /dev/null +++ b/docs/snippets/schemas/v3/azuredevops.schema.mdx @@ -0,0 +1,206 @@ +{/* THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! */} +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "AzureDevOpsConnectionConfig", + "properties": { + "type": { + "const": "azuredevops", + "description": "Azure DevOps Configuration" + }, + "token": { + "description": "A Personal Access Token (PAT).", + "examples": [ + { + "secret": "SECRET_KEY" + } + ], + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "url": { + "type": "string", + "format": "url", + "default": "https://dev.azure.com", + "description": "The URL of the Azure DevOps host. For Azure DevOps Cloud, use https://dev.azure.com. For Azure DevOps Server, use your server URL.", + "examples": [ + "https://dev.azure.com", + "https://azuredevops.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + }, + "deploymentType": { + "type": "string", + "enum": [ + "cloud", + "server" + ], + "description": "The type of Azure DevOps deployment" + }, + "useTfsPath": { + "type": "boolean", + "default": false, + "description": "Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...)." + }, + "orgs": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org" + ] + ], + "description": "List of organizations to sync with. For Cloud, this is the organization name. For Server, this is the collection name. All projects and repositories visible to the provided `token` will be synced, unless explicitly defined in the `exclude` property." + }, + "projects": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+\\/[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org/my-project", + "my-collection/my-project" + ] + ], + "description": "List of specific projects to sync with. Expected to be formatted as '{orgName}/{projectName}' for Cloud or '{collectionName}/{projectName}' for Server." + }, + "repos": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+\\/[\\w.-]+\\/[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org/my-project/my-repo" + ] + ], + "description": "List of individual repositories to sync with. Expected to be formatted as '{orgName}/{projectName}/{repoName}'." + }, + "exclude": { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "default": false, + "description": "Exclude disabled repositories from syncing." + }, + "repos": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of repositories to exclude from syncing. Glob patterns are supported." + }, + "projects": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of projects to exclude from syncing. Glob patterns are supported." + }, + "size": { + "type": "object", + "description": "Exclude repositories based on their size.", + "properties": { + "min": { + "type": "integer", + "description": "Minimum repository size (in bytes) to sync (inclusive). Repositories less than this size will be excluded from syncing." + }, + "max": { + "type": "integer", + "description": "Maximum repository size (in bytes) to sync (inclusive). Repositories greater than this size will be excluded from syncing." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "revisions": { + "type": "object", + "description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.", + "properties": { + "branches": { + "type": "array", + "description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "main", + "release/*" + ], + [ + "**" + ] + ], + "default": [] + }, + "tags": { + "type": "array", + "description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "latest", + "v2.*.*" + ], + [ + "**" + ] + ], + "default": [] + } + }, + "additionalProperties": false + } + }, + "required": [ + "type", + "token", + "deploymentType" + ], + "additionalProperties": false +} +``` diff --git a/docs/snippets/schemas/v3/connection.schema.mdx b/docs/snippets/schemas/v3/connection.schema.mdx index 9731bdeb..48750ae1 100644 --- a/docs/snippets/schemas/v3/connection.schema.mdx +++ b/docs/snippets/schemas/v3/connection.schema.mdx @@ -343,6 +343,11 @@ "default": false, "description": "Exclude archived projects from syncing." }, + "userOwnedProjects": { + "type": "boolean", + "default": false, + "description": "Exclude user-owned projects from syncing." + }, "projects": { "type": "array", "items": { @@ -638,6 +643,47 @@ } }, "additionalProperties": false + }, + "revisions": { + "type": "object", + "description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.", + "properties": { + "branches": { + "type": "array", + "description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "main", + "release/*" + ], + [ + "**" + ] + ], + "default": [] + }, + "tags": { + "type": "array", + "description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "latest", + "v2.*.*" + ], + [ + "**" + ] + ], + "default": [] + } + }, + "additionalProperties": false } }, "required": [ @@ -823,6 +869,209 @@ }, "additionalProperties": false }, + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "AzureDevOpsConnectionConfig", + "properties": { + "type": { + "const": "azuredevops", + "description": "Azure DevOps Configuration" + }, + "token": { + "description": "A Personal Access Token (PAT).", + "examples": [ + { + "secret": "SECRET_KEY" + } + ], + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "url": { + "type": "string", + "format": "url", + "default": "https://dev.azure.com", + "description": "The URL of the Azure DevOps host. For Azure DevOps Cloud, use https://dev.azure.com. For Azure DevOps Server, use your server URL.", + "examples": [ + "https://dev.azure.com", + "https://azuredevops.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + }, + "deploymentType": { + "type": "string", + "enum": [ + "cloud", + "server" + ], + "description": "The type of Azure DevOps deployment" + }, + "useTfsPath": { + "type": "boolean", + "default": false, + "description": "Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...)." + }, + "orgs": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org" + ] + ], + "description": "List of organizations to sync with. For Cloud, this is the organization name. For Server, this is the collection name. All projects and repositories visible to the provided `token` will be synced, unless explicitly defined in the `exclude` property." + }, + "projects": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+\\/[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org/my-project", + "my-collection/my-project" + ] + ], + "description": "List of specific projects to sync with. Expected to be formatted as '{orgName}/{projectName}' for Cloud or '{collectionName}/{projectName}' for Server." + }, + "repos": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+\\/[\\w.-]+\\/[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org/my-project/my-repo" + ] + ], + "description": "List of individual repositories to sync with. Expected to be formatted as '{orgName}/{projectName}/{repoName}'." + }, + "exclude": { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "default": false, + "description": "Exclude disabled repositories from syncing." + }, + "repos": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of repositories to exclude from syncing. Glob patterns are supported." + }, + "projects": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of projects to exclude from syncing. Glob patterns are supported." + }, + "size": { + "type": "object", + "description": "Exclude repositories based on their size.", + "properties": { + "min": { + "type": "integer", + "description": "Minimum repository size (in bytes) to sync (inclusive). Repositories less than this size will be excluded from syncing." + }, + "max": { + "type": "integer", + "description": "Maximum repository size (in bytes) to sync (inclusive). Repositories greater than this size will be excluded from syncing." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "revisions": { + "type": "object", + "description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.", + "properties": { + "branches": { + "type": "array", + "description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "main", + "release/*" + ], + [ + "**" + ] + ], + "default": [] + }, + "tags": { + "type": "array", + "description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "latest", + "v2.*.*" + ], + [ + "**" + ] + ], + "default": [] + } + }, + "additionalProperties": false + } + }, + "required": [ + "type", + "token", + "deploymentType" + ], + "additionalProperties": false + }, { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", diff --git a/docs/snippets/schemas/v3/gerrit.schema.mdx b/docs/snippets/schemas/v3/gerrit.schema.mdx index 561bda80..cd8c0a64 100644 --- a/docs/snippets/schemas/v3/gerrit.schema.mdx +++ b/docs/snippets/schemas/v3/gerrit.schema.mdx @@ -59,6 +59,47 @@ } }, "additionalProperties": false + }, + "revisions": { + "type": "object", + "description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.", + "properties": { + "branches": { + "type": "array", + "description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "main", + "release/*" + ], + [ + "**" + ] + ], + "default": [] + }, + "tags": { + "type": "array", + "description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "latest", + "v2.*.*" + ], + [ + "**" + ] + ], + "default": [] + } + }, + "additionalProperties": false } }, "required": [ diff --git a/docs/snippets/schemas/v3/gitlab.schema.mdx b/docs/snippets/schemas/v3/gitlab.schema.mdx index feadeaac..1d322f44 100644 --- a/docs/snippets/schemas/v3/gitlab.schema.mdx +++ b/docs/snippets/schemas/v3/gitlab.schema.mdx @@ -126,6 +126,11 @@ "default": false, "description": "Exclude archived projects from syncing." }, + "userOwnedProjects": { + "type": "boolean", + "default": false, + "description": "Exclude user-owned projects from syncing." + }, "projects": { "type": "array", "items": { diff --git a/docs/snippets/schemas/v3/index.schema.mdx b/docs/snippets/schemas/v3/index.schema.mdx index 0b7907ed..9df06a9f 100644 --- a/docs/snippets/schemas/v3/index.schema.mdx +++ b/docs/snippets/schemas/v3/index.schema.mdx @@ -69,6 +69,16 @@ "deprecated": true, "description": "This setting is deprecated. Please use the `FORCE_ENABLE_ANONYMOUS_ACCESS` environment variable instead.", "default": false + }, + "experiment_repoDrivenPermissionSyncIntervalMs": { + "type": "number", + "description": "The interval (in milliseconds) at which the repo permission syncer should run. Defaults to 24 hours.", + "minimum": 1 + }, + "experiment_userDrivenPermissionSyncIntervalMs": { + "type": "number", + "description": "The interval (in milliseconds) at which the user permission syncer should run. Defaults to 24 hours.", + "minimum": 1 } }, "additionalProperties": false @@ -92,6 +102,13 @@ ] ] }, + "includeConnections": { + "type": "array", + "description": "List of connections to include in the search context.", + "items": { + "type": "string" + } + }, "exclude": { "type": "array", "description": "List of repositories to exclude from the search context. Expected to be formatted as a URL without any leading http(s):// prefix (e.g., 'github.com/sourcebot-dev/sourcebot'). Glob patterns are supported.", @@ -105,14 +122,18 @@ ] ] }, + "excludeConnections": { + "type": "array", + "description": "List of connections to exclude from the search context.", + "items": { + "type": "string" + } + }, "description": { "type": "string", "description": "Optional description of the search context that surfaces in the UI." } }, - "required": [ - "include" - ], "additionalProperties": false } }, @@ -184,6 +205,16 @@ "deprecated": true, "description": "This setting is deprecated. Please use the `FORCE_ENABLE_ANONYMOUS_ACCESS` environment variable instead.", "default": false + }, + "experiment_repoDrivenPermissionSyncIntervalMs": { + "type": "number", + "description": "The interval (in milliseconds) at which the repo permission syncer should run. Defaults to 24 hours.", + "minimum": 1 + }, + "experiment_userDrivenPermissionSyncIntervalMs": { + "type": "number", + "description": "The interval (in milliseconds) at which the user permission syncer should run. Defaults to 24 hours.", + "minimum": 1 } }, "additionalProperties": false @@ -211,6 +242,13 @@ ] ] }, + "includeConnections": { + "type": "array", + "description": "List of connections to include in the search context.", + "items": { + "type": "string" + } + }, "exclude": { "type": "array", "description": "List of repositories to exclude from the search context. Expected to be formatted as a URL without any leading http(s):// prefix (e.g., 'github.com/sourcebot-dev/sourcebot'). Glob patterns are supported.", @@ -224,14 +262,18 @@ ] ] }, + "excludeConnections": { + "type": "array", + "description": "List of connections to exclude from the search context.", + "items": { + "type": "string" + } + }, "description": { "type": "string", "description": "Optional description of the search context that surfaces in the UI." } }, - "required": [ - "include" - ], "additionalProperties": false } }, @@ -584,6 +626,11 @@ "default": false, "description": "Exclude archived projects from syncing." }, + "userOwnedProjects": { + "type": "boolean", + "default": false, + "description": "Exclude user-owned projects from syncing." + }, "projects": { "type": "array", "items": { @@ -879,6 +926,47 @@ } }, "additionalProperties": false + }, + "revisions": { + "type": "object", + "description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.", + "properties": { + "branches": { + "type": "array", + "description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "main", + "release/*" + ], + [ + "**" + ] + ], + "default": [] + }, + "tags": { + "type": "array", + "description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "latest", + "v2.*.*" + ], + [ + "**" + ] + ], + "default": [] + } + }, + "additionalProperties": false } }, "required": [ @@ -1064,6 +1152,209 @@ }, "additionalProperties": false }, + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "AzureDevOpsConnectionConfig", + "properties": { + "type": { + "const": "azuredevops", + "description": "Azure DevOps Configuration" + }, + "token": { + "description": "A Personal Access Token (PAT).", + "examples": [ + { + "secret": "SECRET_KEY" + } + ], + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "url": { + "type": "string", + "format": "url", + "default": "https://dev.azure.com", + "description": "The URL of the Azure DevOps host. For Azure DevOps Cloud, use https://dev.azure.com. For Azure DevOps Server, use your server URL.", + "examples": [ + "https://dev.azure.com", + "https://azuredevops.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + }, + "deploymentType": { + "type": "string", + "enum": [ + "cloud", + "server" + ], + "description": "The type of Azure DevOps deployment" + }, + "useTfsPath": { + "type": "boolean", + "default": false, + "description": "Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...)." + }, + "orgs": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org" + ] + ], + "description": "List of organizations to sync with. For Cloud, this is the organization name. For Server, this is the collection name. All projects and repositories visible to the provided `token` will be synced, unless explicitly defined in the `exclude` property." + }, + "projects": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+\\/[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org/my-project", + "my-collection/my-project" + ] + ], + "description": "List of specific projects to sync with. Expected to be formatted as '{orgName}/{projectName}' for Cloud or '{collectionName}/{projectName}' for Server." + }, + "repos": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+\\/[\\w.-]+\\/[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org/my-project/my-repo" + ] + ], + "description": "List of individual repositories to sync with. Expected to be formatted as '{orgName}/{projectName}/{repoName}'." + }, + "exclude": { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "default": false, + "description": "Exclude disabled repositories from syncing." + }, + "repos": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of repositories to exclude from syncing. Glob patterns are supported." + }, + "projects": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of projects to exclude from syncing. Glob patterns are supported." + }, + "size": { + "type": "object", + "description": "Exclude repositories based on their size.", + "properties": { + "min": { + "type": "integer", + "description": "Minimum repository size (in bytes) to sync (inclusive). Repositories less than this size will be excluded from syncing." + }, + "max": { + "type": "integer", + "description": "Maximum repository size (in bytes) to sync (inclusive). Repositories greater than this size will be excluded from syncing." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "revisions": { + "type": "object", + "description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.", + "properties": { + "branches": { + "type": "array", + "description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "main", + "release/*" + ], + [ + "**" + ] + ], + "default": [] + }, + "tags": { + "type": "array", + "description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "latest", + "v2.*.*" + ], + [ + "**" + ] + ], + "default": [] + } + }, + "additionalProperties": false + } + }, + "required": [ + "type", + "token", + "deploymentType" + ], + "additionalProperties": false + }, { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", @@ -1136,6 +1427,2852 @@ } }, "additionalProperties": false + }, + "models": { + "type": "array", + "description": "Defines a collection of language models that are available to Sourcebot.", + "items": { + "type": "object", + "title": "LanguageModel", + "definitions": { + "AmazonBedrockLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "amazon-bedrock", + "description": "Amazon Bedrock Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "accessKeyId": { + "description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "accessKeySecret": { + "description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "sessionToken": { + "description": "Optional session token to use with the model. Defaults to the `AWS_SESSION_TOKEN` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "region": { + "type": "string", + "description": "The AWS region. Defaults to the `AWS_REGION` environment variable.", + "examples": [ + "us-east-1", + "us-west-2", + "eu-west-1" + ] + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "AnthropicLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "anthropic", + "description": "Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "AzureLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "azure", + "description": "Azure Configuration" + }, + "model": { + "type": "string", + "description": "The deployment name of the Azure model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "resourceName": { + "type": "string", + "description": "Azure resource name. Defaults to the `AZURE_RESOURCE_NAME` environment variable." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `AZURE_API_KEY` environment variable." + }, + "apiVersion": { + "type": "string", + "description": "Sets a custom api version. Defaults to `preview`." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Use a different URL prefix for API calls. Either this or `resourceName` can be used." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "DeepSeekLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "deepseek", + "description": "DeepSeek Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `DEEPSEEK_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "GoogleGenerativeAILanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "google-generative-ai", + "description": "Google Generative AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "GoogleVertexAnthropicLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex-anthropic", + "description": "Google Vertex AI Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the Anthropic language model running on Google Vertex.", + "examples": [ + "claude-sonnet-4" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "GoogleVertexLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex", + "description": "Google Vertex AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gemini-2.0-flash-exp", + "gemini-1.5-pro", + "gemini-1.5-flash" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "MistralLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "mistral", + "description": "Mistral AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `MISTRAL_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "OpenAILanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "openai", + "description": "OpenAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gpt-4.1", + "o4-mini", + "o3", + "o3-deep-research" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "reasoningEffort": { + "type": "string", + "description": "The reasoning effort to use with the model. Defaults to `medium`. See https://platform.openai.com/docs/guides/reasoning#get-started-with-reasonings", + "examples": [ + "minimal", + "low", + "medium", + "high" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "OpenAICompatibleLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "openai-compatible", + "description": "OpenAI Compatible Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key. If specified, adds an `Authorization` header to request headers with the value Bearer ." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Base URL of the OpenAI-compatible chat completions API endpoint.", + "examples": [ + "http://localhost:8080/v1" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + }, + "queryParams": { + "type": "object", + "description": "Optional query parameters to include in the request url.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model", + "baseUrl" + ], + "additionalProperties": false + }, + "OpenRouterLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "openrouter", + "description": "OpenRouter Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENROUTER_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "XaiLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "xai", + "description": "xAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "grok-beta", + "grok-vision-beta" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `XAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + } + }, + "oneOf": [ + { + "type": "object", + "properties": { + "provider": { + "const": "amazon-bedrock", + "description": "Amazon Bedrock Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "accessKeyId": { + "description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "accessKeySecret": { + "description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "sessionToken": { + "description": "Optional session token to use with the model. Defaults to the `AWS_SESSION_TOKEN` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "region": { + "type": "string", + "description": "The AWS region. Defaults to the `AWS_REGION` environment variable.", + "examples": [ + "us-east-1", + "us-west-2", + "eu-west-1" + ] + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "anthropic", + "description": "Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "azure", + "description": "Azure Configuration" + }, + "model": { + "type": "string", + "description": "The deployment name of the Azure model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "resourceName": { + "type": "string", + "description": "Azure resource name. Defaults to the `AZURE_RESOURCE_NAME` environment variable." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `AZURE_API_KEY` environment variable." + }, + "apiVersion": { + "type": "string", + "description": "Sets a custom api version. Defaults to `preview`." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Use a different URL prefix for API calls. Either this or `resourceName` can be used." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "deepseek", + "description": "DeepSeek Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `DEEPSEEK_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "google-generative-ai", + "description": "Google Generative AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex-anthropic", + "description": "Google Vertex AI Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the Anthropic language model running on Google Vertex.", + "examples": [ + "claude-sonnet-4" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex", + "description": "Google Vertex AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gemini-2.0-flash-exp", + "gemini-1.5-pro", + "gemini-1.5-flash" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "mistral", + "description": "Mistral AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `MISTRAL_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "openai", + "description": "OpenAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gpt-4.1", + "o4-mini", + "o3", + "o3-deep-research" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "reasoningEffort": { + "type": "string", + "description": "The reasoning effort to use with the model. Defaults to `medium`. See https://platform.openai.com/docs/guides/reasoning#get-started-with-reasonings", + "examples": [ + "minimal", + "low", + "medium", + "high" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "openai-compatible", + "description": "OpenAI Compatible Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key. If specified, adds an `Authorization` header to request headers with the value Bearer ." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Base URL of the OpenAI-compatible chat completions API endpoint.", + "examples": [ + "http://localhost:8080/v1" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + }, + "queryParams": { + "type": "object", + "description": "Optional query parameters to include in the request url.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model", + "baseUrl" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "openrouter", + "description": "OpenRouter Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENROUTER_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "xai", + "description": "xAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "grok-beta", + "grok-vision-beta" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `XAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + } + ] + } } }, "additionalProperties": false diff --git a/docs/snippets/schemas/v3/languageModel.schema.mdx b/docs/snippets/schemas/v3/languageModel.schema.mdx new file mode 100644 index 00000000..d90c4a76 --- /dev/null +++ b/docs/snippets/schemas/v3/languageModel.schema.mdx @@ -0,0 +1,2845 @@ +{/* THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! */} +```json +{ + "type": "object", + "title": "LanguageModel", + "definitions": { + "AmazonBedrockLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "amazon-bedrock", + "description": "Amazon Bedrock Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "accessKeyId": { + "description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "accessKeySecret": { + "description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "sessionToken": { + "description": "Optional session token to use with the model. Defaults to the `AWS_SESSION_TOKEN` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "region": { + "type": "string", + "description": "The AWS region. Defaults to the `AWS_REGION` environment variable.", + "examples": [ + "us-east-1", + "us-west-2", + "eu-west-1" + ] + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "AnthropicLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "anthropic", + "description": "Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "AzureLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "azure", + "description": "Azure Configuration" + }, + "model": { + "type": "string", + "description": "The deployment name of the Azure model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "resourceName": { + "type": "string", + "description": "Azure resource name. Defaults to the `AZURE_RESOURCE_NAME` environment variable." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `AZURE_API_KEY` environment variable." + }, + "apiVersion": { + "type": "string", + "description": "Sets a custom api version. Defaults to `preview`." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Use a different URL prefix for API calls. Either this or `resourceName` can be used." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "DeepSeekLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "deepseek", + "description": "DeepSeek Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `DEEPSEEK_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "GoogleGenerativeAILanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "google-generative-ai", + "description": "Google Generative AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "GoogleVertexAnthropicLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex-anthropic", + "description": "Google Vertex AI Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the Anthropic language model running on Google Vertex.", + "examples": [ + "claude-sonnet-4" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "GoogleVertexLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex", + "description": "Google Vertex AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gemini-2.0-flash-exp", + "gemini-1.5-pro", + "gemini-1.5-flash" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "MistralLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "mistral", + "description": "Mistral AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `MISTRAL_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "OpenAILanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "openai", + "description": "OpenAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gpt-4.1", + "o4-mini", + "o3", + "o3-deep-research" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "reasoningEffort": { + "type": "string", + "description": "The reasoning effort to use with the model. Defaults to `medium`. See https://platform.openai.com/docs/guides/reasoning#get-started-with-reasonings", + "examples": [ + "minimal", + "low", + "medium", + "high" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "OpenAICompatibleLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "openai-compatible", + "description": "OpenAI Compatible Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key. If specified, adds an `Authorization` header to request headers with the value Bearer ." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Base URL of the OpenAI-compatible chat completions API endpoint.", + "examples": [ + "http://localhost:8080/v1" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + }, + "queryParams": { + "type": "object", + "description": "Optional query parameters to include in the request url.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model", + "baseUrl" + ], + "additionalProperties": false + }, + "OpenRouterLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "openrouter", + "description": "OpenRouter Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENROUTER_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "XaiLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "xai", + "description": "xAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "grok-beta", + "grok-vision-beta" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `XAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + } + }, + "oneOf": [ + { + "type": "object", + "properties": { + "provider": { + "const": "amazon-bedrock", + "description": "Amazon Bedrock Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "accessKeyId": { + "description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "accessKeySecret": { + "description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "sessionToken": { + "description": "Optional session token to use with the model. Defaults to the `AWS_SESSION_TOKEN` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "region": { + "type": "string", + "description": "The AWS region. Defaults to the `AWS_REGION` environment variable.", + "examples": [ + "us-east-1", + "us-west-2", + "eu-west-1" + ] + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "anthropic", + "description": "Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "azure", + "description": "Azure Configuration" + }, + "model": { + "type": "string", + "description": "The deployment name of the Azure model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "resourceName": { + "type": "string", + "description": "Azure resource name. Defaults to the `AZURE_RESOURCE_NAME` environment variable." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `AZURE_API_KEY` environment variable." + }, + "apiVersion": { + "type": "string", + "description": "Sets a custom api version. Defaults to `preview`." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Use a different URL prefix for API calls. Either this or `resourceName` can be used." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "deepseek", + "description": "DeepSeek Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `DEEPSEEK_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "google-generative-ai", + "description": "Google Generative AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex-anthropic", + "description": "Google Vertex AI Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the Anthropic language model running on Google Vertex.", + "examples": [ + "claude-sonnet-4" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex", + "description": "Google Vertex AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gemini-2.0-flash-exp", + "gemini-1.5-pro", + "gemini-1.5-flash" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "mistral", + "description": "Mistral AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `MISTRAL_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "openai", + "description": "OpenAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gpt-4.1", + "o4-mini", + "o3", + "o3-deep-research" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "reasoningEffort": { + "type": "string", + "description": "The reasoning effort to use with the model. Defaults to `medium`. See https://platform.openai.com/docs/guides/reasoning#get-started-with-reasonings", + "examples": [ + "minimal", + "low", + "medium", + "high" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "openai-compatible", + "description": "OpenAI Compatible Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key. If specified, adds an `Authorization` header to request headers with the value Bearer ." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Base URL of the OpenAI-compatible chat completions API endpoint.", + "examples": [ + "http://localhost:8080/v1" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + }, + "queryParams": { + "type": "object", + "description": "Optional query parameters to include in the request url.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model", + "baseUrl" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "openrouter", + "description": "OpenRouter Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENROUTER_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "xai", + "description": "xAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "grok-beta", + "grok-vision-beta" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `XAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + } + ] +} +``` diff --git a/docs/snippets/schemas/v3/searchContext.schema.mdx b/docs/snippets/schemas/v3/searchContext.schema.mdx index 01477bef..2f0849cf 100644 --- a/docs/snippets/schemas/v3/searchContext.schema.mdx +++ b/docs/snippets/schemas/v3/searchContext.schema.mdx @@ -19,6 +19,13 @@ ] ] }, + "includeConnections": { + "type": "array", + "description": "List of connections to include in the search context.", + "items": { + "type": "string" + } + }, "exclude": { "type": "array", "description": "List of repositories to exclude from the search context. Expected to be formatted as a URL without any leading http(s):// prefix (e.g., 'github.com/sourcebot-dev/sourcebot'). Glob patterns are supported.", @@ -32,14 +39,18 @@ ] ] }, + "excludeConnections": { + "type": "array", + "description": "List of connections to exclude from the search context.", + "items": { + "type": "string" + } + }, "description": { "type": "string", "description": "Optional description of the search context that surfaces in the UI." } }, - "required": [ - "include" - ], "additionalProperties": false } ``` diff --git a/docs/snippets/schemas/v3/shared.schema.mdx b/docs/snippets/schemas/v3/shared.schema.mdx index 97fdbabf..82ca9195 100644 --- a/docs/snippets/schemas/v3/shared.schema.mdx +++ b/docs/snippets/schemas/v3/shared.schema.mdx @@ -74,6 +74,94 @@ } }, "additionalProperties": false + }, + "LanguageModelHeaders": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + }, + "LanguageModelQueryParams": { + "type": "object", + "description": "Optional query parameters to include in the request url.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false } } } diff --git a/entrypoint.sh b/entrypoint.sh index 6753353f..b031b326 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,6 +1,24 @@ #!/bin/sh set -e +# Check if DATABASE_URL is not set +if [ -z "$DATABASE_URL" ]; then + # Check if the individual database variables are set and construct the URL + if [ -n "$DATABASE_HOST" ] && [ -n "$DATABASE_USERNAME" ] && [ -n "$DATABASE_PASSWORD" ] && [ -n "$DATABASE_NAME" ]; then + DATABASE_URL="postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}/${DATABASE_NAME}" + + if [ -n "$DATABASE_ARGS" ]; then + DATABASE_URL="${DATABASE_URL}?$DATABASE_ARGS" + fi + + export DATABASE_URL + else + # Otherwise, fallback to a default value + DATABASE_URL="postgresql://postgres@localhost:5432/sourcebot" + export DATABASE_URL + fi +fi + if [ "$DATABASE_URL" = "postgresql://postgres@localhost:5432/sourcebot" ]; then DATABASE_EMBEDDED="true" fi diff --git a/package.json b/package.json index e118780e..c5909e76 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "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", + "dev:prisma:generate": "yarn with-env yarn workspace @sourcebot/db prisma:generate", "dev:prisma:studio": "yarn with-env yarn workspace @sourcebot/db prisma:studio", "dev:prisma:migrate:reset": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:reset", + "dev:prisma:db:push": "yarn with-env yarn workspace @sourcebot/db prisma:db:push", "build:deps": "yarn workspaces foreach -R --from '{@sourcebot/schemas,@sourcebot/error,@sourcebot/crypto,@sourcebot/db,@sourcebot/shared}' run build" }, "devDependencies": { @@ -23,5 +25,8 @@ "dotenv-cli": "^8.0.0", "npm-run-all": "^4.1.5" }, - "packageManager": "yarn@4.7.0" + "packageManager": "yarn@4.7.0", + "resolutions": { + "prettier": "3.5.3" + } } diff --git a/packages/backend/package.json b/packages/backend/package.json index 6338b0bb..dade7893 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -15,7 +15,7 @@ "@types/micromatch": "^4.0.9", "@types/node": "^22.7.5", "cross-env": "^7.0.3", - "json-schema-to-typescript": "^15.0.2", + "json-schema-to-typescript": "^15.0.4", "tsc-watch": "^6.2.0", "tsx": "^4.19.1", "typescript": "^5.6.2", @@ -37,6 +37,7 @@ "@t3-oss/env-core": "^0.12.0", "@types/express": "^5.0.0", "argparse": "^2.0.1", + "azure-devops-node-api": "^15.1.1", "bullmq": "^5.34.10", "cross-fetch": "^4.0.0", "dotenv": "^16.4.5", diff --git a/packages/backend/src/azuredevops.ts b/packages/backend/src/azuredevops.ts new file mode 100644 index 00000000..a06b9c09 --- /dev/null +++ b/packages/backend/src/azuredevops.ts @@ -0,0 +1,348 @@ +import { AzureDevOpsConnectionConfig } from "@sourcebot/schemas/v3/azuredevops.type"; +import { createLogger } from "@sourcebot/logger"; +import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js"; +import micromatch from "micromatch"; +import { PrismaClient } from "@sourcebot/db"; +import { BackendException, BackendError } from "@sourcebot/error"; +import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js"; +import * as Sentry from "@sentry/node"; +import * as azdev from "azure-devops-node-api"; +import { GitRepository } from "azure-devops-node-api/interfaces/GitInterfaces.js"; + +const logger = createLogger('azuredevops'); +const AZUREDEVOPS_CLOUD_HOSTNAME = "dev.azure.com"; + + +function buildOrgUrl(baseUrl: string, org: string, useTfsPath: boolean): string { + const tfsSegment = useTfsPath ? '/tfs' : ''; + return `${baseUrl}${tfsSegment}/${org}`; +} + +function createAzureDevOpsConnection( + orgUrl: string, + token: string, +): azdev.WebApi { + const authHandler = azdev.getPersonalAccessTokenHandler(token); + return new azdev.WebApi(orgUrl, authHandler); +} + +export const getAzureDevOpsReposFromConfig = async ( + config: AzureDevOpsConnectionConfig, + orgId: number, + db: PrismaClient +) => { + const baseUrl = config.url || `https://${AZUREDEVOPS_CLOUD_HOSTNAME}`; + + const token = config.token ? + await getTokenFromConfig(config.token, orgId, db, logger) : + undefined; + + if (!token) { + const e = new BackendException(BackendError.CONNECTION_SYNC_INVALID_TOKEN, { + message: 'Azure DevOps requires a Personal Access Token', + }); + Sentry.captureException(e); + throw e; + } + + const useTfsPath = config.useTfsPath || false; + let allRepos: GitRepository[] = []; + let notFound: { + users: string[], + orgs: string[], + repos: string[], + } = { + users: [], + orgs: [], + repos: [], + }; + + if (config.orgs) { + const { validRepos, notFoundOrgs } = await getReposForOrganizations( + config.orgs, + baseUrl, + token, + useTfsPath + ); + allRepos = allRepos.concat(validRepos); + notFound.orgs = notFoundOrgs; + } + + if (config.projects) { + const { validRepos, notFoundProjects } = await getReposForProjects( + config.projects, + baseUrl, + token, + useTfsPath + ); + allRepos = allRepos.concat(validRepos); + notFound.repos = notFound.repos.concat(notFoundProjects); + } + + if (config.repos) { + const { validRepos, notFoundRepos } = await getRepos( + config.repos, + baseUrl, + token, + useTfsPath + ); + allRepos = allRepos.concat(validRepos); + notFound.repos = notFound.repos.concat(notFoundRepos); + } + + let repos = allRepos + .filter((repo) => { + const isExcluded = shouldExcludeRepo({ + repo, + exclude: config.exclude, + }); + + return !isExcluded; + }); + + logger.debug(`Found ${repos.length} total repositories.`); + + return { + validRepos: repos, + notFound, + }; +}; + +export const shouldExcludeRepo = ({ + repo, + exclude +}: { + repo: GitRepository, + exclude?: AzureDevOpsConnectionConfig['exclude'] +}) => { + let reason = ''; + const repoName = `${repo.project!.name}/${repo.name}`; + + const shouldExclude = (() => { + if (!repo.remoteUrl) { + reason = 'remoteUrl is undefined'; + return true; + } + + if (!!exclude?.disabled && repo.isDisabled) { + reason = `\`exclude.disabled\` is true`; + return true; + } + + if (exclude?.repos) { + if (micromatch.isMatch(repoName, exclude.repos)) { + reason = `\`exclude.repos\` contains ${repoName}`; + return true; + } + } + + if (exclude?.projects) { + if (micromatch.isMatch(repo.project!.name!, exclude.projects)) { + reason = `\`exclude.projects\` contains ${repo.project!.name}`; + return true; + } + } + + const repoSizeInBytes = repo.size || 0; + if (exclude?.size && repoSizeInBytes) { + const min = exclude.size.min; + const max = exclude.size.max; + + if (min && repoSizeInBytes < min) { + reason = `repo is less than \`exclude.size.min\`=${min} bytes.`; + return true; + } + + if (max && repoSizeInBytes > max) { + reason = `repo is greater than \`exclude.size.max\`=${max} bytes.`; + return true; + } + } + + return false; + })(); + + if (shouldExclude) { + logger.debug(`Excluding repo ${repoName}. Reason: ${reason}`); + return true; + } + + return false; +}; + +async function getReposForOrganizations( + organizations: string[], + baseUrl: string, + token: string, + useTfsPath: boolean +) { + const results = await Promise.allSettled(organizations.map(async (org) => { + try { + logger.debug(`Fetching repositories for organization ${org}...`); + + const { durationMs, data } = await measure(async () => { + const fetchFn = async () => { + const orgUrl = buildOrgUrl(baseUrl, org, useTfsPath); + const connection = createAzureDevOpsConnection(orgUrl, token); // useTfsPath already handled in orgUrl + + const coreApi = await connection.getCoreApi(); + const gitApi = await connection.getGitApi(); + + const projects = await coreApi.getProjects(); + const allRepos: GitRepository[] = []; + for (const project of projects) { + if (!project.id) { + logger.warn(`Encountered project in org ${org} with no id: ${project.name}`); + continue; + } + + try { + const repos = await gitApi.getRepositories(project.id); + allRepos.push(...repos); + } catch (error) { + logger.warn(`Failed to fetch repositories for project ${project.name}: ${error}`); + } + } + + return allRepos; + }; + + return fetchWithRetry(fetchFn, `organization ${org}`, logger); + }); + + logger.debug(`Found ${data.length} repositories in organization ${org} in ${durationMs}ms.`); + return { + type: 'valid' as const, + data + }; + } catch (error) { + Sentry.captureException(error); + logger.error(`Failed to fetch repositories for organization ${org}.`, error); + + // Check if it's a 404-like error (organization not found) + if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) { + logger.error(`Organization ${org} not found or no access`); + return { + type: 'notFound' as const, + value: org + }; + } + throw error; + } + })); + + throwIfAnyFailed(results); + const { validItems: validRepos, notFoundItems: notFoundOrgs } = processPromiseResults(results); + + return { + validRepos, + notFoundOrgs, + }; +} + +async function getReposForProjects( + projects: string[], + baseUrl: string, + token: string, + useTfsPath: boolean +) { + const results = await Promise.allSettled(projects.map(async (project) => { + try { + const [org, projectName] = project.split('/'); + logger.debug(`Fetching repositories for project ${project}...`); + + const { durationMs, data } = await measure(async () => { + const fetchFn = async () => { + const orgUrl = buildOrgUrl(baseUrl, org, useTfsPath); + const connection = createAzureDevOpsConnection(orgUrl, token); + const gitApi = await connection.getGitApi(); + + const repos = await gitApi.getRepositories(projectName); + return repos; + }; + + return fetchWithRetry(fetchFn, `project ${project}`, logger); + }); + + logger.debug(`Found ${data.length} repositories in project ${project} in ${durationMs}ms.`); + return { + type: 'valid' as const, + data + }; + } catch (error) { + Sentry.captureException(error); + logger.error(`Failed to fetch repositories for project ${project}.`, error); + + if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) { + logger.error(`Project ${project} not found or no access`); + return { + type: 'notFound' as const, + value: project + }; + } + throw error; + } + })); + + throwIfAnyFailed(results); + const { validItems: validRepos, notFoundItems: notFoundProjects } = processPromiseResults(results); + + return { + validRepos, + notFoundProjects, + }; +} + +async function getRepos( + repoList: string[], + baseUrl: string, + token: string, + useTfsPath: boolean +) { + const results = await Promise.allSettled(repoList.map(async (repo) => { + try { + const [org, projectName, repoName] = repo.split('/'); + logger.info(`Fetching repository info for ${repo}...`); + + const { durationMs, data: result } = await measure(async () => { + const fetchFn = async () => { + const orgUrl = buildOrgUrl(baseUrl, org, useTfsPath); + const connection = createAzureDevOpsConnection(orgUrl, token); + const gitApi = await connection.getGitApi(); + + const repo = await gitApi.getRepository(repoName, projectName); + return repo; + }; + + return fetchWithRetry(fetchFn, repo, logger); + }); + + logger.info(`Found info for repository ${repo} in ${durationMs}ms`); + return { + type: 'valid' as const, + data: [result] + }; + + } catch (error) { + Sentry.captureException(error); + logger.error(`Failed to fetch repository ${repo}.`, error); + + if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) { + logger.error(`Repository ${repo} not found or no access`); + return { + type: 'notFound' as const, + value: repo + }; + } + throw error; + } + })); + + throwIfAnyFailed(results); + const { validItems: validRepos, notFoundItems: notFoundRepos } = processPromiseResults(results); + + return { + validRepos, + notFoundRepos, + }; +} \ No newline at end of file diff --git a/packages/backend/src/bitbucket.ts b/packages/backend/src/bitbucket.ts index e204850c..cfa591cc 100644 --- a/packages/backend/src/bitbucket.ts +++ b/packages/backend/src/bitbucket.ts @@ -148,13 +148,14 @@ function cloudClient(user: string | undefined, token: string | undefined): Bitbu **/ const getPaginatedCloud = async ( path: CloudGetRequestPath, - get: (url: CloudGetRequestPath) => Promise> + get: (path: CloudGetRequestPath, query?: Record) => Promise> ): Promise => { const results: T[] = []; - let url = path; + let nextPath = path; + let nextQuery = undefined; while (true) { - const response = await get(url); + const response = await get(nextPath, nextQuery); if (!response.values || response.values.length === 0) { break; @@ -166,25 +167,38 @@ const getPaginatedCloud = async ( break; } - url = response.next as CloudGetRequestPath; + const parsedUrl = parseUrl(response.next); + nextPath = parsedUrl.path as CloudGetRequestPath; + nextQuery = parsedUrl.query; } return results; } - + +/** + * Parse the url into a path and query parameters to be used with the api client (openapi-fetch) + */ +function parseUrl(url: string): { path: string; query: Record; } { + const fullUrl = new URL(url); + const path = fullUrl.pathname.replace(/^\/\d+(\.\d+)*/, ''); // remove version number in the beginning of the path + const query = Object.fromEntries(fullUrl.searchParams); + logger.debug(`Parsed url ${url} into path ${path} and query ${JSON.stringify(query)}`); + return { path, query }; +} + async function cloudGetReposForWorkspace(client: BitbucketClient, workspaces: string[]): Promise<{validRepos: CloudRepository[], notFoundWorkspaces: string[]}> { const results = await Promise.allSettled(workspaces.map(async (workspace) => { try { logger.debug(`Fetching all repos for workspace ${workspace}...`); - const path = `/repositories/${workspace}` as CloudGetRequestPath; const { durationMs, data } = await measure(async () => { - const fetchFn = () => getPaginatedCloud(path, async (url) => { - const response = await client.apiClient.GET(url, { + const fetchFn = () => getPaginatedCloud(`/repositories/${workspace}` as CloudGetRequestPath, async (path, query) => { + const response = await client.apiClient.GET(path, { params: { path: { workspace, - } + }, + query: query, } }); const { data, error } = response; @@ -238,11 +252,14 @@ async function cloudGetReposForProjects(client: BitbucketClient, projects: strin logger.debug(`Fetching all repos for project ${project} for workspace ${workspace}...`); try { - const path = `/repositories/${workspace}` as CloudGetRequestPath; - const repos = await getPaginatedCloud(path, async (url) => { - const response = await client.apiClient.GET(url, { + const repos = await getPaginatedCloud(`/repositories/${workspace}` as CloudGetRequestPath, async (path, query) => { + const response = await client.apiClient.GET(path, { params: { + path: { + workspace, + }, query: { + ...query, q: `project.key="${project_name}"` } } diff --git a/packages/backend/src/connectionManager.ts b/packages/backend/src/connectionManager.ts index f025bdf7..ebbffe73 100644 --- a/packages/backend/src/connectionManager.ts +++ b/packages/backend/src/connectionManager.ts @@ -4,19 +4,13 @@ import { Settings } from "./types.js"; import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type"; import { createLogger } from "@sourcebot/logger"; import { Redis } from 'ioredis'; -import { RepoData, compileGithubConfig, compileGitlabConfig, compileGiteaConfig, compileGerritConfig, compileBitbucketConfig, compileGenericGitHostConfig } from "./repoCompileUtils.js"; +import { RepoData, compileGithubConfig, compileGitlabConfig, compileGiteaConfig, compileGerritConfig, compileBitbucketConfig, compileAzureDevOpsConfig, compileGenericGitHostConfig } from "./repoCompileUtils.js"; import { BackendError, BackendException } from "@sourcebot/error"; import { captureEvent } from "./posthog.js"; import { env } from "./env.js"; import * as Sentry from "@sentry/node"; import { loadConfig, syncSearchContexts } from "@sourcebot/shared"; -interface IConnectionManager { - scheduleConnectionSync: (connection: Connection) => Promise; - registerPollingCallback: () => void; - dispose: () => void; -} - const QUEUE_NAME = 'connectionSyncQueue'; type JobPayload = { @@ -30,10 +24,11 @@ type JobResult = { repoCount: number, } -export class ConnectionManager implements IConnectionManager { +export class ConnectionManager { private worker: Worker; private queue: Queue; private logger = createLogger('connection-manager'); + private interval?: NodeJS.Timeout; constructor( private db: PrismaClient, @@ -75,8 +70,9 @@ export class ConnectionManager implements IConnectionManager { }); } - public async registerPollingCallback() { - setInterval(async () => { + public startScheduler() { + this.logger.debug('Starting scheduler'); + this.interval = setInterval(async () => { const thresholdDate = new Date(Date.now() - this.settings.resyncConnectionIntervalMs); const connections = await this.db.connection.findMany({ where: { @@ -177,6 +173,9 @@ export class ConnectionManager implements IConnectionManager { case 'bitbucket': { return await compileBitbucketConfig(config, job.data.connectionId, orgId, this.db); } + case 'azuredevops': { + return await compileAzureDevOpsConfig(config, job.data.connectionId, orgId, this.db, abortController); + } case 'git': { return await compileGenericGitHostConfig(config, job.data.connectionId, orgId); } @@ -366,6 +365,9 @@ export class ConnectionManager implements IConnectionManager { } public dispose() { + if (this.interval) { + clearInterval(this.interval); + } this.worker.close(); this.queue.close(); } diff --git a/packages/backend/src/constants.ts b/packages/backend/src/constants.ts index c0d77f05..89778fb2 100644 --- a/packages/backend/src/constants.ts +++ b/packages/backend/src/constants.ts @@ -15,5 +15,11 @@ export const DEFAULT_SETTINGS: Settings = { maxRepoGarbageCollectionJobConcurrency: 8, repoGarbageCollectionGracePeriodMs: 10 * 1000, // 10 seconds repoIndexTimeoutMs: 1000 * 60 * 60 * 2, // 2 hours - enablePublicAccess: false // deprected, use FORCE_ENABLE_ANONYMOUS_ACCESS instead + enablePublicAccess: false, // deprected, use FORCE_ENABLE_ANONYMOUS_ACCESS instead + experiment_repoDrivenPermissionSyncIntervalMs: 1000 * 60 * 60 * 24, // 24 hours + experiment_userDrivenPermissionSyncIntervalMs: 1000 * 60 * 60 * 24, // 24 hours } + +export const PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES = [ + 'github', +]; \ No newline at end of file diff --git a/packages/backend/src/ee/repoPermissionSyncer.ts b/packages/backend/src/ee/repoPermissionSyncer.ts new file mode 100644 index 00000000..f411c3e3 --- /dev/null +++ b/packages/backend/src/ee/repoPermissionSyncer.ts @@ -0,0 +1,274 @@ +import * as Sentry from "@sentry/node"; +import { PrismaClient, Repo, RepoPermissionSyncJobStatus } from "@sourcebot/db"; +import { createLogger } from "@sourcebot/logger"; +import { hasEntitlement } from "@sourcebot/shared"; +import { Job, Queue, Worker } from 'bullmq'; +import { Redis } from 'ioredis'; +import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "../constants.js"; +import { env } from "../env.js"; +import { createOctokitFromToken, getRepoCollaborators } from "../github.js"; +import { Settings } from "../types.js"; +import { getAuthCredentialsForRepo } from "../utils.js"; + +type RepoPermissionSyncJob = { + jobId: string; +} + +const QUEUE_NAME = 'repoPermissionSyncQueue'; + +const logger = createLogger('repo-permission-syncer'); + + +export class RepoPermissionSyncer { + private queue: Queue; + private worker: Worker; + private interval?: NodeJS.Timeout; + + constructor( + private db: PrismaClient, + private settings: Settings, + redis: Redis, + ) { + this.queue = new Queue(QUEUE_NAME, { + connection: redis, + }); + this.worker = new Worker(QUEUE_NAME, this.runJob.bind(this), { + connection: redis, + concurrency: 1, + }); + this.worker.on('completed', this.onJobCompleted.bind(this)); + this.worker.on('failed', this.onJobFailed.bind(this)); + } + + public startScheduler() { + if (!hasEntitlement('permission-syncing')) { + throw new Error('Permission syncing is not supported in current plan.'); + } + + logger.debug('Starting scheduler'); + + this.interval = setInterval(async () => { + // @todo: make this configurable + const thresholdDate = new Date(Date.now() - this.settings.experiment_repoDrivenPermissionSyncIntervalMs); + + const repos = await this.db.repo.findMany({ + // Repos need their permissions to be synced against the code host when... + where: { + // They belong to a code host that supports permissions syncing + AND: [ + { + external_codeHostType: { + in: PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES, + } + }, + { + OR: [ + { permissionSyncedAt: null }, + { permissionSyncedAt: { lt: thresholdDate } }, + ], + }, + { + NOT: { + permissionSyncJobs: { + some: { + OR: [ + // Don't schedule if there are active jobs + { + status: { + in: [ + RepoPermissionSyncJobStatus.PENDING, + RepoPermissionSyncJobStatus.IN_PROGRESS, + ], + } + }, + // Don't schedule if there are recent failed jobs (within the threshold date). Note `gt` is used here since this is a inverse condition. + { + AND: [ + { status: RepoPermissionSyncJobStatus.FAILED }, + { completedAt: { gt: thresholdDate } }, + ] + } + ] + } + } + } + }, + ] + } + }); + + await this.schedulePermissionSync(repos); + }, 1000 * 5); + } + + public dispose() { + if (this.interval) { + clearInterval(this.interval); + } + this.worker.close(); + this.queue.close(); + } + + private async schedulePermissionSync(repos: Repo[]) { + await this.db.$transaction(async (tx) => { + const jobs = await tx.repoPermissionSyncJob.createManyAndReturn({ + data: repos.map(repo => ({ + repoId: repo.id, + })), + }); + + await this.queue.addBulk(jobs.map((job) => ({ + name: 'repoPermissionSyncJob', + data: { + jobId: job.id, + }, + opts: { + removeOnComplete: env.REDIS_REMOVE_ON_COMPLETE, + removeOnFail: env.REDIS_REMOVE_ON_FAIL, + } + }))) + }); + } + + private async runJob(job: Job) { + const id = job.data.jobId; + const { repo } = await this.db.repoPermissionSyncJob.update({ + where: { + id, + }, + data: { + status: RepoPermissionSyncJobStatus.IN_PROGRESS, + }, + select: { + repo: { + include: { + connections: { + include: { + connection: true, + } + } + } + } + } + }); + + if (!repo) { + throw new Error(`Repo ${id} not found`); + } + + logger.info(`Syncing permissions for repo ${repo.displayName}...`); + + const credentials = await getAuthCredentialsForRepo(repo, this.db, logger); + if (!credentials) { + throw new Error(`No credentials found for repo ${id}`); + } + + const userIds = await (async () => { + if (repo.external_codeHostType === 'github') { + const { octokit } = await createOctokitFromToken({ + token: credentials.token, + url: credentials.hostUrl, + }); + + // @note: this is a bit of a hack since the displayName _might_ not be set.. + // however, this property was introduced many versions ago and _should_ be set + // on each connection sync. Let's throw an error just in case. + if (!repo.displayName) { + throw new Error(`Repo ${id} does not have a displayName`); + } + + const [owner, repoName] = repo.displayName.split('/'); + + const collaborators = await getRepoCollaborators(owner, repoName, octokit); + const githubUserIds = collaborators.map(collaborator => collaborator.id.toString()); + + const accounts = await this.db.account.findMany({ + where: { + provider: 'github', + providerAccountId: { + in: githubUserIds, + } + }, + select: { + userId: true, + }, + }); + + return accounts.map(account => account.userId); + } + + return []; + })(); + + await this.db.$transaction([ + this.db.repo.update({ + where: { + id: repo.id, + }, + data: { + permittedUsers: { + deleteMany: {}, + } + } + }), + this.db.userToRepoPermission.createMany({ + data: userIds.map(userId => ({ + userId, + repoId: repo.id, + })), + }) + ]); + } + + private async onJobCompleted(job: Job) { + const { repo } = await this.db.repoPermissionSyncJob.update({ + where: { + id: job.data.jobId, + }, + data: { + status: RepoPermissionSyncJobStatus.COMPLETED, + repo: { + update: { + permissionSyncedAt: new Date(), + } + }, + completedAt: new Date(), + }, + select: { + repo: true + } + }); + + logger.info(`Permissions synced for repo ${repo.displayName ?? repo.name}`); + } + + private async onJobFailed(job: Job | undefined, err: Error) { + Sentry.captureException(err, { + tags: { + jobId: job?.data.jobId, + queue: QUEUE_NAME, + } + }); + + const errorMessage = (repoName: string) => `Repo permission sync job failed for repo ${repoName}: ${err.message}`; + + if (job) { + const { repo } = await this.db.repoPermissionSyncJob.update({ + where: { + id: job.data.jobId, + }, + data: { + status: RepoPermissionSyncJobStatus.FAILED, + completedAt: new Date(), + errorMessage: err.message, + }, + select: { + repo: true + }, + }); + logger.error(errorMessage(repo.displayName ?? repo.name)); + } else { + logger.error(errorMessage('unknown repo (id not found)')); + } + } +} diff --git a/packages/backend/src/ee/userPermissionSyncer.ts b/packages/backend/src/ee/userPermissionSyncer.ts new file mode 100644 index 00000000..90ae8629 --- /dev/null +++ b/packages/backend/src/ee/userPermissionSyncer.ts @@ -0,0 +1,266 @@ +import * as Sentry from "@sentry/node"; +import { PrismaClient, User, UserPermissionSyncJobStatus } from "@sourcebot/db"; +import { createLogger } from "@sourcebot/logger"; +import { Job, Queue, Worker } from "bullmq"; +import { Redis } from "ioredis"; +import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "../constants.js"; +import { env } from "../env.js"; +import { createOctokitFromToken, getReposForAuthenticatedUser } from "../github.js"; +import { hasEntitlement } from "@sourcebot/shared"; +import { Settings } from "../types.js"; + +const logger = createLogger('user-permission-syncer'); + +const QUEUE_NAME = 'userPermissionSyncQueue'; + +type UserPermissionSyncJob = { + jobId: string; +} + + +export class UserPermissionSyncer { + private queue: Queue; + private worker: Worker; + private interval?: NodeJS.Timeout; + + constructor( + private db: PrismaClient, + private settings: Settings, + redis: Redis, + ) { + this.queue = new Queue(QUEUE_NAME, { + connection: redis, + }); + this.worker = new Worker(QUEUE_NAME, this.runJob.bind(this), { + connection: redis, + concurrency: 1, + }); + this.worker.on('completed', this.onJobCompleted.bind(this)); + this.worker.on('failed', this.onJobFailed.bind(this)); + } + + public startScheduler() { + if (!hasEntitlement('permission-syncing')) { + throw new Error('Permission syncing is not supported in current plan.'); + } + + logger.debug('Starting scheduler'); + + this.interval = setInterval(async () => { + const thresholdDate = new Date(Date.now() - this.settings.experiment_userDrivenPermissionSyncIntervalMs); + + const users = await this.db.user.findMany({ + where: { + AND: [ + { + accounts: { + some: { + provider: { + in: PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES + } + } + } + }, + { + OR: [ + { permissionSyncedAt: null }, + { permissionSyncedAt: { lt: thresholdDate } }, + ] + }, + { + NOT: { + permissionSyncJobs: { + some: { + OR: [ + // Don't schedule if there are active jobs + { + status: { + in: [ + UserPermissionSyncJobStatus.PENDING, + UserPermissionSyncJobStatus.IN_PROGRESS, + ], + } + }, + // Don't schedule if there are recent failed jobs (within the threshold date). Note `gt` is used here since this is a inverse condition. + { + AND: [ + { status: UserPermissionSyncJobStatus.FAILED }, + { completedAt: { gt: thresholdDate } }, + ] + } + ] + } + } + } + }, + ] + } + }); + + await this.schedulePermissionSync(users); + }, 1000 * 5); + } + + public dispose() { + if (this.interval) { + clearInterval(this.interval); + } + this.worker.close(); + this.queue.close(); + } + + private async schedulePermissionSync(users: User[]) { + await this.db.$transaction(async (tx) => { + const jobs = await tx.userPermissionSyncJob.createManyAndReturn({ + data: users.map(user => ({ + userId: user.id, + })), + }); + + await this.queue.addBulk(jobs.map((job) => ({ + name: 'userPermissionSyncJob', + data: { + jobId: job.id, + }, + opts: { + removeOnComplete: env.REDIS_REMOVE_ON_COMPLETE, + removeOnFail: env.REDIS_REMOVE_ON_FAIL, + } + }))) + }); + } + + private async runJob(job: Job) { + const id = job.data.jobId; + const { user } = await this.db.userPermissionSyncJob.update({ + where: { + id, + }, + data: { + status: UserPermissionSyncJobStatus.IN_PROGRESS, + }, + select: { + user: { + include: { + accounts: true, + } + } + } + }); + + if (!user) { + throw new Error(`User ${id} not found`); + } + + logger.info(`Syncing permissions for user ${user.email}...`); + + // Get a list of all repos that the user has access to from all connected accounts. + const repoIds = await (async () => { + const aggregatedRepoIds: Set = new Set(); + + for (const account of user.accounts) { + if (account.provider === 'github') { + if (!account.access_token) { + throw new Error(`User '${user.email}' does not have an GitHub OAuth access token associated with their GitHub account.`); + } + + const { octokit } = await createOctokitFromToken({ + token: account.access_token, + url: env.AUTH_EE_GITHUB_BASE_URL, + }); + // @note: we only care about the private repos since we don't need to build a mapping + // for public repos. + // @see: packages/web/src/prisma.ts + const githubRepos = await getReposForAuthenticatedUser(/* visibility = */ 'private', octokit); + const gitHubRepoIds = githubRepos.map(repo => repo.id.toString()); + + const repos = await this.db.repo.findMany({ + where: { + external_codeHostType: 'github', + external_id: { + in: gitHubRepoIds, + } + } + }); + + repos.forEach(repo => aggregatedRepoIds.add(repo.id)); + } + } + + return Array.from(aggregatedRepoIds); + })(); + + await this.db.$transaction([ + this.db.user.update({ + where: { + id: user.id, + }, + data: { + accessibleRepos: { + deleteMany: {}, + } + } + }), + this.db.userToRepoPermission.createMany({ + data: repoIds.map(repoId => ({ + userId: user.id, + repoId, + })), + skipDuplicates: true, + }) + ]); + } + + private async onJobCompleted(job: Job) { + const { user } = await this.db.userPermissionSyncJob.update({ + where: { + id: job.data.jobId, + }, + data: { + status: UserPermissionSyncJobStatus.COMPLETED, + user: { + update: { + permissionSyncedAt: new Date(), + } + }, + completedAt: new Date(), + }, + select: { + user: true + } + }); + + logger.info(`Permissions synced for user ${user.email}`); + } + + private async onJobFailed(job: Job | undefined, err: Error) { + Sentry.captureException(err, { + tags: { + jobId: job?.data.jobId, + queue: QUEUE_NAME, + } + }); + + const errorMessage = (email: string) => `User permission sync job failed for user ${email}: ${err.message}`; + + if (job) { + const { user } = await this.db.userPermissionSyncJob.update({ + where: { + id: job.data.jobId, + }, + data: { + status: UserPermissionSyncJobStatus.FAILED, + completedAt: new Date(), + errorMessage: err.message, + }, + select: { + user: true, + } + }); + + logger.error(errorMessage(user.email ?? user.id)); + } else { + logger.error(errorMessage('unknown user (id not found)')); + } + } +} \ No newline at end of file diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts index 735361a8..80bbba5e 100644 --- a/packages/backend/src/env.ts +++ b/packages/backend/src/env.ts @@ -43,6 +43,7 @@ export const env = createEnv({ LOGTAIL_TOKEN: z.string().optional(), LOGTAIL_HOST: z.string().url().optional(), + SOURCEBOT_LOG_LEVEL: z.enum(["info", "debug", "warn", "error"]).default("info"), DATABASE_URL: z.string().url().default("postgresql://postgres:postgres@localhost:5432/postgres"), CONFIG_PATH: z.string().optional(), @@ -51,6 +52,9 @@ export const env = createEnv({ REPO_SYNC_RETRY_BASE_SLEEP_SECONDS: numberSchema.default(60), GITLAB_CLIENT_QUERY_TIMEOUT_SECONDS: numberSchema.default(60 * 10), + + EXPERIMENT_EE_PERMISSION_SYNC_ENABLED: booleanSchema.default('false'), + AUTH_EE_GITHUB_BASE_URL: z.string().optional(), }, runtimeEnv: process.env, emptyStringAsUndefined: true, diff --git a/packages/backend/src/git.ts b/packages/backend/src/git.ts index d3dfc76f..c1110625 100644 --- a/packages/backend/src/git.ts +++ b/packages/backend/src/git.ts @@ -1,53 +1,102 @@ import { CheckRepoActions, GitConfigScope, simpleGit, SimpleGitProgressEvent } from 'simple-git'; +import { mkdir } from 'node:fs/promises'; +import { env } from './env.js'; type onProgressFn = (event: SimpleGitProgressEvent) => void; -export const cloneRepository = async (cloneURL: string, path: string, onProgress?: onProgressFn) => { - const git = simpleGit({ - progress: onProgress, - }); +export const cloneRepository = async ( + { + cloneUrl, + authHeader, + path, + onProgress, + }: { + cloneUrl: string, + authHeader?: string, + path: string, + onProgress?: onProgressFn + } +) => { try { - await git.clone( - cloneURL, - path, - [ - "--bare", - ] - ); + await mkdir(path, { recursive: true }); - await git.cwd({ + const git = simpleGit({ + progress: onProgress, + }).cwd({ path, - }).addConfig("remote.origin.fetch", "+refs/heads/*:refs/heads/*"); + }) + + const cloneArgs = [ + "--bare", + ...(authHeader ? ["-c", `http.extraHeader=${authHeader}`] : []) + ]; + + await git.clone(cloneUrl, path, cloneArgs); + + await unsetGitConfig(path, ["remote.origin.url"]); } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(`Failed to clone repository: ${error.message}`); + const baseLog = `Failed to clone repository: ${path}`; + + if (env.SOURCEBOT_LOG_LEVEL !== "debug") { + // Avoid printing the remote URL (that may contain credentials) to logs by default. + throw new Error(`${baseLog}. Set environment variable SOURCEBOT_LOG_LEVEL=debug to see the full error message.`); + } else if (error instanceof Error) { + throw new Error(`${baseLog}. Reason: ${error.message}`); } else { - throw new Error(`Failed to clone repository: ${error}`); + throw new Error(`${baseLog}. Error: ${error}`); } } -} - - -export const fetchRepository = async (path: string, onProgress?: onProgressFn) => { - const git = simpleGit({ - progress: onProgress, - }); +}; +export const fetchRepository = async ( + { + cloneUrl, + authHeader, + path, + onProgress, + }: { + cloneUrl: string, + authHeader?: string, + path: string, + onProgress?: onProgressFn + } +) => { try { - await git.cwd({ + const git = simpleGit({ + progress: onProgress, + }).cwd({ path: path, - }).fetch( - "origin", - [ - "--prune", - "--progress" - ] - ); + }) + + if (authHeader) { + await git.addConfig("http.extraHeader", authHeader); + } + + await git.fetch([ + cloneUrl, + "+refs/heads/*:refs/heads/*", + "--prune", + "--progress" + ]); } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(`Failed to fetch repository ${path}: ${error.message}`); + const baseLog = `Failed to fetch repository: ${path}`; + if (env.SOURCEBOT_LOG_LEVEL !== "debug") { + // Avoid printing the remote URL (that may contain credentials) to logs by default. + throw new Error(`${baseLog}. Set environment variable SOURCEBOT_LOG_LEVEL=debug to see the full error message.`); + } else if (error instanceof Error) { + throw new Error(`${baseLog}. Reason: ${error.message}`); } else { - throw new Error(`Failed to fetch repository ${path}: ${error}`); + throw new Error(`${baseLog}. Error: ${error}`); + } + } finally { + if (authHeader) { + const git = simpleGit({ + progress: onProgress, + }).cwd({ + path: path, + }) + + await git.raw(["config", "--unset", "http.extraHeader", authHeader]); } } } @@ -76,6 +125,33 @@ export const upsertGitConfig = async (path: string, gitConfig: Record { + const git = simpleGit({ + progress: onProgress, + }).cwd(path); + + try { + const configList = await git.listConfig(); + const setKeys = Object.keys(configList.all); + + for (const key of keys) { + if (setKeys.includes(key)) { + await git.raw(['config', '--unset', key]); + } + } + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error(`Failed to unset git config ${path}: ${error.message}`); + } else { + throw new Error(`Failed to unset git config ${path}: ${error}`); + } + } +} + /** * Returns true if `path` is the _root_ of a git repository. */ diff --git a/packages/backend/src/github.ts b/packages/backend/src/github.ts index 376ed039..2b42eed2 100644 --- a/packages/backend/src/github.ts +++ b/packages/backend/src/github.ts @@ -30,16 +30,31 @@ export type OctokitRepository = { size?: number, owner: { avatar_url: string, + login: string, } } const isHttpError = (error: unknown, status: number): boolean => { - return error !== null + return error !== null && typeof error === 'object' - && 'status' in error + && 'status' in error && error.status === status; } +export const createOctokitFromToken = async ({ token, url }: { token?: string, url?: string }): Promise<{ octokit: Octokit, isAuthenticated: boolean }> => { + const octokit = new Octokit({ + auth: token, + ...(url ? { + baseUrl: `${url}/api/v3` + } : {}), + }); + + return { + octokit, + isAuthenticated: !!token, + }; +} + export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, orgId: number, db: PrismaClient, signal: AbortSignal) => { const hostname = config.url ? new URL(config.url).hostname : @@ -48,17 +63,15 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o const token = config.token ? await getTokenFromConfig(config.token, orgId, db, logger) : hostname === GITHUB_CLOUD_HOSTNAME ? - env.FALLBACK_GITHUB_CLOUD_TOKEN : - undefined; + env.FALLBACK_GITHUB_CLOUD_TOKEN : + undefined; - const octokit = new Octokit({ - auth: token, - ...(config.url ? { - baseUrl: `${config.url}/api/v3` - } : {}), + const { octokit, isAuthenticated } = await createOctokitFromToken({ + token, + url: config.url, }); - if (token) { + if (isAuthenticated) { try { await octokit.rest.users.getAuthenticated(); } catch (error) { @@ -106,8 +119,7 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o } if (config.users) { - const isAuthenticated = config.token !== undefined; - const { validRepos, notFoundUsers } = await getReposOwnedByUsers(config.users, isAuthenticated, octokit, signal); + const { validRepos, notFoundUsers } = await getReposOwnedByUsers(config.users, octokit, signal); allRepos = allRepos.concat(validRepos); notFound.users = notFoundUsers; } @@ -128,123 +140,69 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o logger.debug(`Found ${repos.length} total repositories.`); return { - validRepos: repos, + validRepos: repos, notFound, }; } -export const shouldExcludeRepo = ({ - repo, - include, - exclude -} : { - repo: OctokitRepository, - include?: { - topics?: GithubConnectionConfig['topics'] - }, - exclude?: GithubConnectionConfig['exclude'] -}) => { - let reason = ''; - const repoName = repo.full_name; +export const getRepoCollaborators = async (owner: string, repo: string, octokit: Octokit) => { + try { + const fetchFn = () => octokit.paginate(octokit.repos.listCollaborators, { + owner, + repo, + per_page: 100, + }); - const shouldExclude = (() => { - if (!repo.clone_url) { - reason = 'clone_url is undefined'; - return true; - } - - if (!!exclude?.forks && repo.fork) { - reason = `\`exclude.forks\` is true`; - return true; - } - - if (!!exclude?.archived && !!repo.archived) { - reason = `\`exclude.archived\` is true`; - return true; - } - - if (exclude?.repos) { - if (micromatch.isMatch(repoName, exclude.repos)) { - reason = `\`exclude.repos\` contains ${repoName}`; - return true; - } - } - - if (exclude?.topics) { - const configTopics = exclude.topics.map(topic => topic.toLowerCase()); - const repoTopics = repo.topics ?? []; - - const matchingTopics = repoTopics.filter((topic) => micromatch.isMatch(topic, configTopics)); - if (matchingTopics.length > 0) { - reason = `\`exclude.topics\` matches the following topics: ${matchingTopics.join(', ')}`; - return true; - } - } - - if (include?.topics) { - const configTopics = include.topics.map(topic => topic.toLowerCase()); - const repoTopics = repo.topics ?? []; - - const matchingTopics = repoTopics.filter((topic) => micromatch.isMatch(topic, configTopics)); - if (matchingTopics.length === 0) { - reason = `\`include.topics\` does not match any of the following topics: ${configTopics.join(', ')}`; - return true; - } - } - - const repoSizeInBytes = repo.size ? repo.size * 1000 : undefined; - if (exclude?.size && repoSizeInBytes) { - const min = exclude.size.min; - const max = exclude.size.max; - - if (min && repoSizeInBytes < min) { - reason = `repo is less than \`exclude.size.min\`=${min} bytes.`; - return true; - } - - if (max && repoSizeInBytes > max) { - reason = `repo is greater than \`exclude.size.max\`=${max} bytes.`; - return true; - } - } - - return false; - })(); - - if (shouldExclude) { - logger.debug(`Excluding repo ${repoName}. Reason: ${reason}`); - return true; + const collaborators = await fetchWithRetry(fetchFn, `repo ${owner}/${repo}`, logger); + return collaborators; + } catch (error) { + Sentry.captureException(error); + logger.error(`Failed to fetch collaborators for repo ${owner}/${repo}.`, error); + throw error; } - - return false; } -const getReposOwnedByUsers = async (users: string[], isAuthenticated: boolean, octokit: Octokit, signal: AbortSignal) => { +export const getReposForAuthenticatedUser = async (visibility: 'all' | 'private' | 'public' = 'all', octokit: Octokit) => { + try { + const fetchFn = () => octokit.paginate(octokit.repos.listForAuthenticatedUser, { + per_page: 100, + visibility, + }); + + const repos = await fetchWithRetry(fetchFn, `authenticated user`, logger); + return repos; + } catch (error) { + Sentry.captureException(error); + logger.error(`Failed to fetch repositories for authenticated user.`, error); + throw error; + } +} + +const getReposOwnedByUsers = async (users: string[], octokit: Octokit, signal: AbortSignal) => { const results = await Promise.allSettled(users.map(async (user) => { try { logger.debug(`Fetching repository info for user ${user}...`); const { durationMs, data } = await measure(async () => { const fetchFn = async () => { - if (isAuthenticated) { - return octokit.paginate(octokit.repos.listForAuthenticatedUser, { - username: user, - visibility: 'all', - affiliation: 'owner', - per_page: 100, - request: { - signal, - }, - }); - } else { - return octokit.paginate(octokit.repos.listForUser, { - username: user, - per_page: 100, - request: { - signal, - }, - }); - } + let query = `user:${user}`; + // To include forks in the search results, we will need to add fork:true + // see: https://docs.github.com/en/search-github/searching-on-github/searching-for-repositories + query += ' fork:true'; + // @note: We need to use GitHub's search API here since it is the only way + // to get all repositories (private and public) owned by a user that supports + // the username as a parameter. + // @see: https://github.com/orgs/community/discussions/24382#discussioncomment-3243958 + // @see: https://api.github.com/search/repositories?q=user:USERNAME + const searchResults = await octokit.paginate(octokit.rest.search.repos, { + q: query, + per_page: 100, + request: { + signal, + }, + }); + + return searchResults as OctokitRepository[]; }; return fetchWithRetry(fetchFn, `user ${user}`, logger); @@ -371,4 +329,90 @@ const getRepos = async (repoList: string[], octokit: Octokit, signal: AbortSigna validRepos, notFoundRepos, }; -} \ No newline at end of file +} + +export const shouldExcludeRepo = ({ + repo, + include, + exclude +}: { + repo: OctokitRepository, + include?: { + topics?: GithubConnectionConfig['topics'] + }, + exclude?: GithubConnectionConfig['exclude'] +}) => { + let reason = ''; + const repoName = repo.full_name; + + const shouldExclude = (() => { + if (!repo.clone_url) { + reason = 'clone_url is undefined'; + return true; + } + + if (!!exclude?.forks && repo.fork) { + reason = `\`exclude.forks\` is true`; + return true; + } + + if (!!exclude?.archived && !!repo.archived) { + reason = `\`exclude.archived\` is true`; + return true; + } + + if (exclude?.repos) { + if (micromatch.isMatch(repoName, exclude.repos)) { + reason = `\`exclude.repos\` contains ${repoName}`; + return true; + } + } + + if (exclude?.topics) { + const configTopics = exclude.topics.map(topic => topic.toLowerCase()); + const repoTopics = repo.topics ?? []; + + const matchingTopics = repoTopics.filter((topic) => micromatch.isMatch(topic, configTopics)); + if (matchingTopics.length > 0) { + reason = `\`exclude.topics\` matches the following topics: ${matchingTopics.join(', ')}`; + return true; + } + } + + if (include?.topics) { + const configTopics = include.topics.map(topic => topic.toLowerCase()); + const repoTopics = repo.topics ?? []; + + const matchingTopics = repoTopics.filter((topic) => micromatch.isMatch(topic, configTopics)); + if (matchingTopics.length === 0) { + reason = `\`include.topics\` does not match any of the following topics: ${configTopics.join(', ')}`; + return true; + } + } + + const repoSizeInBytes = repo.size ? repo.size * 1000 : undefined; + if (exclude?.size && repoSizeInBytes) { + const min = exclude.size.min; + const max = exclude.size.max; + + if (min && repoSizeInBytes < min) { + reason = `repo is less than \`exclude.size.min\`=${min} bytes.`; + return true; + } + + if (max && repoSizeInBytes > max) { + reason = `repo is greater than \`exclude.size.max\`=${max} bytes.`; + return true; + } + } + + return false; + })(); + + if (shouldExclude) { + logger.debug(`Excluding repo ${repoName}. Reason: ${reason}`); + return true; + } + + return false; +} diff --git a/packages/backend/src/gitlab.test.ts b/packages/backend/src/gitlab.test.ts index 49ffe433..4d9190e0 100644 --- a/packages/backend/src/gitlab.test.ts +++ b/packages/backend/src/gitlab.test.ts @@ -41,3 +41,30 @@ test('shouldExcludeProject returns true when the project is excluded by exclude. })).toBe(true) }); +test('shouldExcludeProject returns true when the project is excluded by exclude.userOwnedProjects.', () => { + const project = { + path_with_namespace: 'test/project', + namespace: { + kind: 'user', + } + } as unknown as ProjectSchema; + + expect(shouldExcludeProject({ + project, + exclude: { + userOwnedProjects: true, + } + })).toBe(true) +}); + +test('shouldExcludeProject returns false when exclude.userOwnedProjects is true but project is group-owned.', () => { + const project = { + path_with_namespace: 'test/project', + namespace: { kind: 'group' }, + } as unknown as ProjectSchema; + + expect(shouldExcludeProject({ + project, + exclude: { userOwnedProjects: true }, + })).toBe(false); +}); diff --git a/packages/backend/src/gitlab.ts b/packages/backend/src/gitlab.ts index 79ab643b..d13692d3 100644 --- a/packages/backend/src/gitlab.ts +++ b/packages/backend/src/gitlab.ts @@ -222,6 +222,11 @@ export const shouldExcludeProject = ({ return true; } + if (exclude?.userOwnedProjects && project.namespace.kind === 'user') { + reason = `\`exclude.userOwnedProjects\` is true`; + return true; + } + if (exclude?.projects) { if (micromatch.isMatch(projectName, exclude.projects)) { reason = `\`exclude.projects\` contains ${projectName}`; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index c93622d6..93f95e0b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,44 +1,37 @@ import "./instrument.js"; -import * as Sentry from "@sentry/node"; +import { PrismaClient } from "@sourcebot/db"; +import { createLogger } from "@sourcebot/logger"; +import { hasEntitlement, loadConfig } from '@sourcebot/shared'; import { existsSync } from 'fs'; import { mkdir } from 'fs/promises'; +import { Redis } from 'ioredis'; import path from 'path'; -import { AppContext } from "./types.js"; -import { main } from "./main.js" -import { PrismaClient } from "@sourcebot/db"; +import { ConnectionManager } from './connectionManager.js'; +import { DEFAULT_SETTINGS } from './constants.js'; import { env } from "./env.js"; -import { createLogger } from "@sourcebot/logger"; +import { RepoPermissionSyncer } from './ee/repoPermissionSyncer.js'; +import { PromClient } from './promClient.js'; +import { RepoManager } from './repoManager.js'; +import { AppContext } from "./types.js"; +import { UserPermissionSyncer } from "./ee/userPermissionSyncer.js"; + const logger = createLogger('backend-entrypoint'); +const getSettings = async (configPath?: string) => { + if (!configPath) { + return DEFAULT_SETTINGS; + } -// Register handler for normal exit -process.on('exit', (code) => { - logger.info(`Process is exiting with code: ${code}`); -}); + const config = await loadConfig(configPath); -// Register handlers for abnormal terminations -process.on('SIGINT', () => { - logger.info('Process interrupted (SIGINT)'); - process.exit(0); -}); + return { + ...DEFAULT_SETTINGS, + ...config.settings, + } +} -process.on('SIGTERM', () => { - logger.info('Process terminated (SIGTERM)'); - process.exit(0); -}); - -// Register handlers for uncaught exceptions and unhandled rejections -process.on('uncaughtException', (err) => { - logger.error(`Uncaught exception: ${err.message}`); - process.exit(1); -}); - -process.on('unhandledRejection', (reason, promise) => { - logger.error(`Unhandled rejection at: ${promise}, reason: ${reason}`); - process.exit(1); -}); const cacheDir = env.DATA_CACHE_DIR; const reposPath = path.join(cacheDir, 'repos'); @@ -59,18 +52,62 @@ const context: AppContext = { const prisma = new PrismaClient(); -main(prisma, context) - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (e) => { - logger.error(e); - Sentry.captureException(e); +const redis = new Redis(env.REDIS_URL, { + maxRetriesPerRequest: null +}); +redis.ping().then(() => { + logger.info('Connected to redis'); +}).catch((err: unknown) => { + logger.error('Failed to connect to redis'); + logger.error(err); + process.exit(1); +}); - await prisma.$disconnect(); - process.exit(1); - }) - .finally(() => { - logger.info("Shutting down..."); - }); +const promClient = new PromClient(); +const settings = await getSettings(env.CONFIG_PATH); + +const connectionManager = new ConnectionManager(prisma, settings, redis); +const repoManager = new RepoManager(prisma, settings, redis, promClient, context); +const repoPermissionSyncer = new RepoPermissionSyncer(prisma, settings, redis); +const userPermissionSyncer = new UserPermissionSyncer(prisma, settings, redis); + +await repoManager.validateIndexedReposHaveShards(); + +connectionManager.startScheduler(); +repoManager.startScheduler(); + +if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && !hasEntitlement('permission-syncing')) { + logger.error('Permission syncing is not supported in current plan. Please contact team@sourcebot.dev for assistance.'); + process.exit(1); +} +else if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing')) { + repoPermissionSyncer.startScheduler(); + userPermissionSyncer.startScheduler(); +} + +const cleanup = async (signal: string) => { + logger.info(`Recieved ${signal}, cleaning up...`); + + connectionManager.dispose(); + repoManager.dispose(); + repoPermissionSyncer.dispose(); + userPermissionSyncer.dispose(); + + await prisma.$disconnect(); + await redis.quit(); +} + +process.on('SIGINT', () => cleanup('SIGINT').finally(() => process.exit(0))); +process.on('SIGTERM', () => cleanup('SIGTERM').finally(() => process.exit(0))); + +// Register handlers for uncaught exceptions and unhandled rejections +process.on('uncaughtException', (err) => { + logger.error(`Uncaught exception: ${err.message}`); + cleanup('uncaughtException').finally(() => process.exit(1)); +}); + +process.on('unhandledRejection', (reason, promise) => { + logger.error(`Unhandled rejection at: ${promise}, reason: ${reason}`); + cleanup('unhandledRejection').finally(() => process.exit(1)); +}); diff --git a/packages/backend/src/main.ts b/packages/backend/src/main.ts deleted file mode 100644 index f3cf0050..00000000 --- a/packages/backend/src/main.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { PrismaClient } from '@sourcebot/db'; -import { createLogger } from "@sourcebot/logger"; -import { AppContext } from "./types.js"; -import { DEFAULT_SETTINGS } from './constants.js'; -import { Redis } from 'ioredis'; -import { ConnectionManager } from './connectionManager.js'; -import { RepoManager } from './repoManager.js'; -import { env } from './env.js'; -import { PromClient } from './promClient.js'; -import { loadConfig } from '@sourcebot/shared'; - -const logger = createLogger('backend-main'); - -const getSettings = async (configPath?: string) => { - if (!configPath) { - return DEFAULT_SETTINGS; - } - - const config = await loadConfig(configPath); - - return { - ...DEFAULT_SETTINGS, - ...config.settings, - } -} - -export const main = async (db: PrismaClient, context: AppContext) => { - const redis = new Redis(env.REDIS_URL, { - maxRetriesPerRequest: null - }); - redis.ping().then(() => { - logger.info('Connected to redis'); - }).catch((err: unknown) => { - logger.error('Failed to connect to redis'); - logger.error(err); - process.exit(1); - }); - - const settings = await getSettings(env.CONFIG_PATH); - - const promClient = new PromClient(); - - const connectionManager = new ConnectionManager(db, settings, redis); - connectionManager.registerPollingCallback(); - - const repoManager = new RepoManager(db, settings, redis, promClient, context); - await repoManager.validateIndexedReposHaveShards(); - await repoManager.blockingPollLoop(); -} diff --git a/packages/backend/src/repoCompileUtils.ts b/packages/backend/src/repoCompileUtils.ts index 0013cd89..098f39c9 100644 --- a/packages/backend/src/repoCompileUtils.ts +++ b/packages/backend/src/repoCompileUtils.ts @@ -4,13 +4,15 @@ import { getGitLabReposFromConfig } from "./gitlab.js"; import { getGiteaReposFromConfig } from "./gitea.js"; import { getGerritReposFromConfig } from "./gerrit.js"; import { BitbucketRepository, getBitbucketReposFromConfig } from "./bitbucket.js"; +import { getAzureDevOpsReposFromConfig } from "./azuredevops.js"; import { SchemaRestRepository as BitbucketServerRepository } from "@coderabbitai/bitbucket/server/openapi"; import { SchemaRepository as BitbucketCloudRepository } from "@coderabbitai/bitbucket/cloud/openapi"; import { Prisma, PrismaClient } from '@sourcebot/db'; import { WithRequired } from "./types.js" import { marshalBool } from "./utils.js"; import { createLogger } from '@sourcebot/logger'; -import { BitbucketConnectionConfig, GerritConnectionConfig, GiteaConnectionConfig, GitlabConnectionConfig, GenericGitHostConnectionConfig } from '@sourcebot/schemas/v3/connection.type'; +import { BitbucketConnectionConfig, GerritConnectionConfig, GiteaConnectionConfig, GitlabConnectionConfig, GenericGitHostConnectionConfig, AzureDevOpsConnectionConfig } from '@sourcebot/schemas/v3/connection.type'; +import { ProjectVisibility } from "azure-devops-node-api/interfaces/CoreInterfaces.js"; import { RepoMetadata } from './types.js'; import path from 'path'; import { glob } from 'glob'; @@ -48,6 +50,7 @@ export const compileGithubConfig = async ( const repoDisplayName = repo.full_name; const repoName = path.join(repoNameRoot, repoDisplayName); const cloneUrl = new URL(repo.clone_url!); + const isPublic = repo.private === false; logger.debug(`Found github repo ${repoDisplayName} with webUrl: ${repo.html_url}`); @@ -62,6 +65,7 @@ export const compileGithubConfig = async ( imageUrl: repo.owner.avatar_url, isFork: repo.fork, isArchived: !!repo.archived, + isPublic: isPublic, org: { connect: { id: orgId, @@ -83,7 +87,7 @@ export const compileGithubConfig = async ( 'zoekt.github-forks': (repo.forks_count ?? 0).toString(), 'zoekt.archived': marshalBool(repo.archived), 'zoekt.fork': marshalBool(repo.fork), - 'zoekt.public': marshalBool(repo.private === false), + 'zoekt.public': marshalBool(isPublic), 'zoekt.display-name': repoDisplayName, }, branches: config.revisions?.branches ?? undefined, @@ -119,6 +123,8 @@ export const compileGitlabConfig = async ( const projectUrl = `${hostUrl}/${project.path_with_namespace}`; const cloneUrl = new URL(project.http_url_to_repo); const isFork = project.forked_from_project !== undefined; + // @todo: we will need to double check whether 'internal' should also be considered public or not. + const isPublic = project.visibility === 'public'; const repoDisplayName = project.path_with_namespace; const repoName = path.join(repoNameRoot, repoDisplayName); // project.avatar_url is not directly accessible with tokens; use the avatar API endpoint if available @@ -137,6 +143,7 @@ export const compileGitlabConfig = async ( displayName: repoDisplayName, imageUrl: avatarUrl, isFork: isFork, + isPublic: isPublic, isArchived: !!project.archived, org: { connect: { @@ -157,7 +164,7 @@ export const compileGitlabConfig = async ( 'zoekt.gitlab-forks': (project.forks_count ?? 0).toString(), 'zoekt.archived': marshalBool(project.archived), 'zoekt.fork': marshalBool(isFork), - 'zoekt.public': marshalBool(project.private === false), + 'zoekt.public': marshalBool(isPublic), 'zoekt.display-name': repoDisplayName, }, branches: config.revisions?.branches ?? undefined, @@ -195,6 +202,7 @@ export const compileGiteaConfig = async ( cloneUrl.host = configUrl.host const repoDisplayName = repo.full_name!; const repoName = path.join(repoNameRoot, repoDisplayName); + const isPublic = repo.internal === false && repo.private === false; logger.debug(`Found gitea repo ${repoDisplayName} with webUrl: ${repo.html_url}`); @@ -208,6 +216,7 @@ export const compileGiteaConfig = async ( displayName: repoDisplayName, imageUrl: repo.owner?.avatar_url, isFork: repo.fork!, + isPublic: isPublic, isArchived: !!repo.archived, org: { connect: { @@ -226,7 +235,7 @@ export const compileGiteaConfig = async ( 'zoekt.name': repoName, 'zoekt.archived': marshalBool(repo.archived), 'zoekt.fork': marshalBool(repo.fork!), - 'zoekt.public': marshalBool(repo.internal === false && repo.private === false), + 'zoekt.public': marshalBool(isPublic), 'zoekt.display-name': repoDisplayName, }, branches: config.revisions?.branches ?? undefined, @@ -310,6 +319,8 @@ export const compileGerritConfig = async ( 'zoekt.public': marshalBool(true), 'zoekt.display-name': repoDisplayName, }, + branches: config.revisions?.branches ?? undefined, + tags: config.revisions?.tags ?? undefined, } satisfies RepoMetadata, }; @@ -407,6 +418,7 @@ export const compileBitbucketConfig = async ( name: repoName, displayName: displayName, isFork: isFork, + isPublic: isPublic, isArchived: isArchived, org: { connect: { @@ -542,6 +554,7 @@ export const compileGenericGitHostConfig_file = async ( } } + export const compileGenericGitHostConfig_url = async ( config: GenericGitHostConnectionConfig, orgId: number, @@ -603,4 +616,87 @@ export const compileGenericGitHostConfig_url = async ( repoData: [repo], notFound, } -} \ No newline at end of file +} + +export const compileAzureDevOpsConfig = async ( + config: AzureDevOpsConnectionConfig, + connectionId: number, + orgId: number, + db: PrismaClient, + abortController: AbortController) => { + + const azureDevOpsReposResult = await getAzureDevOpsReposFromConfig(config, orgId, db); + const azureDevOpsRepos = azureDevOpsReposResult.validRepos; + const notFound = azureDevOpsReposResult.notFound; + + const hostUrl = config.url ?? 'https://dev.azure.com'; + const repoNameRoot = new URL(hostUrl) + .toString() + .replace(/^https?:\/\//, ''); + + const repos = azureDevOpsRepos.map((repo) => { + if (!repo.project) { + throw new Error(`No project found for repository ${repo.name}`); + } + + const repoDisplayName = `${repo.project.name}/${repo.name}`; + const repoName = path.join(repoNameRoot, repoDisplayName); + const isPublic = repo.project.visibility === ProjectVisibility.Public; + + if (!repo.remoteUrl) { + throw new Error(`No remoteUrl found for repository ${repoDisplayName}`); + } + if (!repo.id) { + throw new Error(`No id found for repository ${repoDisplayName}`); + } + + // Construct web URL for the repository + const webUrl = repo.webUrl || `${hostUrl}/${repo.project.name}/_git/${repo.name}`; + + logger.debug(`Found Azure DevOps repo ${repoDisplayName} with webUrl: ${webUrl}`); + + const record: RepoData = { + external_id: repo.id.toString(), + external_codeHostType: 'azuredevops', + external_codeHostUrl: hostUrl, + cloneUrl: webUrl, + webUrl: webUrl, + name: repoName, + displayName: repoDisplayName, + imageUrl: null, + isFork: !!repo.isFork, + isArchived: false, + isPublic: isPublic, + org: { + connect: { + id: orgId, + }, + }, + connections: { + create: { + connectionId: connectionId, + } + }, + metadata: { + gitConfig: { + 'zoekt.web-url-type': 'azuredevops', + 'zoekt.web-url': webUrl, + 'zoekt.name': repoName, + 'zoekt.archived': marshalBool(false), + 'zoekt.fork': marshalBool(!!repo.isFork), + 'zoekt.public': marshalBool(isPublic), + 'zoekt.display-name': repoDisplayName, + }, + branches: config.revisions?.branches ?? undefined, + tags: config.revisions?.tags ?? undefined, + } satisfies RepoMetadata, + }; + + return record; + }) + + return { + repoData: repos, + notFound, + }; +} diff --git a/packages/backend/src/repoManager.ts b/packages/backend/src/repoManager.ts index 491e9d1d..89e41673 100644 --- a/packages/backend/src/repoManager.ts +++ b/packages/backend/src/repoManager.ts @@ -1,27 +1,19 @@ -import { Job, Queue, Worker } from 'bullmq'; -import { Redis } from 'ioredis'; -import { createLogger } from "@sourcebot/logger"; -import { Connection, PrismaClient, Repo, RepoToConnection, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db"; -import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig, BitbucketConnectionConfig } from '@sourcebot/schemas/v3/connection.type'; -import { AppContext, Settings, repoMetadataSchema } from "./types.js"; -import { getRepoPath, getTokenFromConfig, measure, getShardPrefix } from "./utils.js"; -import { cloneRepository, fetchRepository, upsertGitConfig } from "./git.js"; -import { existsSync, readdirSync, promises } from 'fs'; -import { indexGitRepository } from "./zoekt.js"; -import { PromClient } from './promClient.js'; import * as Sentry from "@sentry/node"; +import { PrismaClient, Repo, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db"; +import { createLogger } from "@sourcebot/logger"; +import { Job, Queue, Worker } from 'bullmq'; +import { existsSync, promises, readdirSync } from 'fs'; +import { Redis } from 'ioredis'; import { env } from './env.js'; - -interface IRepoManager { - validateIndexedReposHaveShards: () => Promise; - blockingPollLoop: () => void; - dispose: () => void; -} +import { cloneRepository, fetchRepository, unsetGitConfig, upsertGitConfig } from "./git.js"; +import { PromClient } from './promClient.js'; +import { AppContext, RepoWithConnections, Settings, repoMetadataSchema } from "./types.js"; +import { getAuthCredentialsForRepo, getRepoPath, getShardPrefix, measure } from "./utils.js"; +import { indexGitRepository } from "./zoekt.js"; const REPO_INDEXING_QUEUE = 'repoIndexingQueue'; const REPO_GC_QUEUE = 'repoGarbageCollectionQueue'; -type RepoWithConnections = Repo & { connections: (RepoToConnection & { connection: Connection })[] }; type RepoIndexingPayload = { repo: RepoWithConnections, } @@ -32,11 +24,12 @@ type RepoGarbageCollectionPayload = { const logger = createLogger('repo-manager'); -export class RepoManager implements IRepoManager { +export class RepoManager { private indexWorker: Worker; private indexQueue: Queue; private gcWorker: Worker; private gcQueue: Queue; + private interval?: NodeJS.Timeout; constructor( private db: PrismaClient, @@ -68,14 +61,13 @@ export class RepoManager implements IRepoManager { this.gcWorker.on('failed', this.onGarbageCollectionJobFailed.bind(this)); } - public async blockingPollLoop() { - while (true) { + public startScheduler() { + logger.debug('Starting scheduler'); + this.interval = setInterval(async () => { await this.fetchAndScheduleRepoIndexing(); await this.fetchAndScheduleRepoGarbageCollection(); await this.fetchAndScheduleRepoTimeouts(); - - await new Promise(resolve => setTimeout(resolve, this.settings.reindexRepoPollingIntervalMs)); - } + }, this.settings.reindexRepoPollingIntervalMs); } /////////////////////////// @@ -169,62 +161,6 @@ export class RepoManager implements IRepoManager { } } - - // TODO: do this better? ex: try using the tokens from all the connections - // We can no longer use repo.cloneUrl directly since it doesn't contain the token for security reasons. As a result, we need to - // fetch the token here using the connections from the repo. Multiple connections could be referencing this repo, and each - // may have their own token. This method will just pick the first connection that has a token (if one exists) and uses that. This - // may technically cause syncing to fail if that connection's token just so happens to not have access to the repo it's referencing. - private async getCloneCredentialsForRepo(repo: RepoWithConnections, db: PrismaClient): Promise<{ username?: string, password: string } | undefined> { - - for (const { connection } of repo.connections) { - if (connection.connectionType === 'github') { - const config = connection.config as unknown as GithubConnectionConfig; - if (config.token) { - const token = await getTokenFromConfig(config.token, connection.orgId, db, logger); - return { - password: token, - } - } - } - - else if (connection.connectionType === 'gitlab') { - const config = connection.config as unknown as GitlabConnectionConfig; - if (config.token) { - const token = await getTokenFromConfig(config.token, connection.orgId, db, logger); - return { - username: 'oauth2', - password: token, - } - } - } - - else if (connection.connectionType === 'gitea') { - const config = connection.config as unknown as GiteaConnectionConfig; - if (config.token) { - const token = await getTokenFromConfig(config.token, connection.orgId, db, logger); - return { - password: token, - } - } - } - - else if (connection.connectionType === 'bitbucket') { - const config = connection.config as unknown as BitbucketConnectionConfig; - if (config.token) { - const token = await getTokenFromConfig(config.token, connection.orgId, db, logger); - const username = config.user ?? 'x-token-auth'; - return { - username, - password: token, - } - } - } - } - - return undefined; - } - private async syncGitRepository(repo: RepoWithConnections, repoAlreadyInIndexingState: boolean) { const { path: repoPath, isReadOnly } = getRepoPath(repo, this.ctx); @@ -237,11 +173,27 @@ export class RepoManager implements IRepoManager { await promises.rm(repoPath, { recursive: true, force: true }); } - if (existsSync(repoPath) && !isReadOnly) { - logger.info(`Fetching ${repo.displayName}...`); + const credentials = await getAuthCredentialsForRepo(repo, this.db); + const cloneUrlMaybeWithToken = credentials?.cloneUrlWithToken ?? repo.cloneUrl; + const authHeader = credentials?.authHeader ?? undefined; - const { durationMs } = await measure(() => fetchRepository(repoPath, ({ method, stage, progress }) => { - logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`) + if (existsSync(repoPath) && !isReadOnly) { + // @NOTE: in #483, we changed the cloning method s.t., we _no longer_ + // write the clone URL (which could contain a auth token) to the + // `remote.origin.url` entry. For the upgrade scenario, we want + // to unset this key since it is no longer needed, hence this line. + // This will no-op if the key is already unset. + // @see: https://github.com/sourcebot-dev/sourcebot/pull/483 + await unsetGitConfig(repoPath, ["remote.origin.url"]); + + logger.info(`Fetching ${repo.displayName}...`); + const { durationMs } = await measure(() => fetchRepository({ + cloneUrl: cloneUrlMaybeWithToken, + authHeader, + path: repoPath, + onProgress: ({ method, stage, progress }) => { + logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`) + } })); const fetchDuration_s = durationMs / 1000; @@ -251,24 +203,13 @@ export class RepoManager implements IRepoManager { } else if (!isReadOnly) { logger.info(`Cloning ${repo.displayName}...`); - const auth = await this.getCloneCredentialsForRepo(repo, this.db); - const cloneUrl = new URL(repo.cloneUrl); - if (auth) { - // @note: URL has a weird behavior where if you set the password but - // _not_ the username, the ":" delimiter will still be present in the - // URL (e.g., https://:password@example.com). To get around this, if - // we only have a password, we set the username to the password. - // @see: https://www.typescriptlang.org/play/?#code/MYewdgzgLgBArgJwDYwLwzAUwO4wKoBKAMgBQBEAFlFAA4QBcA9I5gB4CGAtjUpgHShOZADQBKANwAoREj412ECNhAIAJmhhl5i5WrJTQkELz5IQAcxIy+UEAGUoCAJZhLo0UA - if (!auth.username) { - cloneUrl.username = auth.password; - } else { - cloneUrl.username = auth.username; - cloneUrl.password = auth.password; + const { durationMs } = await measure(() => cloneRepository({ + cloneUrl: cloneUrlMaybeWithToken, + authHeader, + path: repoPath, + onProgress: ({ method, stage, progress }) => { + logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`) } - } - - const { durationMs } = await measure(() => cloneRepository(cloneUrl.toString(), repoPath, ({ method, stage, progress }) => { - logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`) })); const cloneDuration_s = durationMs / 1000; @@ -540,7 +481,7 @@ export class RepoManager implements IRepoManager { public async validateIndexedReposHaveShards() { logger.info('Validating indexed repos have shards...'); - + const indexedRepos = await this.db.repo.findMany({ where: { repoIndexingStatus: RepoIndexingStatus.INDEXED @@ -552,16 +493,15 @@ export class RepoManager implements IRepoManager { return; } + const files = readdirSync(this.ctx.indexPath); const reposToReindex: number[] = []; - for (const repo of indexedRepos) { const shardPrefix = getShardPrefix(repo.orgId, repo.id); - + // TODO: this doesn't take into account if a repo has multiple shards and only some of them are missing. To support that, this logic // would need to know how many total shards are expected for this repo let hasShards = false; try { - const files = readdirSync(this.ctx.indexPath); hasShards = files.some(file => file.startsWith(shardPrefix)); } catch (error) { logger.error(`Failed to read index directory ${this.ctx.indexPath}: ${error}`); @@ -615,6 +555,9 @@ export class RepoManager implements IRepoManager { } public async dispose() { + if (this.interval) { + clearInterval(this.interval); + } this.indexWorker.close(); this.indexQueue.close(); this.gcQueue.close(); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 58674f49..2ea42d04 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -1,3 +1,4 @@ +import { Connection, Repo, RepoToConnection } from "@sourcebot/db"; import { Settings as SettingsSchema } from "@sourcebot/schemas/v3/index.type"; import { z } from "zod"; @@ -50,4 +51,14 @@ export type DeepPartial = T extends object ? { } : T; // @see: https://stackoverflow.com/a/69328045 -export type WithRequired = T & { [P in K]-?: T[P] }; \ No newline at end of file +export type WithRequired = T & { [P in K]-?: T[P] }; + +export type RepoWithConnections = Repo & { connections: (RepoToConnection & { connection: Connection })[] }; + + +export type RepoAuthCredentials = { + hostUrl?: string; + token: string; + cloneUrlWithToken?: string; + authHeader?: string; +} \ No newline at end of file diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index 3245828d..e6ac5f93 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -1,10 +1,11 @@ import { Logger } from "winston"; -import { AppContext } from "./types.js"; +import { AppContext, RepoAuthCredentials, RepoWithConnections } from "./types.js"; import path from 'path'; import { PrismaClient, Repo } from "@sourcebot/db"; import { getTokenFromConfig as getTokenFromConfigBase } from "@sourcebot/crypto"; import { BackendException, BackendError } from "@sourcebot/error"; import * as Sentry from "@sentry/node"; +import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig, BitbucketConnectionConfig, AzureDevOpsConnectionConfig } from '@sourcebot/schemas/v3/connection.type'; export const measure = async (cb: () => Promise) => { const start = Date.now(); @@ -116,4 +117,127 @@ export const fetchWithRetry = async ( throw e; } } -} \ No newline at end of file +} + +// TODO: do this better? ex: try using the tokens from all the connections +// We can no longer use repo.cloneUrl directly since it doesn't contain the token for security reasons. As a result, we need to +// fetch the token here using the connections from the repo. Multiple connections could be referencing this repo, and each +// may have their own token. This method will just pick the first connection that has a token (if one exists) and uses that. This +// may technically cause syncing to fail if that connection's token just so happens to not have access to the repo it's referencing. +export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: PrismaClient, logger?: Logger): Promise => { + for (const { connection } of repo.connections) { + if (connection.connectionType === 'github') { + const config = connection.config as unknown as GithubConnectionConfig; + if (config.token) { + const token = await getTokenFromConfig(config.token, connection.orgId, db, logger); + return { + hostUrl: config.url, + token, + cloneUrlWithToken: createGitCloneUrlWithToken( + repo.cloneUrl, + { + password: token, + } + ), + } + } + } else if (connection.connectionType === 'gitlab') { + const config = connection.config as unknown as GitlabConnectionConfig; + if (config.token) { + const token = await getTokenFromConfig(config.token, connection.orgId, db, logger); + return { + hostUrl: config.url, + token, + cloneUrlWithToken: createGitCloneUrlWithToken( + repo.cloneUrl, + { + username: 'oauth2', + password: token + } + ), + } + } + } else if (connection.connectionType === 'gitea') { + const config = connection.config as unknown as GiteaConnectionConfig; + if (config.token) { + const token = await getTokenFromConfig(config.token, connection.orgId, db, logger); + return { + hostUrl: config.url, + token, + cloneUrlWithToken: createGitCloneUrlWithToken( + repo.cloneUrl, + { + password: token + } + ), + } + } + } else if (connection.connectionType === 'bitbucket') { + const config = connection.config as unknown as BitbucketConnectionConfig; + if (config.token) { + const token = await getTokenFromConfig(config.token, connection.orgId, db, logger); + const username = config.user ?? 'x-token-auth'; + return { + hostUrl: config.url, + token, + cloneUrlWithToken: createGitCloneUrlWithToken( + repo.cloneUrl, + { + username, + password: token + } + ), + } + } + } else if (connection.connectionType === 'azuredevops') { + const config = connection.config as unknown as AzureDevOpsConnectionConfig; + if (config.token) { + const token = await getTokenFromConfig(config.token, connection.orgId, db, logger); + + // For ADO server, multiple auth schemes may be supported. If the ADO deployment supports NTLM, the git clone will default + // to this over basic auth. As a result, we cannot embed the token in the clone URL and must force basic auth by passing in the token + // appropriately in the header. To do this, we set the authHeader field here + if (config.deploymentType === 'server') { + return { + hostUrl: config.url, + token, + authHeader: "Authorization: Basic " + Buffer.from(`:${token}`).toString('base64') + } + } else { + return { + hostUrl: config.url, + token, + cloneUrlWithToken: createGitCloneUrlWithToken( + repo.cloneUrl, + { + // @note: If we don't provide a username, the password will be set as the username. This seems to work + // for ADO cloud but not for ADO server. To fix this, we set a placeholder username to ensure the password + // is set correctly + username: 'user', + password: token + } + ), + } + } + } + } + } + + return undefined; +} + +const createGitCloneUrlWithToken = (cloneUrl: string, credentials: { username?: string, password: string }) => { + const url = new URL(cloneUrl); + // @note: URL has a weird behavior where if you set the password but + // _not_ the username, the ":" delimiter will still be present in the + // URL (e.g., https://:password@example.com). To get around this, if + // we only have a password, we set the username to the password. + // @see: https://www.typescriptlang.org/play/?#code/MYewdgzgLgBArgJwDYwLwzAUwO4wKoBKAMgBQBEAFlFAA4QBcA9I5gB4CGAtjUpgHShOZADQBKANwAoREj412ECNhAIAJmhhl5i5WrJTQkELz5IQAcxIy+UEAGUoCAJZhLo0UA + if (!credentials.username) { + url.username = credentials.password; + } else { + url.username = credentials.username; + url.password = credentials.password; + } + return url.toString(); +} diff --git a/packages/backend/src/zoekt.ts b/packages/backend/src/zoekt.ts index 86a191e4..076820c9 100644 --- a/packages/backend/src/zoekt.ts +++ b/packages/backend/src/zoekt.ts @@ -63,7 +63,7 @@ export const indexGitRepository = async (repo: Repo, settings: Settings, ctx: Ap `-index ${ctx.indexPath}`, `-max_trigram_count ${settings.maxTrigramCount}`, `-file_limit ${settings.maxFileSize}`, - `-branches ${revisions.join(',')}`, + `-branches "${revisions.join(',')}"`, `-tenant_id ${repo.orgId}`, `-repo_id ${repo.id}`, `-shard_prefix ${shardPrefix}`, @@ -76,6 +76,20 @@ export const indexGitRepository = async (repo: Repo, settings: Settings, ctx: Ap reject(error); return; } + + if (stdout) { + stdout.split('\n').filter(line => line.trim()).forEach(line => { + logger.info(line); + }); + } + if (stderr) { + stderr.split('\n').filter(line => line.trim()).forEach(line => { + // TODO: logging as regular info here and not error because non error logs are being + // streamed in stderr and incorrectly being logged as errors at a high level + logger.info(line); + }); + } + resolve({ stdout, stderr diff --git a/packages/db/prisma/migrations/20250722201612_add_chat_table/migration.sql b/packages/db/prisma/migrations/20250722201612_add_chat_table/migration.sql new file mode 100644 index 00000000..38d64354 --- /dev/null +++ b/packages/db/prisma/migrations/20250722201612_add_chat_table/migration.sql @@ -0,0 +1,23 @@ +-- CreateEnum +CREATE TYPE "ChatVisibility" AS ENUM ('PRIVATE', 'PUBLIC'); + +-- CreateTable +CREATE TABLE "Chat" ( + "id" TEXT NOT NULL, + "name" TEXT, + "createdById" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "orgId" INTEGER NOT NULL, + "visibility" "ChatVisibility" NOT NULL DEFAULT 'PRIVATE', + "isReadonly" BOOLEAN NOT NULL DEFAULT false, + "messages" JSONB NOT NULL, + + CONSTRAINT "Chat_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Chat" ADD CONSTRAINT "Chat_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Chat" ADD CONSTRAINT "Chat_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/migrations/20250919221836_add_indexes_to_repo_tables/migration.sql b/packages/db/prisma/migrations/20250919221836_add_indexes_to_repo_tables/migration.sql new file mode 100644 index 00000000..2b67f1f9 --- /dev/null +++ b/packages/db/prisma/migrations/20250919221836_add_indexes_to_repo_tables/migration.sql @@ -0,0 +1,5 @@ +-- CreateIndex +CREATE INDEX "Repo_orgId_idx" ON "Repo"("orgId"); + +-- CreateIndex +CREATE INDEX "RepoToConnection_repoId_connectionId_idx" ON "RepoToConnection"("repoId", "connectionId"); diff --git a/packages/db/prisma/migrations/20250920232318_add_permission_sync_tables/migration.sql b/packages/db/prisma/migrations/20250920232318_add_permission_sync_tables/migration.sql new file mode 100644 index 00000000..9e921c6d --- /dev/null +++ b/packages/db/prisma/migrations/20250920232318_add_permission_sync_tables/migration.sql @@ -0,0 +1,59 @@ +-- CreateEnum +CREATE TYPE "RepoPermissionSyncJobStatus" AS ENUM ('PENDING', 'IN_PROGRESS', 'COMPLETED', 'FAILED'); + +-- CreateEnum +CREATE TYPE "UserPermissionSyncJobStatus" AS ENUM ('PENDING', 'IN_PROGRESS', 'COMPLETED', 'FAILED'); + +-- AlterTable +ALTER TABLE "Repo" ADD COLUMN "isPublic" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "permissionSyncedAt" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "permissionSyncedAt" TIMESTAMP(3); + +-- CreateTable +CREATE TABLE "RepoPermissionSyncJob" ( + "id" TEXT NOT NULL, + "status" "RepoPermissionSyncJobStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "completedAt" TIMESTAMP(3), + "errorMessage" TEXT, + "repoId" INTEGER NOT NULL, + + CONSTRAINT "RepoPermissionSyncJob_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserPermissionSyncJob" ( + "id" TEXT NOT NULL, + "status" "UserPermissionSyncJobStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "completedAt" TIMESTAMP(3), + "errorMessage" TEXT, + "userId" TEXT NOT NULL, + + CONSTRAINT "UserPermissionSyncJob_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserToRepoPermission" ( + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "repoId" INTEGER NOT NULL, + "userId" TEXT NOT NULL, + + CONSTRAINT "UserToRepoPermission_pkey" PRIMARY KEY ("repoId","userId") +); + +-- AddForeignKey +ALTER TABLE "RepoPermissionSyncJob" ADD CONSTRAINT "RepoPermissionSyncJob_repoId_fkey" FOREIGN KEY ("repoId") REFERENCES "Repo"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserPermissionSyncJob" ADD CONSTRAINT "UserPermissionSyncJob_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserToRepoPermission" ADD CONSTRAINT "UserToRepoPermission_repoId_fkey" FOREIGN KEY ("repoId") REFERENCES "Repo"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserToRepoPermission" ADD CONSTRAINT "UserToRepoPermission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index bddc8473..bdebbc69 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -35,29 +35,35 @@ enum StripeSubscriptionStatus { INACTIVE } +enum ChatVisibility { + PRIVATE + PUBLIC +} + model Repo { - id Int @id @default(autoincrement()) - name String - displayName String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - /// When the repo was last indexed successfully. - indexedAt DateTime? + id Int @id @default(autoincrement()) + name String /// Full repo name, including the vcs hostname (ex. github.com/sourcebot-dev/sourcebot) + displayName String? /// Display name of the repo for UI (ex. sourcebot-dev/sourcebot) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + indexedAt DateTime? /// When the repo was last indexed successfully. isFork Boolean isArchived Boolean - metadata Json // For schema see repoMetadataSchema in packages/backend/src/types.ts + isPublic Boolean @default(false) + metadata Json /// For schema see repoMetadataSchema in packages/backend/src/types.ts cloneUrl String webUrl String? connections RepoToConnection[] imageUrl String? - repoIndexingStatus RepoIndexingStatus @default(NEW) + repoIndexingStatus RepoIndexingStatus @default(NEW) - // The id of the repo in the external service - external_id String - // The type of the external service (e.g., github, gitlab, etc.) - external_codeHostType String - // The base url of the external service (e.g., https://github.com) - external_codeHostUrl String + permittedUsers UserToRepoPermission[] + permissionSyncJobs RepoPermissionSyncJob[] + permissionSyncedAt DateTime? /// When the permissions were last synced successfully. + + external_id String /// The id of the repo in the external service + external_codeHostType String /// The type of the external service (e.g., github, gitlab, etc.) + external_codeHostUrl String /// The base url of the external service (e.g., https://github.com) org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) orgId Int @@ -65,14 +71,35 @@ model Repo { searchContexts SearchContext[] @@unique([external_id, external_codeHostUrl, orgId]) + @@index([orgId]) +} + +enum RepoPermissionSyncJobStatus { + PENDING + IN_PROGRESS + COMPLETED + FAILED +} + +model RepoPermissionSyncJob { + id String @id @default(cuid()) + status RepoPermissionSyncJobStatus @default(PENDING) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + completedAt DateTime? + + errorMessage String? + + repo Repo @relation(fields: [repoId], references: [id], onDelete: Cascade) + repoId Int } model SearchContext { id Int @id @default(autoincrement()) - name String + name String description String? - repos Repo[] + repos Repo[] org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) orgId Int @@ -113,6 +140,7 @@ model RepoToConnection { repoId Int @@id([connectionId, repoId]) + @@index([repoId, connectionId]) } model Invite { @@ -141,7 +169,7 @@ model AccountRequest { createdAt DateTime @default(now()) - requestedBy User @relation(fields: [requestedById], references: [id], onDelete: Cascade) + requestedBy User @relation(fields: [requestedById], references: [id], onDelete: Cascade) requestedById String @unique org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@ -163,7 +191,7 @@ model Org { apiKeys ApiKey[] isOnboarded Boolean @default(false) imageUrl String? - metadata Json? // For schema see orgMetadataSchema in packages/web/src/types.ts + metadata Json? // For schema see orgMetadataSchema in packages/web/src/types.ts memberApprovalRequired Boolean @default(true) @@ -173,16 +201,18 @@ model Org { /// List of pending invites to this organization invites Invite[] - + /// The invite id for this organization inviteLinkEnabled Boolean @default(false) - inviteLinkId String? + inviteLinkId String? audits Audit[] accountRequests AccountRequest[] searchContexts SearchContext[] + + chats Chat[] } enum OrgRole { @@ -221,63 +251,98 @@ model Secret { } model ApiKey { - name String - hash String @id @unique + name String + hash String @id @unique - createdAt DateTime @default(now()) + createdAt DateTime @default(now()) lastUsedAt DateTime? org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) orgId Int - createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade) + createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade) createdById String - } model Audit { - id String @id @default(cuid()) + id String @id @default(cuid()) timestamp DateTime @default(now()) - - action String - actorId String - actorType String - targetId String - targetType String + + action String + actorId String + actorType String + targetId String + targetType String sourcebotVersion String - metadata Json? + metadata Json? org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) orgId Int @@index([actorId, actorType, targetId, targetType, orgId]) - // Fast path for analytics queries – orgId is first because we assume most deployments are single tenant @@index([orgId, timestamp, action, actorId], map: "idx_audit_core_actions_full") - // Fast path for analytics queries for a specific user @@index([actorId, timestamp], map: "idx_audit_actor_time_full") } // @see : https://authjs.dev/concepts/database-models#user model User { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? - email String? @unique + email String? @unique hashedPassword String? emailVerified DateTime? image String? accounts Account[] orgs UserToOrg[] accountRequest AccountRequest? + accessibleRepos UserToRepoPermission[] /// List of pending invites that the user has created invites Invite[] apiKeys ApiKey[] + chats Chat[] + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + permissionSyncJobs UserPermissionSyncJob[] + permissionSyncedAt DateTime? +} + +enum UserPermissionSyncJobStatus { + PENDING + IN_PROGRESS + COMPLETED + FAILED +} + +model UserPermissionSyncJob { + id String @id @default(cuid()) + status UserPermissionSyncJobStatus @default(PENDING) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + completedAt DateTime? + + errorMessage String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String +} + +model UserToRepoPermission { + createdAt DateTime @default(now()) + + repo Repo @relation(fields: [repoId], references: [id], onDelete: Cascade) + repoId Int + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + + @@id([repoId, userId]) } // @see : https://authjs.dev/concepts/database-models#account @@ -311,3 +376,23 @@ model VerificationToken { @@unique([identifier, token]) } + +model Chat { + id String @id @default(cuid()) + + name String? + + createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade) + createdById String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) + orgId Int + + visibility ChatVisibility @default(PRIVATE) + isReadonly Boolean @default(false) + + messages Json // This is a JSON array of `Message` types from @ai-sdk/ui-utils. +} diff --git a/packages/db/tools/scriptRunner.ts b/packages/db/tools/scriptRunner.ts index 8e72063a..499a3770 100644 --- a/packages/db/tools/scriptRunner.ts +++ b/packages/db/tools/scriptRunner.ts @@ -4,6 +4,8 @@ import { migrateDuplicateConnections } from "./scripts/migrate-duplicate-connect import { injectAuditData } from "./scripts/inject-audit-data"; import { confirmAction } from "./utils"; import { createLogger } from "@sourcebot/logger"; +import { injectRepoData } from "./scripts/inject-repo-data"; +import { testRepoQueryPerf } from "./scripts/test-repo-query-perf"; export interface Script { run: (prisma: PrismaClient) => Promise; @@ -12,6 +14,8 @@ export interface Script { export const scripts: Record = { "migrate-duplicate-connections": migrateDuplicateConnections, "inject-audit-data": injectAuditData, + "inject-repo-data": injectRepoData, + "test-repo-query-perf": testRepoQueryPerf, } const parser = new ArgumentParser(); diff --git a/packages/db/tools/scripts/inject-repo-data.ts b/packages/db/tools/scripts/inject-repo-data.ts new file mode 100644 index 00000000..209880f3 --- /dev/null +++ b/packages/db/tools/scripts/inject-repo-data.ts @@ -0,0 +1,64 @@ +import { Script } from "../scriptRunner"; +import { PrismaClient } from "../../dist"; +import { createLogger } from "@sourcebot/logger"; + +const logger = createLogger('inject-repo-data'); + +const NUM_REPOS = 100000; + +export const injectRepoData: Script = { + run: async (prisma: PrismaClient) => { + const orgId = 1; + + // Check if org exists + const org = await prisma.org.findUnique({ + where: { id: orgId } + }); + + if (!org) { + await prisma.org.create({ + data: { + id: orgId, + name: 'Test Org', + domain: 'test-org.com' + } + }); + } + + const connection = await prisma.connection.create({ + data: { + orgId, + name: 'test-connection', + connectionType: 'github', + config: {} + } + }); + + + logger.info(`Creating ${NUM_REPOS} repos...`); + + for (let i = 0; i < NUM_REPOS; i++) { + await prisma.repo.create({ + data: { + name: `test-repo-${i}`, + isFork: false, + isArchived: false, + metadata: {}, + cloneUrl: `https://github.com/test-org/test-repo-${i}`, + webUrl: `https://github.com/test-org/test-repo-${i}`, + orgId, + external_id: `test-repo-${i}`, + external_codeHostType: 'github', + external_codeHostUrl: 'https://github.com', + connections: { + create: { + connectionId: connection.id, + } + } + } + }); + } + + logger.info(`Created ${NUM_REPOS} repos.`); + } +}; \ No newline at end of file diff --git a/packages/db/tools/scripts/test-repo-query-perf.ts b/packages/db/tools/scripts/test-repo-query-perf.ts new file mode 100644 index 00000000..ee07d14f --- /dev/null +++ b/packages/db/tools/scripts/test-repo-query-perf.ts @@ -0,0 +1,28 @@ +import { Script } from "../scriptRunner"; +import { PrismaClient } from "../../dist"; +import { createLogger } from "@sourcebot/logger"; + +const logger = createLogger('test-repo-query-perf'); + +export const testRepoQueryPerf: Script = { + run: async (prisma: PrismaClient) => { + + + const start = Date.now(); + const allRepos = await prisma.repo.findMany({ + where: { + orgId: 1, + }, + include: { + connections: { + include: { + connection: true, + } + } + } + }); + + const durationMs = Date.now() - start; + logger.info(`Found ${allRepos.length} repos in ${durationMs}ms`); + } +}; \ No newline at end of file diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index b856337f..aca038a0 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.6] - 2025-09-26 +- Fix `linkedConnections is required` schema error. + +## [1.0.5] - 2025-09-15 + +### Changed +- Updated API client to match the latest Sourcebot release. [#356](https://github.com/sourcebot-dev/sourcebot/pull/356) + +## [1.0.4] - 2025-08-04 + +### Fixed +- Fixed issue where console logs were resulting in "unexpected token" errors on the MCP client. [#429](https://github.com/sourcebot-dev/sourcebot/pull/429) + ## [1.0.3] - 2025-06-18 ### Changed diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 27808b13..0c64bdc3 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -207,18 +207,10 @@ Sourcebot supports the following code hosts: - [Gitea](https://docs.sourcebot.dev/docs/connections/gitea) - [Gerrit](https://docs.sourcebot.dev/docs/connections/gerrit) -| Don't see your code host? Open a [GitHub discussion](https://github.com/sourcebot-dev/sourcebot/discussions/categories/ideas). +| Don't see your code host? Open a [feature request](https://github.com/sourcebot-dev/sourcebot/issues/new?template=feature_request.md). ## Future Work ### Semantic Search Currently, Sourcebot only supports regex-based code search (powered by [zoekt](https://github.com/sourcegraph/zoekt) under the hood). It is great for scenarios when the agent is searching for is something that is super precise and well-represented in the source code (e.g., a specific function name, a error string, etc.). It is not-so-great for _fuzzy_ searches where the objective is to find some loosely defined _category_ or _concept_ in the code (e.g., find code that verifies JWT tokens). The LLM can approximate this by crafting regex searches that attempt to capture a concept (e.g., it might try a query like `"jwt|token|(verify|validate).*(jwt|token)"`), but often yields sub-optimal search results that aren't related. Tools like Cursor solve this with [embedding models](https://docs.cursor.com/context/codebase-indexing) to capture the semantic meaning of code, allowing for LLMs to search using natural language. We would like to extend Sourcebot to support semantic search and expose this capability over MCP as a tool (e.g., `semantic_search_code` tool). [GitHub Discussion](https://github.com/sourcebot-dev/sourcebot/discussions/297) - -### Code Navigation - -Another idea is to allow LLMs to traverse abstract syntax trees (ASTs) of a codebase to enable reliable code navigation. This could be packaged as tools like `goto_definition`, `find_all_references`, etc., which could be useful for LLMs to get additional code context. [GitHub Discussion](https://github.com/sourcebot-dev/sourcebot/discussions/296) - -### Got an idea? - -Open up a [GitHub discussion](https://github.com/sourcebot-dev/sourcebot/discussions/categories/feature-requests)! diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 969a465d..b11c8361 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@sourcebot/mcp", - "version": "1.0.3", + "version": "1.0.6", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/mcp/src/client.ts b/packages/mcp/src/client.ts index c7d7d230..3754c605 100644 --- a/packages/mcp/src/client.ts +++ b/packages/mcp/src/client.ts @@ -4,7 +4,6 @@ import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, Search import { isServiceError } from './utils.js'; export const search = async (request: SearchRequest): Promise => { - console.debug(`Executing search request: ${JSON.stringify(request, null, 2)}`); const result = await fetch(`${env.SOURCEBOT_HOST}/api/search`, { method: 'POST', headers: { diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index dfd0ebe2..4411b580 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -75,8 +75,6 @@ server.tool( query += ` case:no`; } - console.debug(`Executing search request: ${query}`); - const response = await search({ query, matches: env.DEFAULT_MATCHES, @@ -163,10 +161,10 @@ server.tool( }; } - const content: TextContent[] = response.repos.map(repo => { + const content: TextContent[] = response.map(repo => { return { type: "text", - text: `id: ${repo.name}\nurl: ${repo.webUrl}`, + text: `id: ${repo.repoName}\nurl: ${repo.webUrl}`, } }); @@ -214,7 +212,6 @@ server.tool( const runServer = async () => { const transport = new StdioServerTransport(); await server.connect(transport); - console.info('Sourcebot MCP server ready'); } runServer().catch((error) => { diff --git a/packages/mcp/src/schemas.ts b/packages/mcp/src/schemas.ts index ad94b7dc..40736b59 100644 --- a/packages/mcp/src/schemas.ts +++ b/packages/mcp/src/schemas.ts @@ -92,16 +92,30 @@ export const searchResponseSchema = z.object({ isBranchFilteringEnabled: z.boolean(), }); -export const repositorySchema = z.object({ - name: z.string(), - branches: z.array(z.string()), +enum RepoIndexingStatus { + NEW = 'NEW', + IN_INDEX_QUEUE = 'IN_INDEX_QUEUE', + INDEXING = 'INDEXING', + INDEXED = 'INDEXED', + FAILED = 'FAILED', + IN_GC_QUEUE = 'IN_GC_QUEUE', + GARBAGE_COLLECTING = 'GARBAGE_COLLECTING', + GARBAGE_COLLECTION_FAILED = 'GARBAGE_COLLECTION_FAILED' +} + +export const repositoryQuerySchema = z.object({ + codeHostType: z.string(), + repoId: z.number(), + repoName: z.string(), + repoDisplayName: z.string().optional(), + repoCloneUrl: z.string(), webUrl: z.string().optional(), - rawConfig: z.record(z.string(), z.string()).optional(), + imageUrl: z.string().optional(), + indexedAt: z.coerce.date().optional(), + repoIndexingStatus: z.nativeEnum(RepoIndexingStatus), }); -export const listRepositoriesResponseSchema = z.object({ - repos: z.array(repositorySchema), -}); +export const listRepositoriesResponseSchema = repositoryQuerySchema.array(); export const fileSourceRequestSchema = z.object({ fileName: z.string(), diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts index f789c8c1..9c858fe5 100644 --- a/packages/mcp/src/types.ts +++ b/packages/mcp/src/types.ts @@ -22,7 +22,6 @@ export type SearchResultChunk = SearchResultFile["chunks"][number]; export type SearchSymbol = z.infer; export type ListRepositoriesResponse = z.infer; -export type Repository = ListRepositoriesResponse["repos"][number]; export type FileSourceRequest = z.infer; export type FileSourceResponse = z.infer; diff --git a/packages/schemas/src/v3/azuredevops.schema.ts b/packages/schemas/src/v3/azuredevops.schema.ts new file mode 100644 index 00000000..3b36fbed --- /dev/null +++ b/packages/schemas/src/v3/azuredevops.schema.ts @@ -0,0 +1,205 @@ +// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! +const schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "AzureDevOpsConnectionConfig", + "properties": { + "type": { + "const": "azuredevops", + "description": "Azure DevOps Configuration" + }, + "token": { + "description": "A Personal Access Token (PAT).", + "examples": [ + { + "secret": "SECRET_KEY" + } + ], + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "url": { + "type": "string", + "format": "url", + "default": "https://dev.azure.com", + "description": "The URL of the Azure DevOps host. For Azure DevOps Cloud, use https://dev.azure.com. For Azure DevOps Server, use your server URL.", + "examples": [ + "https://dev.azure.com", + "https://azuredevops.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + }, + "deploymentType": { + "type": "string", + "enum": [ + "cloud", + "server" + ], + "description": "The type of Azure DevOps deployment" + }, + "useTfsPath": { + "type": "boolean", + "default": false, + "description": "Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...)." + }, + "orgs": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org" + ] + ], + "description": "List of organizations to sync with. For Cloud, this is the organization name. For Server, this is the collection name. All projects and repositories visible to the provided `token` will be synced, unless explicitly defined in the `exclude` property." + }, + "projects": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+\\/[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org/my-project", + "my-collection/my-project" + ] + ], + "description": "List of specific projects to sync with. Expected to be formatted as '{orgName}/{projectName}' for Cloud or '{collectionName}/{projectName}' for Server." + }, + "repos": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+\\/[\\w.-]+\\/[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org/my-project/my-repo" + ] + ], + "description": "List of individual repositories to sync with. Expected to be formatted as '{orgName}/{projectName}/{repoName}'." + }, + "exclude": { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "default": false, + "description": "Exclude disabled repositories from syncing." + }, + "repos": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of repositories to exclude from syncing. Glob patterns are supported." + }, + "projects": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of projects to exclude from syncing. Glob patterns are supported." + }, + "size": { + "type": "object", + "description": "Exclude repositories based on their size.", + "properties": { + "min": { + "type": "integer", + "description": "Minimum repository size (in bytes) to sync (inclusive). Repositories less than this size will be excluded from syncing." + }, + "max": { + "type": "integer", + "description": "Maximum repository size (in bytes) to sync (inclusive). Repositories greater than this size will be excluded from syncing." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "revisions": { + "type": "object", + "description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.", + "properties": { + "branches": { + "type": "array", + "description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "main", + "release/*" + ], + [ + "**" + ] + ], + "default": [] + }, + "tags": { + "type": "array", + "description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "latest", + "v2.*.*" + ], + [ + "**" + ] + ], + "default": [] + } + }, + "additionalProperties": false + } + }, + "required": [ + "type", + "token", + "deploymentType" + ], + "additionalProperties": false +} as const; +export { schema as azuredevopsSchema }; \ No newline at end of file diff --git a/packages/schemas/src/v3/azuredevops.type.ts b/packages/schemas/src/v3/azuredevops.type.ts new file mode 100644 index 00000000..b6ef68da --- /dev/null +++ b/packages/schemas/src/v3/azuredevops.type.ts @@ -0,0 +1,89 @@ +// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! + +export interface AzureDevOpsConnectionConfig { + /** + * Azure DevOps Configuration + */ + type: "azuredevops"; + /** + * A Personal Access Token (PAT). + */ + token: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * The URL of the Azure DevOps host. For Azure DevOps Cloud, use https://dev.azure.com. For Azure DevOps Server, use your server URL. + */ + url?: string; + /** + * The type of Azure DevOps deployment + */ + deploymentType: "cloud" | "server"; + /** + * Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...). + */ + useTfsPath?: boolean; + /** + * List of organizations to sync with. For Cloud, this is the organization name. For Server, this is the collection name. All projects and repositories visible to the provided `token` will be synced, unless explicitly defined in the `exclude` property. + */ + orgs?: string[]; + /** + * List of specific projects to sync with. Expected to be formatted as '{orgName}/{projectName}' for Cloud or '{collectionName}/{projectName}' for Server. + */ + projects?: string[]; + /** + * List of individual repositories to sync with. Expected to be formatted as '{orgName}/{projectName}/{repoName}'. + */ + repos?: string[]; + exclude?: { + /** + * Exclude disabled repositories from syncing. + */ + disabled?: boolean; + /** + * List of repositories to exclude from syncing. Glob patterns are supported. + */ + repos?: string[]; + /** + * List of projects to exclude from syncing. Glob patterns are supported. + */ + projects?: string[]; + /** + * Exclude repositories based on their size. + */ + size?: { + /** + * Minimum repository size (in bytes) to sync (inclusive). Repositories less than this size will be excluded from syncing. + */ + min?: number; + /** + * Maximum repository size (in bytes) to sync (inclusive). Repositories greater than this size will be excluded from syncing. + */ + max?: number; + }; + }; + revisions?: GitRevisions; +} +/** + * The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored. + */ +export interface GitRevisions { + /** + * List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored. + */ + branches?: string[]; + /** + * List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored. + */ + tags?: string[]; +} diff --git a/packages/schemas/src/v3/connection.schema.ts b/packages/schemas/src/v3/connection.schema.ts index d0a72c72..e0bcbc48 100644 --- a/packages/schemas/src/v3/connection.schema.ts +++ b/packages/schemas/src/v3/connection.schema.ts @@ -342,6 +342,11 @@ const schema = { "default": false, "description": "Exclude archived projects from syncing." }, + "userOwnedProjects": { + "type": "boolean", + "default": false, + "description": "Exclude user-owned projects from syncing." + }, "projects": { "type": "array", "items": { @@ -637,6 +642,47 @@ const schema = { } }, "additionalProperties": false + }, + "revisions": { + "type": "object", + "description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.", + "properties": { + "branches": { + "type": "array", + "description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "main", + "release/*" + ], + [ + "**" + ] + ], + "default": [] + }, + "tags": { + "type": "array", + "description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "latest", + "v2.*.*" + ], + [ + "**" + ] + ], + "default": [] + } + }, + "additionalProperties": false } }, "required": [ @@ -822,6 +868,209 @@ const schema = { }, "additionalProperties": false }, + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "AzureDevOpsConnectionConfig", + "properties": { + "type": { + "const": "azuredevops", + "description": "Azure DevOps Configuration" + }, + "token": { + "description": "A Personal Access Token (PAT).", + "examples": [ + { + "secret": "SECRET_KEY" + } + ], + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "url": { + "type": "string", + "format": "url", + "default": "https://dev.azure.com", + "description": "The URL of the Azure DevOps host. For Azure DevOps Cloud, use https://dev.azure.com. For Azure DevOps Server, use your server URL.", + "examples": [ + "https://dev.azure.com", + "https://azuredevops.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + }, + "deploymentType": { + "type": "string", + "enum": [ + "cloud", + "server" + ], + "description": "The type of Azure DevOps deployment" + }, + "useTfsPath": { + "type": "boolean", + "default": false, + "description": "Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...)." + }, + "orgs": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org" + ] + ], + "description": "List of organizations to sync with. For Cloud, this is the organization name. For Server, this is the collection name. All projects and repositories visible to the provided `token` will be synced, unless explicitly defined in the `exclude` property." + }, + "projects": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+\\/[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org/my-project", + "my-collection/my-project" + ] + ], + "description": "List of specific projects to sync with. Expected to be formatted as '{orgName}/{projectName}' for Cloud or '{collectionName}/{projectName}' for Server." + }, + "repos": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+\\/[\\w.-]+\\/[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org/my-project/my-repo" + ] + ], + "description": "List of individual repositories to sync with. Expected to be formatted as '{orgName}/{projectName}/{repoName}'." + }, + "exclude": { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "default": false, + "description": "Exclude disabled repositories from syncing." + }, + "repos": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of repositories to exclude from syncing. Glob patterns are supported." + }, + "projects": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of projects to exclude from syncing. Glob patterns are supported." + }, + "size": { + "type": "object", + "description": "Exclude repositories based on their size.", + "properties": { + "min": { + "type": "integer", + "description": "Minimum repository size (in bytes) to sync (inclusive). Repositories less than this size will be excluded from syncing." + }, + "max": { + "type": "integer", + "description": "Maximum repository size (in bytes) to sync (inclusive). Repositories greater than this size will be excluded from syncing." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "revisions": { + "type": "object", + "description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.", + "properties": { + "branches": { + "type": "array", + "description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "main", + "release/*" + ], + [ + "**" + ] + ], + "default": [] + }, + "tags": { + "type": "array", + "description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "latest", + "v2.*.*" + ], + [ + "**" + ] + ], + "default": [] + } + }, + "additionalProperties": false + } + }, + "required": [ + "type", + "token", + "deploymentType" + ], + "additionalProperties": false + }, { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", diff --git a/packages/schemas/src/v3/connection.type.ts b/packages/schemas/src/v3/connection.type.ts index d1d2bc18..df31c465 100644 --- a/packages/schemas/src/v3/connection.type.ts +++ b/packages/schemas/src/v3/connection.type.ts @@ -6,6 +6,7 @@ export type ConnectionConfig = | GiteaConnectionConfig | GerritConnectionConfig | BitbucketConnectionConfig + | AzureDevOpsConnectionConfig | GenericGitHostConnectionConfig; export interface GithubConnectionConfig { @@ -153,6 +154,10 @@ export interface GitlabConnectionConfig { * Exclude archived projects from syncing. */ archived?: boolean; + /** + * Exclude user-owned projects from syncing. + */ + userOwnedProjects?: boolean; /** * List of projects to exclude from syncing. Glob patterns are supported. The project's namespace must be specified, see: https://docs.gitlab.com/ee/user/namespace/ */ @@ -244,6 +249,7 @@ export interface GerritConnectionConfig { */ hidden?: boolean; }; + revisions?: GitRevisions; } export interface BitbucketConnectionConfig { /** @@ -306,6 +312,80 @@ export interface BitbucketConnectionConfig { }; revisions?: GitRevisions; } +export interface AzureDevOpsConnectionConfig { + /** + * Azure DevOps Configuration + */ + type: "azuredevops"; + /** + * A Personal Access Token (PAT). + */ + token: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * The URL of the Azure DevOps host. For Azure DevOps Cloud, use https://dev.azure.com. For Azure DevOps Server, use your server URL. + */ + url?: string; + /** + * The type of Azure DevOps deployment + */ + deploymentType: "cloud" | "server"; + /** + * Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...). + */ + useTfsPath?: boolean; + /** + * List of organizations to sync with. For Cloud, this is the organization name. For Server, this is the collection name. All projects and repositories visible to the provided `token` will be synced, unless explicitly defined in the `exclude` property. + */ + orgs?: string[]; + /** + * List of specific projects to sync with. Expected to be formatted as '{orgName}/{projectName}' for Cloud or '{collectionName}/{projectName}' for Server. + */ + projects?: string[]; + /** + * List of individual repositories to sync with. Expected to be formatted as '{orgName}/{projectName}/{repoName}'. + */ + repos?: string[]; + exclude?: { + /** + * Exclude disabled repositories from syncing. + */ + disabled?: boolean; + /** + * List of repositories to exclude from syncing. Glob patterns are supported. + */ + repos?: string[]; + /** + * List of projects to exclude from syncing. Glob patterns are supported. + */ + projects?: string[]; + /** + * Exclude repositories based on their size. + */ + size?: { + /** + * Minimum repository size (in bytes) to sync (inclusive). Repositories less than this size will be excluded from syncing. + */ + min?: number; + /** + * Maximum repository size (in bytes) to sync (inclusive). Repositories greater than this size will be excluded from syncing. + */ + max?: number; + }; + }; + revisions?: GitRevisions; +} export interface GenericGitHostConnectionConfig { /** * Generic Git host configuration diff --git a/packages/schemas/src/v3/gerrit.schema.ts b/packages/schemas/src/v3/gerrit.schema.ts index b8b99e76..8733ba8d 100644 --- a/packages/schemas/src/v3/gerrit.schema.ts +++ b/packages/schemas/src/v3/gerrit.schema.ts @@ -58,6 +58,47 @@ const schema = { } }, "additionalProperties": false + }, + "revisions": { + "type": "object", + "description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.", + "properties": { + "branches": { + "type": "array", + "description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "main", + "release/*" + ], + [ + "**" + ] + ], + "default": [] + }, + "tags": { + "type": "array", + "description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "latest", + "v2.*.*" + ], + [ + "**" + ] + ], + "default": [] + } + }, + "additionalProperties": false } }, "required": [ diff --git a/packages/schemas/src/v3/gerrit.type.ts b/packages/schemas/src/v3/gerrit.type.ts index 752a63b3..01462e3a 100644 --- a/packages/schemas/src/v3/gerrit.type.ts +++ b/packages/schemas/src/v3/gerrit.type.ts @@ -27,4 +27,18 @@ export interface GerritConnectionConfig { */ hidden?: boolean; }; + revisions?: GitRevisions; +} +/** + * The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored. + */ +export interface GitRevisions { + /** + * List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored. + */ + branches?: string[]; + /** + * List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored. + */ + tags?: string[]; } diff --git a/packages/schemas/src/v3/gitlab.schema.ts b/packages/schemas/src/v3/gitlab.schema.ts index 891ca4eb..72d367e1 100644 --- a/packages/schemas/src/v3/gitlab.schema.ts +++ b/packages/schemas/src/v3/gitlab.schema.ts @@ -125,6 +125,11 @@ const schema = { "default": false, "description": "Exclude archived projects from syncing." }, + "userOwnedProjects": { + "type": "boolean", + "default": false, + "description": "Exclude user-owned projects from syncing." + }, "projects": { "type": "array", "items": { diff --git a/packages/schemas/src/v3/gitlab.type.ts b/packages/schemas/src/v3/gitlab.type.ts index f5a293ce..f25193b8 100644 --- a/packages/schemas/src/v3/gitlab.type.ts +++ b/packages/schemas/src/v3/gitlab.type.ts @@ -56,6 +56,10 @@ export interface GitlabConnectionConfig { * Exclude archived projects from syncing. */ archived?: boolean; + /** + * Exclude user-owned projects from syncing. + */ + userOwnedProjects?: boolean; /** * List of projects to exclude from syncing. Glob patterns are supported. The project's namespace must be specified, see: https://docs.gitlab.com/ee/user/namespace/ */ diff --git a/packages/schemas/src/v3/index.schema.ts b/packages/schemas/src/v3/index.schema.ts index 1eafaffe..38ec2f0a 100644 --- a/packages/schemas/src/v3/index.schema.ts +++ b/packages/schemas/src/v3/index.schema.ts @@ -68,6 +68,16 @@ const schema = { "deprecated": true, "description": "This setting is deprecated. Please use the `FORCE_ENABLE_ANONYMOUS_ACCESS` environment variable instead.", "default": false + }, + "experiment_repoDrivenPermissionSyncIntervalMs": { + "type": "number", + "description": "The interval (in milliseconds) at which the repo permission syncer should run. Defaults to 24 hours.", + "minimum": 1 + }, + "experiment_userDrivenPermissionSyncIntervalMs": { + "type": "number", + "description": "The interval (in milliseconds) at which the user permission syncer should run. Defaults to 24 hours.", + "minimum": 1 } }, "additionalProperties": false @@ -91,6 +101,13 @@ const schema = { ] ] }, + "includeConnections": { + "type": "array", + "description": "List of connections to include in the search context.", + "items": { + "type": "string" + } + }, "exclude": { "type": "array", "description": "List of repositories to exclude from the search context. Expected to be formatted as a URL without any leading http(s):// prefix (e.g., 'github.com/sourcebot-dev/sourcebot'). Glob patterns are supported.", @@ -104,14 +121,18 @@ const schema = { ] ] }, + "excludeConnections": { + "type": "array", + "description": "List of connections to exclude from the search context.", + "items": { + "type": "string" + } + }, "description": { "type": "string", "description": "Optional description of the search context that surfaces in the UI." } }, - "required": [ - "include" - ], "additionalProperties": false } }, @@ -183,6 +204,16 @@ const schema = { "deprecated": true, "description": "This setting is deprecated. Please use the `FORCE_ENABLE_ANONYMOUS_ACCESS` environment variable instead.", "default": false + }, + "experiment_repoDrivenPermissionSyncIntervalMs": { + "type": "number", + "description": "The interval (in milliseconds) at which the repo permission syncer should run. Defaults to 24 hours.", + "minimum": 1 + }, + "experiment_userDrivenPermissionSyncIntervalMs": { + "type": "number", + "description": "The interval (in milliseconds) at which the user permission syncer should run. Defaults to 24 hours.", + "minimum": 1 } }, "additionalProperties": false @@ -210,6 +241,13 @@ const schema = { ] ] }, + "includeConnections": { + "type": "array", + "description": "List of connections to include in the search context.", + "items": { + "type": "string" + } + }, "exclude": { "type": "array", "description": "List of repositories to exclude from the search context. Expected to be formatted as a URL without any leading http(s):// prefix (e.g., 'github.com/sourcebot-dev/sourcebot'). Glob patterns are supported.", @@ -223,14 +261,18 @@ const schema = { ] ] }, + "excludeConnections": { + "type": "array", + "description": "List of connections to exclude from the search context.", + "items": { + "type": "string" + } + }, "description": { "type": "string", "description": "Optional description of the search context that surfaces in the UI." } }, - "required": [ - "include" - ], "additionalProperties": false } }, @@ -583,6 +625,11 @@ const schema = { "default": false, "description": "Exclude archived projects from syncing." }, + "userOwnedProjects": { + "type": "boolean", + "default": false, + "description": "Exclude user-owned projects from syncing." + }, "projects": { "type": "array", "items": { @@ -878,6 +925,47 @@ const schema = { } }, "additionalProperties": false + }, + "revisions": { + "type": "object", + "description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.", + "properties": { + "branches": { + "type": "array", + "description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "main", + "release/*" + ], + [ + "**" + ] + ], + "default": [] + }, + "tags": { + "type": "array", + "description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "latest", + "v2.*.*" + ], + [ + "**" + ] + ], + "default": [] + } + }, + "additionalProperties": false } }, "required": [ @@ -1063,6 +1151,209 @@ const schema = { }, "additionalProperties": false }, + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "AzureDevOpsConnectionConfig", + "properties": { + "type": { + "const": "azuredevops", + "description": "Azure DevOps Configuration" + }, + "token": { + "description": "A Personal Access Token (PAT).", + "examples": [ + { + "secret": "SECRET_KEY" + } + ], + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "url": { + "type": "string", + "format": "url", + "default": "https://dev.azure.com", + "description": "The URL of the Azure DevOps host. For Azure DevOps Cloud, use https://dev.azure.com. For Azure DevOps Server, use your server URL.", + "examples": [ + "https://dev.azure.com", + "https://azuredevops.example.com" + ], + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$" + }, + "deploymentType": { + "type": "string", + "enum": [ + "cloud", + "server" + ], + "description": "The type of Azure DevOps deployment" + }, + "useTfsPath": { + "type": "boolean", + "default": false, + "description": "Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...)." + }, + "orgs": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org" + ] + ], + "description": "List of organizations to sync with. For Cloud, this is the organization name. For Server, this is the collection name. All projects and repositories visible to the provided `token` will be synced, unless explicitly defined in the `exclude` property." + }, + "projects": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+\\/[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org/my-project", + "my-collection/my-project" + ] + ], + "description": "List of specific projects to sync with. Expected to be formatted as '{orgName}/{projectName}' for Cloud or '{collectionName}/{projectName}' for Server." + }, + "repos": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[\\w.-]+\\/[\\w.-]+\\/[\\w.-]+$" + }, + "default": [], + "examples": [ + [ + "my-org/my-project/my-repo" + ] + ], + "description": "List of individual repositories to sync with. Expected to be formatted as '{orgName}/{projectName}/{repoName}'." + }, + "exclude": { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "default": false, + "description": "Exclude disabled repositories from syncing." + }, + "repos": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of repositories to exclude from syncing. Glob patterns are supported." + }, + "projects": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "List of projects to exclude from syncing. Glob patterns are supported." + }, + "size": { + "type": "object", + "description": "Exclude repositories based on their size.", + "properties": { + "min": { + "type": "integer", + "description": "Minimum repository size (in bytes) to sync (inclusive). Repositories less than this size will be excluded from syncing." + }, + "max": { + "type": "integer", + "description": "Maximum repository size (in bytes) to sync (inclusive). Repositories greater than this size will be excluded from syncing." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "revisions": { + "type": "object", + "description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.", + "properties": { + "branches": { + "type": "array", + "description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "main", + "release/*" + ], + [ + "**" + ] + ], + "default": [] + }, + "tags": { + "type": "array", + "description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.", + "items": { + "type": "string" + }, + "examples": [ + [ + "latest", + "v2.*.*" + ], + [ + "**" + ] + ], + "default": [] + } + }, + "additionalProperties": false + } + }, + "required": [ + "type", + "token", + "deploymentType" + ], + "additionalProperties": false + }, { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", @@ -1135,6 +1426,2852 @@ const schema = { } }, "additionalProperties": false + }, + "models": { + "type": "array", + "description": "Defines a collection of language models that are available to Sourcebot.", + "items": { + "type": "object", + "title": "LanguageModel", + "definitions": { + "AmazonBedrockLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "amazon-bedrock", + "description": "Amazon Bedrock Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "accessKeyId": { + "description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "accessKeySecret": { + "description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "sessionToken": { + "description": "Optional session token to use with the model. Defaults to the `AWS_SESSION_TOKEN` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "region": { + "type": "string", + "description": "The AWS region. Defaults to the `AWS_REGION` environment variable.", + "examples": [ + "us-east-1", + "us-west-2", + "eu-west-1" + ] + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "AnthropicLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "anthropic", + "description": "Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "AzureLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "azure", + "description": "Azure Configuration" + }, + "model": { + "type": "string", + "description": "The deployment name of the Azure model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "resourceName": { + "type": "string", + "description": "Azure resource name. Defaults to the `AZURE_RESOURCE_NAME` environment variable." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `AZURE_API_KEY` environment variable." + }, + "apiVersion": { + "type": "string", + "description": "Sets a custom api version. Defaults to `preview`." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Use a different URL prefix for API calls. Either this or `resourceName` can be used." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "DeepSeekLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "deepseek", + "description": "DeepSeek Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `DEEPSEEK_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "GoogleGenerativeAILanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "google-generative-ai", + "description": "Google Generative AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "GoogleVertexAnthropicLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex-anthropic", + "description": "Google Vertex AI Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the Anthropic language model running on Google Vertex.", + "examples": [ + "claude-sonnet-4" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "GoogleVertexLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex", + "description": "Google Vertex AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gemini-2.0-flash-exp", + "gemini-1.5-pro", + "gemini-1.5-flash" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "MistralLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "mistral", + "description": "Mistral AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `MISTRAL_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "OpenAILanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "openai", + "description": "OpenAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gpt-4.1", + "o4-mini", + "o3", + "o3-deep-research" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "reasoningEffort": { + "type": "string", + "description": "The reasoning effort to use with the model. Defaults to `medium`. See https://platform.openai.com/docs/guides/reasoning#get-started-with-reasonings", + "examples": [ + "minimal", + "low", + "medium", + "high" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "OpenAICompatibleLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "openai-compatible", + "description": "OpenAI Compatible Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key. If specified, adds an `Authorization` header to request headers with the value Bearer ." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Base URL of the OpenAI-compatible chat completions API endpoint.", + "examples": [ + "http://localhost:8080/v1" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + }, + "queryParams": { + "type": "object", + "description": "Optional query parameters to include in the request url.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model", + "baseUrl" + ], + "additionalProperties": false + }, + "OpenRouterLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "openrouter", + "description": "OpenRouter Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENROUTER_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "XaiLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "xai", + "description": "xAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "grok-beta", + "grok-vision-beta" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `XAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + } + }, + "oneOf": [ + { + "type": "object", + "properties": { + "provider": { + "const": "amazon-bedrock", + "description": "Amazon Bedrock Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "accessKeyId": { + "description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "accessKeySecret": { + "description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "sessionToken": { + "description": "Optional session token to use with the model. Defaults to the `AWS_SESSION_TOKEN` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "region": { + "type": "string", + "description": "The AWS region. Defaults to the `AWS_REGION` environment variable.", + "examples": [ + "us-east-1", + "us-west-2", + "eu-west-1" + ] + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "anthropic", + "description": "Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "azure", + "description": "Azure Configuration" + }, + "model": { + "type": "string", + "description": "The deployment name of the Azure model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "resourceName": { + "type": "string", + "description": "Azure resource name. Defaults to the `AZURE_RESOURCE_NAME` environment variable." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `AZURE_API_KEY` environment variable." + }, + "apiVersion": { + "type": "string", + "description": "Sets a custom api version. Defaults to `preview`." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Use a different URL prefix for API calls. Either this or `resourceName` can be used." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "deepseek", + "description": "DeepSeek Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `DEEPSEEK_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "google-generative-ai", + "description": "Google Generative AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex-anthropic", + "description": "Google Vertex AI Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the Anthropic language model running on Google Vertex.", + "examples": [ + "claude-sonnet-4" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex", + "description": "Google Vertex AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gemini-2.0-flash-exp", + "gemini-1.5-pro", + "gemini-1.5-flash" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "mistral", + "description": "Mistral AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `MISTRAL_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "openai", + "description": "OpenAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gpt-4.1", + "o4-mini", + "o3", + "o3-deep-research" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "reasoningEffort": { + "type": "string", + "description": "The reasoning effort to use with the model. Defaults to `medium`. See https://platform.openai.com/docs/guides/reasoning#get-started-with-reasonings", + "examples": [ + "minimal", + "low", + "medium", + "high" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "openai-compatible", + "description": "OpenAI Compatible Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key. If specified, adds an `Authorization` header to request headers with the value Bearer ." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Base URL of the OpenAI-compatible chat completions API endpoint.", + "examples": [ + "http://localhost:8080/v1" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + }, + "queryParams": { + "type": "object", + "description": "Optional query parameters to include in the request url.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model", + "baseUrl" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "openrouter", + "description": "OpenRouter Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENROUTER_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "xai", + "description": "xAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "grok-beta", + "grok-vision-beta" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `XAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + } + ] + } } }, "additionalProperties": false diff --git a/packages/schemas/src/v3/index.type.ts b/packages/schemas/src/v3/index.type.ts index 1390bf4e..4e8982dc 100644 --- a/packages/schemas/src/v3/index.type.ts +++ b/packages/schemas/src/v3/index.type.ts @@ -10,7 +10,21 @@ export type ConnectionConfig = | GiteaConnectionConfig | GerritConnectionConfig | BitbucketConnectionConfig + | AzureDevOpsConnectionConfig | GenericGitHostConnectionConfig; +export type LanguageModel = + | AmazonBedrockLanguageModel + | AnthropicLanguageModel + | AzureLanguageModel + | DeepSeekLanguageModel + | GoogleGenerativeAILanguageModel + | GoogleVertexAnthropicLanguageModel + | GoogleVertexLanguageModel + | MistralLanguageModel + | OpenAILanguageModel + | OpenAICompatibleLanguageModel + | OpenRouterLanguageModel + | XaiLanguageModel; export interface SourcebotConfig { $schema?: string; @@ -27,6 +41,10 @@ export interface SourcebotConfig { connections?: { [k: string]: ConnectionConfig; }; + /** + * Defines a collection of language models that are available to Sourcebot. + */ + models?: LanguageModel[]; } /** * Defines the global settings for Sourcebot. @@ -84,6 +102,14 @@ export interface Settings { * This setting is deprecated. Please use the `FORCE_ENABLE_ANONYMOUS_ACCESS` environment variable instead. */ enablePublicAccess?: boolean; + /** + * The interval (in milliseconds) at which the repo permission syncer should run. Defaults to 24 hours. + */ + experiment_repoDrivenPermissionSyncIntervalMs?: number; + /** + * The interval (in milliseconds) at which the user permission syncer should run. Defaults to 24 hours. + */ + experiment_userDrivenPermissionSyncIntervalMs?: number; } /** * Search context @@ -98,11 +124,19 @@ export interface SearchContext { /** * List of repositories to include in the search context. Expected to be formatted as a URL without any leading http(s):// prefix (e.g., 'github.com/sourcebot-dev/sourcebot'). Glob patterns are supported. */ - include: string[]; + include?: string[]; + /** + * List of connections to include in the search context. + */ + includeConnections?: string[]; /** * List of repositories to exclude from the search context. Expected to be formatted as a URL without any leading http(s):// prefix (e.g., 'github.com/sourcebot-dev/sourcebot'). Glob patterns are supported. */ exclude?: string[]; + /** + * List of connections to exclude from the search context. + */ + excludeConnections?: string[]; /** * Optional description of the search context that surfaces in the UI. */ @@ -253,6 +287,10 @@ export interface GitlabConnectionConfig { * Exclude archived projects from syncing. */ archived?: boolean; + /** + * Exclude user-owned projects from syncing. + */ + userOwnedProjects?: boolean; /** * List of projects to exclude from syncing. Glob patterns are supported. The project's namespace must be specified, see: https://docs.gitlab.com/ee/user/namespace/ */ @@ -344,6 +382,7 @@ export interface GerritConnectionConfig { */ hidden?: boolean; }; + revisions?: GitRevisions; } export interface BitbucketConnectionConfig { /** @@ -406,6 +445,80 @@ export interface BitbucketConnectionConfig { }; revisions?: GitRevisions; } +export interface AzureDevOpsConnectionConfig { + /** + * Azure DevOps Configuration + */ + type: "azuredevops"; + /** + * A Personal Access Token (PAT). + */ + token: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * The URL of the Azure DevOps host. For Azure DevOps Cloud, use https://dev.azure.com. For Azure DevOps Server, use your server URL. + */ + url?: string; + /** + * The type of Azure DevOps deployment + */ + deploymentType: "cloud" | "server"; + /** + * Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...). + */ + useTfsPath?: boolean; + /** + * List of organizations to sync with. For Cloud, this is the organization name. For Server, this is the collection name. All projects and repositories visible to the provided `token` will be synced, unless explicitly defined in the `exclude` property. + */ + orgs?: string[]; + /** + * List of specific projects to sync with. Expected to be formatted as '{orgName}/{projectName}' for Cloud or '{collectionName}/{projectName}' for Server. + */ + projects?: string[]; + /** + * List of individual repositories to sync with. Expected to be formatted as '{orgName}/{projectName}/{repoName}'. + */ + repos?: string[]; + exclude?: { + /** + * Exclude disabled repositories from syncing. + */ + disabled?: boolean; + /** + * List of repositories to exclude from syncing. Glob patterns are supported. + */ + repos?: string[]; + /** + * List of projects to exclude from syncing. Glob patterns are supported. + */ + projects?: string[]; + /** + * Exclude repositories based on their size. + */ + size?: { + /** + * Minimum repository size (in bytes) to sync (inclusive). Repositories less than this size will be excluded from syncing. + */ + min?: number; + /** + * Maximum repository size (in bytes) to sync (inclusive). Repositories greater than this size will be excluded from syncing. + */ + max?: number; + }; + }; + revisions?: GitRevisions; +} export interface GenericGitHostConnectionConfig { /** * Generic Git host configuration @@ -417,3 +530,538 @@ export interface GenericGitHostConnectionConfig { url: string; revisions?: GitRevisions; } +export interface AmazonBedrockLanguageModel { + /** + * Amazon Bedrock Configuration + */ + provider: "amazon-bedrock"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable. + */ + accessKeyId?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable. + */ + accessKeySecret?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional session token to use with the model. Defaults to the `AWS_SESSION_TOKEN` environment variable. + */ + sessionToken?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * The AWS region. Defaults to the `AWS_REGION` environment variable. + */ + region?: string; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +/** + * Optional headers to use with the model. + */ +export interface LanguageModelHeaders { + /** + * This interface was referenced by `LanguageModelHeaders`'s JSON-Schema definition + * via the `patternProperty` "^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$". + */ + [k: string]: + | string + | ( + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + } + ); +} +export interface AnthropicLanguageModel { + /** + * Anthropic Configuration + */ + provider: "anthropic"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface AzureLanguageModel { + /** + * Azure Configuration + */ + provider: "azure"; + /** + * The deployment name of the Azure model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Azure resource name. Defaults to the `AZURE_RESOURCE_NAME` environment variable. + */ + resourceName?: string; + /** + * Optional API key to use with the model. Defaults to the `AZURE_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Sets a custom api version. Defaults to `preview`. + */ + apiVersion?: string; + /** + * Use a different URL prefix for API calls. Either this or `resourceName` can be used. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface DeepSeekLanguageModel { + /** + * DeepSeek Configuration + */ + provider: "deepseek"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `DEEPSEEK_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface GoogleGenerativeAILanguageModel { + /** + * Google Generative AI Configuration + */ + provider: "google-generative-ai"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface GoogleVertexAnthropicLanguageModel { + /** + * Google Vertex AI Anthropic Configuration + */ + provider: "google-vertex-anthropic"; + /** + * The name of the Anthropic language model running on Google Vertex. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable. + */ + project?: string; + /** + * The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable. + */ + region?: string; + /** + * Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials. + */ + credentials?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface GoogleVertexLanguageModel { + /** + * Google Vertex AI Configuration + */ + provider: "google-vertex"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable. + */ + project?: string; + /** + * The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable. + */ + region?: string; + /** + * Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials. + */ + credentials?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface MistralLanguageModel { + /** + * Mistral AI Configuration + */ + provider: "mistral"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `MISTRAL_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface OpenAILanguageModel { + /** + * OpenAI Configuration + */ + provider: "openai"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + /** + * The reasoning effort to use with the model. Defaults to `medium`. See https://platform.openai.com/docs/guides/reasoning#get-started-with-reasonings + */ + reasoningEffort?: string; + headers?: LanguageModelHeaders; +} +export interface OpenAICompatibleLanguageModel { + /** + * OpenAI Compatible Configuration + */ + provider: "openai-compatible"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key. If specified, adds an `Authorization` header to request headers with the value Bearer . + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Base URL of the OpenAI-compatible chat completions API endpoint. + */ + baseUrl: string; + headers?: LanguageModelHeaders; + queryParams?: LanguageModelQueryParams; +} +/** + * Optional query parameters to include in the request url. + */ +export interface LanguageModelQueryParams { + /** + * This interface was referenced by `LanguageModelQueryParams`'s JSON-Schema definition + * via the `patternProperty` "^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$". + */ + [k: string]: + | string + | ( + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + } + ); +} +export interface OpenRouterLanguageModel { + /** + * OpenRouter Configuration + */ + provider: "openrouter"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `OPENROUTER_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface XaiLanguageModel { + /** + * xAI Configuration + */ + provider: "xai"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `XAI_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} diff --git a/packages/schemas/src/v3/languageModel.schema.ts b/packages/schemas/src/v3/languageModel.schema.ts new file mode 100644 index 00000000..cd879e59 --- /dev/null +++ b/packages/schemas/src/v3/languageModel.schema.ts @@ -0,0 +1,2844 @@ +// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! +const schema = { + "type": "object", + "title": "LanguageModel", + "definitions": { + "AmazonBedrockLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "amazon-bedrock", + "description": "Amazon Bedrock Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "accessKeyId": { + "description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "accessKeySecret": { + "description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "sessionToken": { + "description": "Optional session token to use with the model. Defaults to the `AWS_SESSION_TOKEN` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "region": { + "type": "string", + "description": "The AWS region. Defaults to the `AWS_REGION` environment variable.", + "examples": [ + "us-east-1", + "us-west-2", + "eu-west-1" + ] + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "AnthropicLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "anthropic", + "description": "Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "AzureLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "azure", + "description": "Azure Configuration" + }, + "model": { + "type": "string", + "description": "The deployment name of the Azure model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "resourceName": { + "type": "string", + "description": "Azure resource name. Defaults to the `AZURE_RESOURCE_NAME` environment variable." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `AZURE_API_KEY` environment variable." + }, + "apiVersion": { + "type": "string", + "description": "Sets a custom api version. Defaults to `preview`." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Use a different URL prefix for API calls. Either this or `resourceName` can be used." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "DeepSeekLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "deepseek", + "description": "DeepSeek Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `DEEPSEEK_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "GoogleGenerativeAILanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "google-generative-ai", + "description": "Google Generative AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "GoogleVertexAnthropicLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex-anthropic", + "description": "Google Vertex AI Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the Anthropic language model running on Google Vertex.", + "examples": [ + "claude-sonnet-4" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "GoogleVertexLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex", + "description": "Google Vertex AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gemini-2.0-flash-exp", + "gemini-1.5-pro", + "gemini-1.5-flash" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "MistralLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "mistral", + "description": "Mistral AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `MISTRAL_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "OpenAILanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "openai", + "description": "OpenAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gpt-4.1", + "o4-mini", + "o3", + "o3-deep-research" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "reasoningEffort": { + "type": "string", + "description": "The reasoning effort to use with the model. Defaults to `medium`. See https://platform.openai.com/docs/guides/reasoning#get-started-with-reasonings", + "examples": [ + "minimal", + "low", + "medium", + "high" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "OpenAICompatibleLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "openai-compatible", + "description": "OpenAI Compatible Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key. If specified, adds an `Authorization` header to request headers with the value Bearer ." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Base URL of the OpenAI-compatible chat completions API endpoint.", + "examples": [ + "http://localhost:8080/v1" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + }, + "queryParams": { + "type": "object", + "description": "Optional query parameters to include in the request url.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model", + "baseUrl" + ], + "additionalProperties": false + }, + "OpenRouterLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "openrouter", + "description": "OpenRouter Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENROUTER_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + "XaiLanguageModel": { + "type": "object", + "properties": { + "provider": { + "const": "xai", + "description": "xAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "grok-beta", + "grok-vision-beta" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `XAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + } + }, + "oneOf": [ + { + "type": "object", + "properties": { + "provider": { + "const": "amazon-bedrock", + "description": "Amazon Bedrock Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "accessKeyId": { + "description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "accessKeySecret": { + "description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "sessionToken": { + "description": "Optional session token to use with the model. Defaults to the `AWS_SESSION_TOKEN` environment variable.", + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + }, + "region": { + "type": "string", + "description": "The AWS region. Defaults to the `AWS_REGION` environment variable.", + "examples": [ + "us-east-1", + "us-west-2", + "eu-west-1" + ] + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "anthropic", + "description": "Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "azure", + "description": "Azure Configuration" + }, + "model": { + "type": "string", + "description": "The deployment name of the Azure model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "resourceName": { + "type": "string", + "description": "Azure resource name. Defaults to the `AZURE_RESOURCE_NAME` environment variable." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `AZURE_API_KEY` environment variable." + }, + "apiVersion": { + "type": "string", + "description": "Sets a custom api version. Defaults to `preview`." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Use a different URL prefix for API calls. Either this or `resourceName` can be used." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "deepseek", + "description": "DeepSeek Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `DEEPSEEK_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "google-generative-ai", + "description": "Google Generative AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex-anthropic", + "description": "Google Vertex AI Anthropic Configuration" + }, + "model": { + "type": "string", + "description": "The name of the Anthropic language model running on Google Vertex.", + "examples": [ + "claude-sonnet-4" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "google-vertex", + "description": "Google Vertex AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gemini-2.0-flash-exp", + "gemini-1.5-pro", + "gemini-1.5-flash" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "project": { + "type": "string", + "description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable." + }, + "region": { + "type": "string", + "description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.", + "examples": [ + "us-central1", + "us-east1", + "europe-west1" + ] + }, + "credentials": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "mistral", + "description": "Mistral AI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `MISTRAL_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "openai", + "description": "OpenAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "gpt-4.1", + "o4-mini", + "o3", + "o3-deep-research" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "reasoningEffort": { + "type": "string", + "description": "The reasoning effort to use with the model. Defaults to `medium`. See https://platform.openai.com/docs/guides/reasoning#get-started-with-reasonings", + "examples": [ + "minimal", + "low", + "medium", + "high" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "openai-compatible", + "description": "OpenAI Compatible Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key. If specified, adds an `Authorization` header to request headers with the value Bearer ." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Base URL of the OpenAI-compatible chat completions API endpoint.", + "examples": [ + "http://localhost:8080/v1" + ] + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + }, + "queryParams": { + "type": "object", + "description": "Optional query parameters to include in the request url.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model", + "baseUrl" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "openrouter", + "description": "OpenRouter Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model." + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `OPENROUTER_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "const": "xai", + "description": "xAI Configuration" + }, + "model": { + "type": "string", + "description": "The name of the language model.", + "examples": [ + "grok-beta", + "grok-vision-beta" + ] + }, + "displayName": { + "type": "string", + "description": "Optional display name." + }, + "token": { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ], + "description": "Optional API key to use with the model. Defaults to the `XAI_API_KEY` environment variable." + }, + "baseUrl": { + "type": "string", + "format": "url", + "pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$", + "description": "Optional base URL." + }, + "headers": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider", + "model" + ], + "additionalProperties": false + } + ] +} as const; +export { schema as languageModelSchema }; \ No newline at end of file diff --git a/packages/schemas/src/v3/languageModel.type.ts b/packages/schemas/src/v3/languageModel.type.ts new file mode 100644 index 00000000..803a6c17 --- /dev/null +++ b/packages/schemas/src/v3/languageModel.type.ts @@ -0,0 +1,551 @@ +// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! + +export type LanguageModel = + | AmazonBedrockLanguageModel + | AnthropicLanguageModel + | AzureLanguageModel + | DeepSeekLanguageModel + | GoogleGenerativeAILanguageModel + | GoogleVertexAnthropicLanguageModel + | GoogleVertexLanguageModel + | MistralLanguageModel + | OpenAILanguageModel + | OpenAICompatibleLanguageModel + | OpenRouterLanguageModel + | XaiLanguageModel; + +export interface AmazonBedrockLanguageModel { + /** + * Amazon Bedrock Configuration + */ + provider: "amazon-bedrock"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable. + */ + accessKeyId?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable. + */ + accessKeySecret?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional session token to use with the model. Defaults to the `AWS_SESSION_TOKEN` environment variable. + */ + sessionToken?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * The AWS region. Defaults to the `AWS_REGION` environment variable. + */ + region?: string; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +/** + * Optional headers to use with the model. + */ +export interface LanguageModelHeaders { + /** + * This interface was referenced by `LanguageModelHeaders`'s JSON-Schema definition + * via the `patternProperty` "^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$". + */ + [k: string]: + | string + | ( + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + } + ); +} +export interface AnthropicLanguageModel { + /** + * Anthropic Configuration + */ + provider: "anthropic"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface AzureLanguageModel { + /** + * Azure Configuration + */ + provider: "azure"; + /** + * The deployment name of the Azure model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Azure resource name. Defaults to the `AZURE_RESOURCE_NAME` environment variable. + */ + resourceName?: string; + /** + * Optional API key to use with the model. Defaults to the `AZURE_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Sets a custom api version. Defaults to `preview`. + */ + apiVersion?: string; + /** + * Use a different URL prefix for API calls. Either this or `resourceName` can be used. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface DeepSeekLanguageModel { + /** + * DeepSeek Configuration + */ + provider: "deepseek"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `DEEPSEEK_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface GoogleGenerativeAILanguageModel { + /** + * Google Generative AI Configuration + */ + provider: "google-generative-ai"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface GoogleVertexAnthropicLanguageModel { + /** + * Google Vertex AI Anthropic Configuration + */ + provider: "google-vertex-anthropic"; + /** + * The name of the Anthropic language model running on Google Vertex. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable. + */ + project?: string; + /** + * The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable. + */ + region?: string; + /** + * Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials. + */ + credentials?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface GoogleVertexLanguageModel { + /** + * Google Vertex AI Configuration + */ + provider: "google-vertex"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable. + */ + project?: string; + /** + * The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable. + */ + region?: string; + /** + * Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials. + */ + credentials?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface MistralLanguageModel { + /** + * Mistral AI Configuration + */ + provider: "mistral"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `MISTRAL_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface OpenAILanguageModel { + /** + * OpenAI Configuration + */ + provider: "openai"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + /** + * The reasoning effort to use with the model. Defaults to `medium`. See https://platform.openai.com/docs/guides/reasoning#get-started-with-reasonings + */ + reasoningEffort?: string; + headers?: LanguageModelHeaders; +} +export interface OpenAICompatibleLanguageModel { + /** + * OpenAI Compatible Configuration + */ + provider: "openai-compatible"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key. If specified, adds an `Authorization` header to request headers with the value Bearer . + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Base URL of the OpenAI-compatible chat completions API endpoint. + */ + baseUrl: string; + headers?: LanguageModelHeaders; + queryParams?: LanguageModelQueryParams; +} +/** + * Optional query parameters to include in the request url. + */ +export interface LanguageModelQueryParams { + /** + * This interface was referenced by `LanguageModelQueryParams`'s JSON-Schema definition + * via the `patternProperty` "^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$". + */ + [k: string]: + | string + | ( + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + } + ); +} +export interface OpenRouterLanguageModel { + /** + * OpenRouter Configuration + */ + provider: "openrouter"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `OPENROUTER_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} +export interface XaiLanguageModel { + /** + * xAI Configuration + */ + provider: "xai"; + /** + * The name of the language model. + */ + model: string; + /** + * Optional display name. + */ + displayName?: string; + /** + * Optional API key to use with the model. Defaults to the `XAI_API_KEY` environment variable. + */ + token?: + | { + /** + * The name of the secret that contains the token. + */ + secret: string; + } + | { + /** + * The name of the environment variable that contains the token. Only supported in declarative connection configs. + */ + env: string; + }; + /** + * Optional base URL. + */ + baseUrl?: string; + headers?: LanguageModelHeaders; +} diff --git a/packages/schemas/src/v3/searchContext.schema.ts b/packages/schemas/src/v3/searchContext.schema.ts index c36cb801..7d97b94f 100644 --- a/packages/schemas/src/v3/searchContext.schema.ts +++ b/packages/schemas/src/v3/searchContext.schema.ts @@ -18,6 +18,13 @@ const schema = { ] ] }, + "includeConnections": { + "type": "array", + "description": "List of connections to include in the search context.", + "items": { + "type": "string" + } + }, "exclude": { "type": "array", "description": "List of repositories to exclude from the search context. Expected to be formatted as a URL without any leading http(s):// prefix (e.g., 'github.com/sourcebot-dev/sourcebot'). Glob patterns are supported.", @@ -31,14 +38,18 @@ const schema = { ] ] }, + "excludeConnections": { + "type": "array", + "description": "List of connections to exclude from the search context.", + "items": { + "type": "string" + } + }, "description": { "type": "string", "description": "Optional description of the search context that surfaces in the UI." } }, - "required": [ - "include" - ], "additionalProperties": false } as const; export { schema as searchContextSchema }; \ No newline at end of file diff --git a/packages/schemas/src/v3/searchContext.type.ts b/packages/schemas/src/v3/searchContext.type.ts index 255b5a03..ca2cb1b2 100644 --- a/packages/schemas/src/v3/searchContext.type.ts +++ b/packages/schemas/src/v3/searchContext.type.ts @@ -7,11 +7,19 @@ export interface SearchContext { /** * List of repositories to include in the search context. Expected to be formatted as a URL without any leading http(s):// prefix (e.g., 'github.com/sourcebot-dev/sourcebot'). Glob patterns are supported. */ - include: string[]; + include?: string[]; + /** + * List of connections to include in the search context. + */ + includeConnections?: string[]; /** * List of repositories to exclude from the search context. Expected to be formatted as a URL without any leading http(s):// prefix (e.g., 'github.com/sourcebot-dev/sourcebot'). Glob patterns are supported. */ exclude?: string[]; + /** + * List of connections to exclude from the search context. + */ + excludeConnections?: string[]; /** * Optional description of the search context that surfaces in the UI. */ diff --git a/packages/schemas/src/v3/shared.schema.ts b/packages/schemas/src/v3/shared.schema.ts index 0c1792ae..5ecacd44 100644 --- a/packages/schemas/src/v3/shared.schema.ts +++ b/packages/schemas/src/v3/shared.schema.ts @@ -73,6 +73,94 @@ const schema = { } }, "additionalProperties": false + }, + "LanguageModelHeaders": { + "type": "object", + "description": "Optional headers to use with the model.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false + }, + "LanguageModelQueryParams": { + "type": "object", + "description": "Optional query parameters to include in the request url.", + "patternProperties": { + "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "secret": { + "type": "string", + "description": "The name of the secret that contains the token." + } + }, + "required": [ + "secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "The name of the environment variable that contains the token. Only supported in declarative connection configs." + } + }, + "required": [ + "env" + ], + "additionalProperties": false + } + ] + } + ] + } + }, + "additionalProperties": false } } } as const; diff --git a/packages/schemas/src/v3/shared.type.ts b/packages/schemas/src/v3/shared.type.ts index eeec734c..727de2be 100644 --- a/packages/schemas/src/v3/shared.type.ts +++ b/packages/schemas/src/v3/shared.type.ts @@ -37,3 +37,29 @@ export interface GitRevisions { */ tags?: string[]; } +/** + * Optional headers to use with the model. + * + * This interface was referenced by `Shared`'s JSON-Schema + * via the `definition` "LanguageModelHeaders". + */ +export interface LanguageModelHeaders { + /** + * This interface was referenced by `LanguageModelHeaders`'s JSON-Schema definition + * via the `patternProperty` "^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$". + */ + [k: string]: string | Token; +} +/** + * Optional query parameters to include in the request url. + * + * This interface was referenced by `Shared`'s JSON-Schema + * via the `definition` "LanguageModelQueryParams". + */ +export interface LanguageModelQueryParams { + /** + * This interface was referenced by `LanguageModelQueryParams`'s JSON-Schema definition + * via the `patternProperty` "^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$". + */ + [k: string]: string | Token; +} diff --git a/packages/shared/src/ee/syncSearchContexts.ts b/packages/shared/src/ee/syncSearchContexts.ts index 7ab59b35..8dd932ae 100644 --- a/packages/shared/src/ee/syncSearchContexts.ts +++ b/packages/shared/src/ee/syncSearchContexts.ts @@ -36,9 +36,39 @@ export const syncSearchContexts = async (params: SyncSearchContextsParams) => { } }); - let newReposInContext = allRepos.filter(repo => { - return micromatch.isMatch(repo.name, newContextConfig.include); - }); + let newReposInContext: { id: number, name: string }[] = []; + if(newContextConfig.include) { + newReposInContext = allRepos.filter(repo => { + return micromatch.isMatch(repo.name, newContextConfig.include!); + }); + } + + if(newContextConfig.includeConnections) { + const connections = await db.connection.findMany({ + where: { + orgId, + name: { + in: newContextConfig.includeConnections, + } + }, + include: { + repos: { + select: { + repo: { + select: { + id: true, + name: true, + } + } + } + } + } + }); + + for (const connection of connections) { + newReposInContext = newReposInContext.concat(connection.repos.map(repo => repo.repo)); + } + } if (newContextConfig.exclude) { const exclude = newContextConfig.exclude; @@ -47,6 +77,35 @@ export const syncSearchContexts = async (params: SyncSearchContextsParams) => { }); } + if (newContextConfig.excludeConnections) { + const connections = await db.connection.findMany({ + where: { + orgId, + name: { + in: newContextConfig.excludeConnections, + } + }, + include: { + repos: { + select: { + repo: { + select: { + id: true, + name: true, + } + } + } + } + } + }); + + for (const connection of connections) { + newReposInContext = newReposInContext.filter(repo => { + return !connection.repos.map(r => r.repo.id).includes(repo.id); + }); + } + } + const currentReposInContext = (await db.searchContext.findUnique({ where: { name_orgId: { diff --git a/packages/shared/src/entitlements.ts b/packages/shared/src/entitlements.ts index 965989c1..be40b927 100644 --- a/packages/shared/src/entitlements.ts +++ b/packages/shared/src/entitlements.ts @@ -38,15 +38,16 @@ const entitlements = [ "sso", "code-nav", "audit", - "analytics" + "analytics", + "permission-syncing" ] as const; export type Entitlement = (typeof entitlements)[number]; const entitlementsByPlan: Record = { oss: ["anonymous-access"], "cloud:team": ["billing", "multi-tenancy", "sso", "code-nav"], - "self-hosted:enterprise": ["search-contexts", "sso", "code-nav", "audit", "analytics"], - "self-hosted:enterprise-unlimited": ["search-contexts", "anonymous-access", "sso", "code-nav", "audit", "analytics"], + "self-hosted:enterprise": ["search-contexts", "sso", "code-nav", "audit", "analytics", "permission-syncing"], + "self-hosted:enterprise-unlimited": ["search-contexts", "anonymous-access", "sso", "code-nav", "audit", "analytics", "permission-syncing"], // Special entitlement for https://demo.sourcebot.dev "cloud:demo": ["anonymous-access", "code-nav", "search-contexts"], } as const; diff --git a/packages/shared/src/index.server.ts b/packages/shared/src/index.server.ts index 3cc4be65..ce00642e 100644 --- a/packages/shared/src/index.server.ts +++ b/packages/shared/src/index.server.ts @@ -12,6 +12,7 @@ export type { export { base64Decode, loadConfig, + loadJsonFile, isRemotePath, } from "./utils.js"; export { diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index 94f66324..69cbc52e 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -3,6 +3,7 @@ import { indexSchema } from "@sourcebot/schemas/v3/index.schema"; import { readFile } from 'fs/promises'; import stripJsonComments from 'strip-json-comments'; import { Ajv } from "ajv"; +import { z } from "zod"; const ajv = new Ajv({ validateFormats: false, @@ -18,6 +19,66 @@ export const isRemotePath = (path: string) => { return path.startsWith('https://') || path.startsWith('http://'); } +// TODO: Merge this with config loading logic which uses AJV +export const loadJsonFile = async ( + filePath: string, + schema: any +): Promise => { + const fileContent = await (async () => { + if (isRemotePath(filePath)) { + const response = await fetch(filePath); + if (!response.ok) { + throw new Error(`Failed to fetch file ${filePath}: ${response.statusText}`); + } + return response.text(); + } else { + // Retry logic for handling race conditions with mounted volumes + const maxAttempts = 5; + const retryDelayMs = 2000; + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await readFile(filePath, { + encoding: 'utf-8', + }); + } catch (error) { + lastError = error as Error; + + // Only retry on ENOENT errors (file not found) + if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') { + throw error; // Throw immediately for non-ENOENT errors + } + + // Log warning before retry (except on the last attempt) + if (attempt < maxAttempts) { + console.warn(`File not found, retrying in 2s... (Attempt ${attempt}/${maxAttempts})`); + await new Promise(resolve => setTimeout(resolve, retryDelayMs)); + } + } + } + + // If we've exhausted all retries, throw the last ENOENT error + if (lastError) { + throw lastError; + } + + throw new Error('Failed to load file after all retry attempts'); + } + })(); + + const parsedData = JSON.parse(stripJsonComments(fileContent)); + + try { + return schema.parse(parsedData); + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error(`File '${filePath}' is invalid: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`); + } + throw error; + } +} + export const loadConfig = async (configPath: string): Promise => { const configContent = await (async () => { if (isRemotePath(configPath)) { @@ -27,9 +88,38 @@ export const loadConfig = async (configPath: string): Promise = } return response.text(); } else { - return readFile(configPath, { - encoding: 'utf-8', - }); + // Retry logic for handling race conditions with mounted volumes + const maxAttempts = 5; + const retryDelayMs = 2000; + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await readFile(configPath, { + encoding: 'utf-8', + }); + } catch (error) { + lastError = error as Error; + + // Only retry on ENOENT errors (file not found) + if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') { + throw error; // Throw immediately for non-ENOENT errors + } + + // Log warning before retry (except on the last attempt) + if (attempt < maxAttempts) { + console.warn(`Config file not found, retrying in 2s... (Attempt ${attempt}/${maxAttempts})`); + await new Promise(resolve => setTimeout(resolve, retryDelayMs)); + } + } + } + + // If we've exhausted all retries, throw the last ENOENT error + if (lastError) { + throw lastError; + } + + throw new Error('Failed to load config after all retry attempts'); } })(); diff --git a/packages/web/.eslintignore b/packages/web/.eslintignore index 68469290..b1a30067 100644 --- a/packages/web/.eslintignore +++ b/packages/web/.eslintignore @@ -1,2 +1,3 @@ # shadcn components src/components/ +next-env.d.ts \ No newline at end of file diff --git a/packages/web/next.config.mjs b/packages/web/next.config.mjs index de511a92..2ab0b642 100644 --- a/packages/web/next.config.mjs +++ b/packages/web/next.config.mjs @@ -38,6 +38,8 @@ const nextConfig = { }, ] }, + + turbopack: {} }; export default withSentryConfig(nextConfig, { diff --git a/packages/web/package.json b/packages/web/package.json index 62ac9690..709d0e47 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -3,16 +3,28 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", - "build": "next build", + "dev": "next dev --turbopack", + "build": "cross-env SKIP_ENV_VALIDATION=1 next build", "start": "next start", - "lint": "cross-env SKIP_ENV_VALIDATION=1 next lint", - "test": "vitest", + "lint": "cross-env SKIP_ENV_VALIDATION=1 eslint .", + "test": "cross-env SKIP_ENV_VALIDATION=1 vitest", "dev:emails": "email dev --dir ./src/emails", "stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe" }, "dependencies": { + "@ai-sdk/amazon-bedrock": "^3.0.22", + "@ai-sdk/anthropic": "^2.0.17", + "@ai-sdk/azure": "^2.0.32", + "@ai-sdk/deepseek": "^1.0.18", + "@ai-sdk/google": "^2.0.14", + "@ai-sdk/google-vertex": "^3.0.27", + "@ai-sdk/mistral": "^2.0.14", + "@ai-sdk/openai": "^2.0.32", + "@ai-sdk/openai-compatible": "^1.0.18", + "@ai-sdk/react": "^2.0.45", + "@ai-sdk/xai": "^2.0.20", "@auth/prisma-adapter": "^2.7.4", + "@aws-sdk/credential-providers": "^3.890.0", "@codemirror/commands": "^6.6.0", "@codemirror/lang-cpp": "^6.0.2", "@codemirror/lang-css": "^6.3.0", @@ -43,9 +55,15 @@ "@hookform/resolvers": "^3.9.0", "@iconify/react": "^5.1.0", "@iizukak/codemirror-lang-wgsl": "^0.3.0", + "@openrouter/ai-sdk-provider": "^1.1.0", + "@opentelemetry/api-logs": "^0.203.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/sdk-logs": "^0.203.0", + "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.5", "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-hover-card": "^1.1.6", @@ -82,16 +100,20 @@ "@stripe/react-stripe-js": "^3.1.1", "@stripe/stripe-js": "^5.6.0", "@t3-oss/env-nextjs": "^0.12.0", + "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "^5.53.3", "@tanstack/react-table": "^8.20.5", "@tanstack/react-virtual": "^3.10.8", "@uidotdev/usehooks": "^2.4.1", "@uiw/codemirror-themes": "^4.23.6", "@uiw/react-codemirror": "^4.23.0", + "@vercel/otel": "^1.13.0", "@viz-js/lang-dot": "^1.0.4", "@xiechao/codemirror-lang-handlebars": "^1.0.4", + "ai": "^5.0.45", "ajv": "^8.17.1", "bcryptjs": "^3.0.2", + "chokidar": "^4.0.3", "class-variance-authority": "^0.7.0", "client-only": "^0.0.1", "clsx": "^2.1.1", @@ -117,14 +139,17 @@ "embla-carousel-react": "^8.3.0", "escape-string-regexp": "^5.0.0", "fuse.js": "^7.0.0", - "google-auth-library": "^9.15.1", + "google-auth-library": "^10.1.0", "graphql": "^16.9.0", "http-status-codes": "^2.3.0", "input-otp": "^1.4.2", + "langfuse": "^3.38.4", + "langfuse-vercel": "^3.38.4", "lucide-react": "^0.517.0", "micromatch": "^4.0.8", - "next": "14.2.26", + "next": "15.5.0", "next-auth": "^5.0.0-beta.25", + "next-navigation-guard": "^0.2.0", "next-themes": "^0.3.0", "nodemailer": "^6.10.0", "octokit": "^4.1.3", @@ -133,42 +158,53 @@ "posthog-js": "^1.161.5", "pretty-bytes": "^6.1.1", "psl": "^1.15.0", - "react": "^18", + "react": "19.1.1", "react-device-detect": "^2.2.3", - "react-dom": "^18", + "react-dom": "19.1.1", "react-hook-form": "^7.53.0", "react-hotkeys-hook": "^4.5.1", "react-icons": "^5.3.0", + "react-markdown": "^10.1.0", "react-resizable-panels": "^2.1.1", "recharts": "^2.15.3", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", "scroll-into-view-if-needed": "^3.1.0", "server-only": "^0.0.1", "sharp": "^0.33.5", "simple-git": "^3.27.0", + "slate": "^0.117.0", + "slate-dom": "^0.116.0", + "slate-history": "^0.113.1", + "slate-react": "^0.117.1", "strip-json-comments": "^5.0.1", "stripe": "^17.6.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "usehooks-ts": "^3.1.0", "vscode-icons-js": "^11.6.1", - "zod": "^3.24.3", + "zod": "^3.25.74", "zod-to-json-schema": "^3.24.5" }, "devDependencies": { + "@eslint/eslintrc": "^3", "@tanstack/eslint-plugin-query": "^5.74.7", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", "@types/micromatch": "^4.0.9", "@types/node": "^20", "@types/nodemailer": "^6.4.17", "@types/psl": "^1.1.3", - "@types/react": "^18", - "@types/react-dom": "^18", - "@typescript-eslint/eslint-plugin": "^8.3.0", - "@typescript-eslint/parser": "^8.3.0", + "@types/react": "19.1.10", + "@types/react-dom": "19.1.7", + "@typescript-eslint/eslint-plugin": "^8.40.0", + "@typescript-eslint/parser": "^8.40.0", "cross-env": "^7.0.3", "eslint": "^8", - "eslint-config-next": "14.2.6", - "eslint-plugin-react": "^7.35.0", - "eslint-plugin-react-hooks": "^4.6.2", + "eslint-config-next": "15.5.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", "jsdom": "^25.0.1", "npm-run-all": "^4.1.5", "postcss": "^8", @@ -177,6 +213,11 @@ "tsx": "^4.19.2", "typescript": "^5", "vite-tsconfig-paths": "^5.1.3", - "vitest": "^2.1.5" + "vitest": "^2.1.5", + "vitest-mock-extended": "^3.1.0" + }, + "resolutions": { + "@types/react": "19.1.10", + "@types/react-dom": "19.1.7" } } diff --git a/packages/web/public/anthropic.svg b/packages/web/public/anthropic.svg new file mode 100644 index 00000000..578301de --- /dev/null +++ b/packages/web/public/anthropic.svg @@ -0,0 +1,7 @@ + + + Anthropic Icon Streamline Icon: https://streamlinehq.com + + + + \ No newline at end of file diff --git a/packages/web/public/ask_sb_tutorial_at_mentions.png b/packages/web/public/ask_sb_tutorial_at_mentions.png new file mode 100644 index 00000000..412b02fe Binary files /dev/null and b/packages/web/public/ask_sb_tutorial_at_mentions.png differ diff --git a/packages/web/public/ask_sb_tutorial_citations.png b/packages/web/public/ask_sb_tutorial_citations.png new file mode 100644 index 00000000..2266fdb5 Binary files /dev/null and b/packages/web/public/ask_sb_tutorial_citations.png differ diff --git a/packages/web/public/ask_sb_tutorial_search_scope.png b/packages/web/public/ask_sb_tutorial_search_scope.png new file mode 100644 index 00000000..33376ca8 Binary files /dev/null and b/packages/web/public/ask_sb_tutorial_search_scope.png differ diff --git a/packages/web/public/azureai.svg b/packages/web/public/azureai.svg new file mode 100644 index 00000000..c4ed5c4d --- /dev/null +++ b/packages/web/public/azureai.svg @@ -0,0 +1 @@ +AzureAI \ No newline at end of file diff --git a/packages/web/public/azuredevops.svg b/packages/web/public/azuredevops.svg new file mode 100644 index 00000000..3d4a462f --- /dev/null +++ b/packages/web/public/azuredevops.svg @@ -0,0 +1 @@ +Icon-devops-261 \ No newline at end of file diff --git a/packages/web/public/bedrock.svg b/packages/web/public/bedrock.svg new file mode 100644 index 00000000..a5649399 --- /dev/null +++ b/packages/web/public/bedrock.svg @@ -0,0 +1 @@ +Bedrock \ No newline at end of file diff --git a/packages/web/public/deepseek.svg b/packages/web/public/deepseek.svg new file mode 100644 index 00000000..3fc23024 --- /dev/null +++ b/packages/web/public/deepseek.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/packages/web/public/gemini.svg b/packages/web/public/gemini.svg new file mode 100644 index 00000000..3d7ccb42 --- /dev/null +++ b/packages/web/public/gemini.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/web/public/mistral.svg b/packages/web/public/mistral.svg new file mode 100644 index 00000000..8e03e244 --- /dev/null +++ b/packages/web/public/mistral.svg @@ -0,0 +1 @@ +Mistral \ No newline at end of file diff --git a/packages/web/public/openai.svg b/packages/web/public/openai.svg new file mode 100644 index 00000000..3b4eff96 --- /dev/null +++ b/packages/web/public/openai.svg @@ -0,0 +1,2 @@ + +OpenAI icon \ No newline at end of file diff --git a/packages/web/public/openrouter.svg b/packages/web/public/openrouter.svg new file mode 100644 index 00000000..e6cca2a8 --- /dev/null +++ b/packages/web/public/openrouter.svg @@ -0,0 +1 @@ +OpenRouter \ No newline at end of file diff --git a/packages/web/public/xai.svg b/packages/web/public/xai.svg new file mode 100644 index 00000000..536e7139 --- /dev/null +++ b/packages/web/public/xai.svg @@ -0,0 +1 @@ +Grok \ No newline at end of file diff --git a/packages/web/src/__mocks__/prisma.ts b/packages/web/src/__mocks__/prisma.ts new file mode 100644 index 00000000..4db4de46 --- /dev/null +++ b/packages/web/src/__mocks__/prisma.ts @@ -0,0 +1,50 @@ +import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID, SINGLE_TENANT_ORG_NAME } from '@/lib/constants'; +import { ApiKey, Org, PrismaClient, User } from '@prisma/client'; +import { beforeEach, vi } from 'vitest'; +import { mockDeep, mockReset } from 'vitest-mock-extended'; + +beforeEach(() => { + mockReset(prisma); +}); + +export const prisma = mockDeep(); + +export const MOCK_ORG: Org = { + id: SINGLE_TENANT_ORG_ID, + name: SINGLE_TENANT_ORG_NAME, + domain: SINGLE_TENANT_ORG_DOMAIN, + createdAt: new Date(), + updatedAt: new Date(), + isOnboarded: true, + imageUrl: null, + metadata: null, + memberApprovalRequired: false, + stripeCustomerId: null, + stripeSubscriptionStatus: null, + stripeLastUpdatedAt: null, + inviteLinkEnabled: false, + inviteLinkId: null +} + +export const MOCK_API_KEY: ApiKey = { + name: 'Test API Key', + hash: 'apikey', + createdAt: new Date(), + lastUsedAt: new Date(), + orgId: 1, + createdById: '1', +} + +export const MOCK_USER: User = { + id: '1', + name: 'Test User', + email: 'test@test.com', + createdAt: new Date(), + updatedAt: new Date(), + hashedPassword: null, + emailVerified: null, + image: null, + permissionSyncedAt: null +} + +export const userScopedPrismaClientExtension = vi.fn(); \ No newline at end of file diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index ed73950a..5b73922c 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -1,44 +1,46 @@ 'use server'; +import { getAuditService } from "@/ee/features/audit/factory"; import { env } from "@/env.mjs"; +import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils"; import { ErrorCode } from "@/lib/errorCodes"; import { notAuthenticated, notFound, orgNotFound, secretAlreadyExists, ServiceError, ServiceErrorException, unexpectedError } from "@/lib/serviceError"; -import { CodeHostType, isServiceError } from "@/lib/utils"; +import { CodeHostType, getOrgMetadata, isHttpError, isServiceError } from "@/lib/utils"; import { prisma } from "@/prisma"; import { render } from "@react-email/components"; import * as Sentry from '@sentry/nextjs'; -import { decrypt, encrypt, generateApiKey, hashSecret, getTokenFromConfig } from "@sourcebot/crypto"; -import { ConnectionSyncStatus, OrgRole, Prisma, RepoIndexingStatus, StripeSubscriptionStatus, Org, ApiKey } from "@sourcebot/db"; +import { decrypt, encrypt, generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/crypto"; +import { ApiKey, ConnectionSyncStatus, Org, OrgRole, Prisma, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db"; +import { createLogger } from "@sourcebot/logger"; +import { azuredevopsSchema } from "@sourcebot/schemas/v3/azuredevops.schema"; +import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema"; import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type"; +import { genericGitHostSchema } from "@sourcebot/schemas/v3/genericGitHost.schema"; import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema"; import { giteaSchema } from "@sourcebot/schemas/v3/gitea.schema"; -import { githubSchema } from "@sourcebot/schemas/v3/github.schema"; -import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema"; -import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type"; -import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"; import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type"; +import { githubSchema } from "@sourcebot/schemas/v3/github.schema"; +import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type"; +import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema"; +import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"; +import { getPlan, hasEntitlement } from "@sourcebot/shared"; import Ajv from "ajv"; import { StatusCodes } from "http-status-codes"; import { cookies, headers } from "next/headers"; import { createTransport } from "nodemailer"; +import { Octokit } from "octokit"; import { auth } from "./auth"; import { getConnection } from "./data/connection"; +import { getOrgFromDomain } from "./data/org"; +import { decrementOrgSeatCount, getSubscriptionForOrg } from "./ee/features/billing/serverUtils"; import { IS_BILLING_ENABLED } from "./ee/features/billing/stripe"; import InviteUserEmail from "./emails/inviteUserEmail"; -import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SINGLE_TENANT_ORG_DOMAIN, SOURCEBOT_GUEST_USER_ID, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants"; -import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas"; -import { TenancyMode, ApiKeyPayload } from "./lib/types"; -import { decrementOrgSeatCount, getSubscriptionForOrg } from "./ee/features/billing/serverUtils"; -import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema"; -import { genericGitHostSchema } from "@sourcebot/schemas/v3/genericGitHost.schema"; -import { getPlan, hasEntitlement } from "@sourcebot/shared"; -import JoinRequestSubmittedEmail from "./emails/joinRequestSubmittedEmail"; import JoinRequestApprovedEmail from "./emails/joinRequestApprovedEmail"; -import { createLogger } from "@sourcebot/logger"; -import { getAuditService } from "@/ee/features/audit/factory"; -import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils"; -import { getOrgMetadata } from "@/lib/utils"; -import { getOrgFromDomain } from "./data/org"; +import JoinRequestSubmittedEmail from "./emails/joinRequestSubmittedEmail"; +import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SEARCH_MODE_COOKIE_NAME, SINGLE_TENANT_ORG_DOMAIN, SOURCEBOT_GUEST_USER_ID, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants"; +import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas"; +import { ApiKeyPayload, TenancyMode } from "./lib/types"; +import { withAuthV2, withOptionalAuthV2 } from "./withAuthV2"; const ajv = new Ajv({ validateFormats: false, @@ -59,6 +61,11 @@ export const sew = async (fn: () => Promise): Promise => } catch (e) { Sentry.captureException(e); logger.error(e); + + if (e instanceof Error) { + return unexpectedError(e.message); + } + return unexpectedError(`An unexpected error occurred. Please try again later.`); } } @@ -180,7 +187,7 @@ export const withTenancyModeEnforcement = async(mode: TenancyMode, fn: () => ////// Actions /////// -export const createOrg = (name: string, domain: string): Promise<{ id: number } | ServiceError> => sew(() => +export const createOrg = async (name: string, domain: string): Promise<{ id: number } | ServiceError> => sew(() => withTenancyModeEnforcement('multi', () => withAuth(async (userId) => { const org = await prisma.org.create({ @@ -287,7 +294,7 @@ export const completeOnboarding = async (domain: string): Promise<{ success: boo }) )); -export const getSecrets = (domain: string): Promise<{ createdAt: Date; key: string; }[] | ServiceError> => sew(() => +export const getSecrets = async (domain: string): Promise<{ createdAt: Date; key: string; }[] | ServiceError> => sew(() => withAuth((userId) => withOrgMembership(userId, domain, async ({ org }) => { const secrets = await prisma.secret.findMany({ @@ -631,111 +638,96 @@ export const getConnectionInfo = async (connectionId: number, domain: string) => } }))); -export const getRepos = async (domain: string, filter: { status?: RepoIndexingStatus[], connectionId?: number } = {}) => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { - const repos = await prisma.repo.findMany({ - where: { - orgId: org.id, - ...(filter.status ? { - repoIndexingStatus: { in: filter.status } - } : {}), - ...(filter.connectionId ? { - connections: { - some: { - connectionId: filter.connectionId - } - } - } : {}), - }, - include: { +export const getRepos = async (filter: { status?: RepoIndexingStatus[], connectionId?: number } = {}) => sew(() => + withOptionalAuthV2(async ({ org, prisma }) => { + const repos = await prisma.repo.findMany({ + where: { + orgId: org.id, + ...(filter.status ? { + repoIndexingStatus: { in: filter.status } + } : {}), + ...(filter.connectionId ? { connections: { - include: { - connection: true, + some: { + connectionId: filter.connectionId } } - } - }); - - return repos.map((repo) => repositoryQuerySchema.parse({ - codeHostType: repo.external_codeHostType, - repoId: repo.id, - repoName: repo.name, - repoDisplayName: repo.displayName ?? undefined, - repoCloneUrl: repo.cloneUrl, - webUrl: repo.webUrl ?? undefined, - linkedConnections: repo.connections.map(({ connection }) => ({ - id: connection.id, - name: connection.name, - })), - imageUrl: repo.imageUrl ?? undefined, - indexedAt: repo.indexedAt ?? undefined, - repoIndexingStatus: repo.repoIndexingStatus, - })); - }, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true - )); - -export const getRepoInfoByName = async (repoName: string, domain: string) => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { - // @note: repo names are represented by their remote url - // on the code host. E.g.,: - // - github.com/sourcebot-dev/sourcebot - // - gitlab.com/gitlab-org/gitlab - // - gerrit.wikimedia.org/r/mediawiki/extensions/OnionsPorFavor - // etc. - // - // For most purposes, repo names are unique within an org, so using - // findFirst is equivalent to findUnique. Duplicates _can_ occur when - // a repository is specified by its remote url in a generic `git` - // connection. For example: - // - // ```json - // { - // "connections": { - // "connection-1": { - // "type": "github", - // "repos": [ - // "sourcebot-dev/sourcebot" - // ] - // }, - // "connection-2": { - // "type": "git", - // "url": "file:///tmp/repos/sourcebot" - // } - // } - // } - // ``` - // - // In this scenario, both repos will be named "github.com/sourcebot-dev/sourcebot". - // We will leave this as an edge case for now since it's unlikely to happen in practice. - // - // @v4-todo: we could add a unique constraint on repo name + orgId to help de-duplicate - // these cases. - // @see: repoCompileUtils.ts - const repo = await prisma.repo.findFirst({ - where: { - name: repoName, - orgId: org.id, - }, - }); - - if (!repo) { - return notFound(); + } : {}), } + }); - return { - id: repo.id, - name: repo.name, - displayName: repo.displayName ?? undefined, - codeHostType: repo.external_codeHostType, - webUrl: repo.webUrl ?? undefined, - imageUrl: repo.imageUrl ?? undefined, - indexedAt: repo.indexedAt ?? undefined, - repoIndexingStatus: repo.repoIndexingStatus, - } - }, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true - )); + return repos.map((repo) => repositoryQuerySchema.parse({ + codeHostType: repo.external_codeHostType, + repoId: repo.id, + repoName: repo.name, + repoDisplayName: repo.displayName ?? undefined, + repoCloneUrl: repo.cloneUrl, + webUrl: repo.webUrl ?? undefined, + imageUrl: repo.imageUrl ?? undefined, + indexedAt: repo.indexedAt ?? undefined, + repoIndexingStatus: repo.repoIndexingStatus, + })) + })); + +export const getRepoInfoByName = async (repoName: string) => sew(() => + withOptionalAuthV2(async ({ org, prisma }) => { + // @note: repo names are represented by their remote url + // on the code host. E.g.,: + // - github.com/sourcebot-dev/sourcebot + // - gitlab.com/gitlab-org/gitlab + // - gerrit.wikimedia.org/r/mediawiki/extensions/OnionsPorFavor + // etc. + // + // For most purposes, repo names are unique within an org, so using + // findFirst is equivalent to findUnique. Duplicates _can_ occur when + // a repository is specified by its remote url in a generic `git` + // connection. For example: + // + // ```json + // { + // "connections": { + // "connection-1": { + // "type": "github", + // "repos": [ + // "sourcebot-dev/sourcebot" + // ] + // }, + // "connection-2": { + // "type": "git", + // "url": "file:///tmp/repos/sourcebot" + // } + // } + // } + // ``` + // + // In this scenario, both repos will be named "github.com/sourcebot-dev/sourcebot". + // We will leave this as an edge case for now since it's unlikely to happen in practice. + // + // @v4-todo: we could add a unique constraint on repo name + orgId to help de-duplicate + // these cases. + // @see: repoCompileUtils.ts + const repo = await prisma.repo.findFirst({ + where: { + name: repoName, + orgId: org.id, + }, + }); + + if (!repo) { + return notFound(); + } + + return { + id: repo.id, + name: repo.name, + displayName: repo.displayName ?? undefined, + codeHostType: repo.external_codeHostType, + webUrl: repo.webUrl ?? undefined, + imageUrl: repo.imageUrl ?? undefined, + indexedAt: repo.indexedAt ?? undefined, + repoIndexingStatus: repo.repoIndexingStatus, + } + })); export const createConnection = async (name: string, type: CodeHostType, connectionConfig: string, domain: string): Promise<{ id: number } | ServiceError> => sew(() => withAuth((userId) => @@ -785,6 +777,142 @@ export const createConnection = async (name: string, type: CodeHostType, connect }, OrgRole.OWNER) )); +export const experimental_addGithubRepositoryByUrl = async (repositoryUrl: string): Promise<{ connectionId: number } | ServiceError> => sew(() => + withOptionalAuthV2(async ({ org, prisma }) => { + if (env.EXPERIMENT_SELF_SERVE_REPO_INDEXING_ENABLED !== 'true') { + return { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.INVALID_REQUEST_BODY, + message: "This feature is not enabled.", + } satisfies ServiceError; + } + + // Parse repository URL to extract owner/repo + const repoInfo = (() => { + const url = repositoryUrl.trim(); + + // Handle various GitHub URL formats + const patterns = [ + // https://github.com/owner/repo or https://github.com/owner/repo.git + /^https?:\/\/github\.com\/([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?\/?$/, + // github.com/owner/repo + /^github\.com\/([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?\/?$/, + // owner/repo + /^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/ + ]; + + for (const pattern of patterns) { + const match = url.match(pattern); + if (match) { + return { + owner: match[1], + repo: match[2] + }; + } + } + + return null; + })(); + + if (!repoInfo) { + return { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.INVALID_REQUEST_BODY, + message: "Invalid repository URL format. Please use 'owner/repo' or 'https://github.com/owner/repo' format.", + } satisfies ServiceError; + } + + const { owner, repo } = repoInfo; + + // Use GitHub API to fetch repository information and get the external_id + const octokit = new Octokit({ + auth: env.EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN + }); + + let githubRepo; + try { + const response = await octokit.rest.repos.get({ + owner, + repo, + }); + githubRepo = response.data; + } catch (error) { + if (isHttpError(error, 404)) { + return { + statusCode: StatusCodes.NOT_FOUND, + errorCode: ErrorCode.INVALID_REQUEST_BODY, + message: `Repository '${owner}/${repo}' not found or is private. Only public repositories can be added.`, + } satisfies ServiceError; + } + + if (isHttpError(error, 403)) { + return { + statusCode: StatusCodes.FORBIDDEN, + errorCode: ErrorCode.INVALID_REQUEST_BODY, + message: `Access to repository '${owner}/${repo}' is forbidden. Only public repositories can be added.`, + } satisfies ServiceError; + } + + return { + statusCode: StatusCodes.INTERNAL_SERVER_ERROR, + errorCode: ErrorCode.INVALID_REQUEST_BODY, + message: `Failed to fetch repository information: ${error instanceof Error ? error.message : 'Unknown error'}`, + } satisfies ServiceError; + } + + if (githubRepo.private) { + return { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.INVALID_REQUEST_BODY, + message: "Only public repositories can be added.", + } satisfies ServiceError; + } + + // Check if this repository is already connected using the external_id + const existingRepo = await prisma.repo.findFirst({ + where: { + orgId: org.id, + external_id: githubRepo.id.toString(), + external_codeHostType: 'github', + external_codeHostUrl: 'https://github.com', + } + }); + + if (existingRepo) { + return { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.CONNECTION_ALREADY_EXISTS, + message: "This repository already exists.", + } satisfies ServiceError; + } + + const connectionName = `${owner}-${repo}-${Date.now()}`; + + // Create GitHub connection config + const connectionConfig: GithubConnectionConfig = { + type: "github" as const, + repos: [`${owner}/${repo}`], + ...(env.EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN ? { + token: { + env: 'EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN' + } + } : {}) + }; + + const connection = await prisma.connection.create({ + data: { + orgId: org.id, + name: connectionName, + config: connectionConfig as unknown as Prisma.InputJsonValue, + connectionType: 'github', + } + }); + + return { + connectionId: connection.id, + } + })); + export const updateConnectionDisplayName = async (connectionId: number, name: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() => withAuth((userId) => withOrgMembership(userId, domain, async ({ org }) => { @@ -889,24 +1017,22 @@ export const flagConnectionForSync = async (connectionId: number, domain: string }) )); -export const flagReposForIndex = async (repoIds: number[], domain: string) => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { - await prisma.repo.updateMany({ - where: { - id: { in: repoIds }, - orgId: org.id, - }, - data: { - repoIndexingStatus: RepoIndexingStatus.NEW, - } - }); - - return { - success: true, +export const flagReposForIndex = async (repoIds: number[]) => sew(() => + withAuthV2(async ({ org, prisma }) => { + await prisma.repo.updateMany({ + where: { + id: { in: repoIds }, + orgId: org.id, + }, + data: { + repoIndexingStatus: RepoIndexingStatus.NEW, } - }) - )); + }); + + return { + success: true, + } + })); export const deleteConnection = async (connectionId: number, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() => withAuth((userId) => @@ -1846,7 +1972,7 @@ export const rejectAccountRequest = async (requestId: string, domain: string) => )); export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => { - await cookies().set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true'); + await (await cookies()).set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true'); return true; }); @@ -1857,84 +1983,87 @@ export const getSearchContexts = async (domain: string) => sew(() => where: { orgId: org.id, }, + include: { + repos: true, + }, }); return searchContexts.map((context) => ({ + id: context.id, name: context.name, description: context.description ?? undefined, + repoNames: context.repos.map((repo) => repo.name), })); }, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true )); -export const getRepoImage = async (repoId: number, domain: string): Promise => sew(async () => { - return await withAuth(async (userId) => { - return await withOrgMembership(userId, domain, async ({ org }) => { - const repo = await prisma.repo.findUnique({ - where: { - id: repoId, - orgId: org.id, - }, - include: { - connections: { - include: { - connection: true, - } +export const getRepoImage = async (repoId: number): Promise => sew(async () => { + return await withOptionalAuthV2(async ({ org, prisma }) => { + const repo = await prisma.repo.findUnique({ + where: { + id: repoId, + orgId: org.id, + }, + include: { + connections: { + include: { + connection: true, } } + }, + }); + + if (!repo || !repo.imageUrl) { + return notFound(); + } + + const authHeaders: Record = {}; + for (const { connection } of repo.connections) { + try { + if (connection.connectionType === 'github') { + const config = connection.config as unknown as GithubConnectionConfig; + if (config.token) { + const token = await getTokenFromConfig(config.token, connection.orgId, prisma); + authHeaders['Authorization'] = `token ${token}`; + break; + } + } else if (connection.connectionType === 'gitlab') { + const config = connection.config as unknown as GitlabConnectionConfig; + if (config.token) { + const token = await getTokenFromConfig(config.token, connection.orgId, prisma); + authHeaders['PRIVATE-TOKEN'] = token; + break; + } + } else if (connection.connectionType === 'gitea') { + const config = connection.config as unknown as GiteaConnectionConfig; + if (config.token) { + const token = await getTokenFromConfig(config.token, connection.orgId, prisma); + authHeaders['Authorization'] = `token ${token}`; + break; + } + } + } catch (error) { + logger.warn(`Failed to get token for connection ${connection.id}:`, error); + } + } + + try { + const response = await fetch(repo.imageUrl, { + headers: authHeaders, }); - if (!repo || !repo.imageUrl) { + if (!response.ok) { + logger.warn(`Failed to fetch image from ${repo.imageUrl}: ${response.status}`); return notFound(); } - const authHeaders: Record = {}; - for (const { connection } of repo.connections) { - try { - if (connection.connectionType === 'github') { - const config = connection.config as unknown as GithubConnectionConfig; - if (config.token) { - const token = await getTokenFromConfig(config.token, connection.orgId, prisma); - authHeaders['Authorization'] = `token ${token}`; - break; - } - } else if (connection.connectionType === 'gitlab') { - const config = connection.config as unknown as GitlabConnectionConfig; - if (config.token) { - const token = await getTokenFromConfig(config.token, connection.orgId, prisma); - authHeaders['PRIVATE-TOKEN'] = token; - break; - } - } else if (connection.connectionType === 'gitea') { - const config = connection.config as unknown as GiteaConnectionConfig; - if (config.token) { - const token = await getTokenFromConfig(config.token, connection.orgId, prisma); - authHeaders['Authorization'] = `token ${token}`; - break; - } - } - } catch (error) { - logger.warn(`Failed to get token for connection ${connection.id}:`, error); - } - } - - try { - const response = await fetch(repo.imageUrl, { - headers: authHeaders, - }); - - if (!response.ok) { - logger.warn(`Failed to fetch image from ${repo.imageUrl}: ${response.status}`); - return notFound(); - } - - const imageBuffer = await response.arrayBuffer(); - return imageBuffer; - } catch (error) { - logger.error(`Error proxying image for repo ${repoId}:`, error); - return notFound(); - } - }, /* minRequiredRole = */ OrgRole.GUEST); - }, /* allowAnonymousAccess = */ true); + const imageBuffer = await response.arrayBuffer(); + return imageBuffer; + } catch (error) { + logger.error(`Error proxying image for repo ${repoId}:`, error); + return notFound(); + } + }) }); export const getAnonymousAccessStatus = async (domain: string): Promise => sew(async () => { @@ -1998,6 +2127,20 @@ export const setAnonymousAccessStatus = async (domain: string, enabled: boolean) }); }); +export async function setSearchModeCookie(searchMode: "precise" | "agentic") { + const cookieStore = await cookies(); + cookieStore.set(SEARCH_MODE_COOKIE_NAME, searchMode, { + httpOnly: false, // Allow client-side access + }); +} + +export async function setAgenticSearchTutorialDismissedCookie(dismissed: boolean) { + const cookieStore = await cookies(); + cookieStore.set(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, dismissed ? "true" : "false", { + httpOnly: false, // Allow client-side access + }); +} + ////// Helpers /////// const parseConnectionConfig = (config: string) => { @@ -2025,6 +2168,8 @@ const parseConnectionConfig = (config: string) => { return gerritSchema; case 'bitbucket': return bitbucketSchema; + case 'azuredevops': + return azuredevopsSchema; case 'git': return genericGitHostSchema; } @@ -2059,7 +2204,8 @@ const parseConnectionConfig = (config: string) => { switch (connectionType) { case "gitea": case "github": - case "bitbucket": { + case "bitbucket": + case "azuredevops": { return { numRepos: parsedConfig.repos?.length, hasToken: !!parsedConfig.token, diff --git a/packages/web/src/app/[domain]/agents/page.tsx b/packages/web/src/app/[domain]/agents/page.tsx index 3bbccd0c..03b2a2d6 100644 --- a/packages/web/src/app/[domain]/agents/page.tsx +++ b/packages/web/src/app/[domain]/agents/page.tsx @@ -13,7 +13,13 @@ const agents = [ }, ]; -export default function AgentsPage({ params: { domain } }: { params: { domain: string } }) { +export default async function AgentsPage(props: { params: Promise<{ domain: string }> }) { + const params = await props.params; + + const { + domain + } = params; + return (
diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx index c25f9a33..01a84447 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx @@ -10,17 +10,16 @@ interface CodePreviewPanelProps { path: string; repoName: string; revisionName?: string; - domain: string; } -export const CodePreviewPanel = async ({ path, repoName, revisionName, domain }: CodePreviewPanelProps) => { +export const CodePreviewPanel = async ({ path, repoName, revisionName }: CodePreviewPanelProps) => { const [fileSourceResponse, repoInfoResponse] = await Promise.all([ getFileSource({ fileName: path, repository: repoName, branch: revisionName, - }, domain), - getRepoInfoByName(repoName, domain), + }), + getRepoInfoByName(repoName), ]); if (isServiceError(fileSourceResponse) || isServiceError(repoInfoResponse)) { @@ -34,6 +33,11 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, domain }: webUrl: repoInfoResponse.webUrl, }); + // @todo: this is a hack to support linking to files for ADO. ADO doesn't support web urls with HEAD so we replace it with main. THis + // will break if the default branch is not main. + const fileWebUrl = repoInfoResponse.codeHostType === "azuredevops" && fileSourceResponse.webUrl ? + fileSourceResponse.webUrl.replace("version=GBHEAD", "version=GBmain") : fileSourceResponse.webUrl; + return ( <>
@@ -45,10 +49,13 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, domain }: displayName: repoInfoResponse.displayName, webUrl: repoInfoResponse.webUrl, }} + branchDisplayName={revisionName} /> - {(fileSourceResponse.webUrl && codeHostInfo) && ( + + {(fileWebUrl && codeHostInfo) && ( + { - captureEvent('wa_browse_find_references_pressed', {}); + captureEvent('wa_find_references_pressed', { + source: 'browse', + }); createAuditAction({ action: "user.performed_find_references", metadata: { @@ -160,7 +162,9 @@ export const PureCodePreviewPanel = ({ // If we resolve multiple matches, instead of navigating to the first match, we should // instead popup the bottom sheet with the list of matches. const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => { - captureEvent('wa_browse_goto_definition_pressed', {}); + captureEvent('wa_goto_definition_pressed', { + source: 'browse', + }); createAuditAction({ action: "user.performed_goto_definition", metadata: { diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx index 17860185..cdb8f3ca 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx @@ -1,11 +1,12 @@ 'use client'; -import { useCallback, useRef } from "react"; +import { useRef } from "react"; import { FileTreeItem } from "@/features/fileTree/actions"; import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent"; -import { useBrowseNavigation } from "../../hooks/useBrowseNavigation"; +import { getBrowsePath } from "../../hooks/useBrowseNavigation"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useBrowseParams } from "../../hooks/useBrowseParams"; +import { useDomain } from "@/hooks/useDomain"; interface PureTreePreviewPanelProps { items: FileTreeItem[]; @@ -13,18 +14,9 @@ interface PureTreePreviewPanelProps { export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => { const { repoName, revisionName } = useBrowseParams(); - const { navigateToPath } = useBrowseNavigation(); const scrollAreaRef = useRef(null); - - const onNodeClicked = useCallback((node: FileTreeItem) => { - navigateToPath({ - repoName: repoName, - revisionName: revisionName, - path: node.path, - pathType: node.type === 'tree' ? 'tree' : 'blob', - }); - }, [navigateToPath, repoName, revisionName]); - + const domain = useDomain(); + return ( { isActive={false} depth={0} isCollapseChevronVisible={false} - onClick={() => onNodeClicked(item)} parentRef={scrollAreaRef} + href={getBrowsePath({ + repoName, + revisionName, + path: item.path, + pathType: item.type === 'tree' ? 'tree' : 'blob', + domain, + })} /> ))} diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/rangeHighlightingExtension.ts b/packages/web/src/app/[domain]/browse/[...path]/components/rangeHighlightingExtension.ts index 7f1076c3..cb0fe977 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/rangeHighlightingExtension.ts +++ b/packages/web/src/app/[domain]/browse/[...path]/components/rangeHighlightingExtension.ts @@ -9,7 +9,7 @@ const markDecoration = Decoration.mark({ }); const lineDecoration = Decoration.line({ - attributes: { class: "lineHighlight" }, + attributes: { class: "cm-range-border-radius lineHighlight" }, }); export const rangeHighlightingExtension = (range: BrowseHighlightRange) => StateField.define({ diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx index f2484507..4a0c3857 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx @@ -10,17 +10,16 @@ interface TreePreviewPanelProps { path: string; repoName: string; revisionName?: string; - domain: string; } -export const TreePreviewPanel = async ({ path, repoName, revisionName, domain }: TreePreviewPanelProps) => { +export const TreePreviewPanel = async ({ path, repoName, revisionName }: TreePreviewPanelProps) => { const [repoInfoResponse, folderContentsResponse] = await Promise.all([ - getRepoInfoByName(repoName, domain), + getRepoInfoByName(repoName), getFolderContents({ repoName, revisionName: revisionName ?? 'HEAD', path, - }, domain) + }) ]); if (isServiceError(folderContentsResponse) || isServiceError(repoInfoResponse)) { @@ -39,6 +38,8 @@ export const TreePreviewPanel = async ({ path, repoName, revisionName, domain }: webUrl: repoInfoResponse.webUrl, }} pathType="tree" + isFileIconVisible={false} + branchDisplayName={revisionName} />
diff --git a/packages/web/src/app/[domain]/browse/[...path]/page.tsx b/packages/web/src/app/[domain]/browse/[...path]/page.tsx index 12689f86..84c87912 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/page.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/page.tsx @@ -5,14 +5,19 @@ import { Loader2 } from "lucide-react"; import { TreePreviewPanel } from "./components/treePreviewPanel"; interface BrowsePageProps { - params: { + params: Promise<{ path: string[]; - domain: string; - }; + }>; } -export default async function BrowsePage({ params: { path: _rawPath, domain } }: BrowsePageProps) { - const rawPath = decodeURIComponent(_rawPath.join('/')); +export default async function BrowsePage(props: BrowsePageProps) { + const params = await props.params; + + const { + path: _rawPath, + } = params; + + const rawPath = _rawPath.join('/'); const { repoName, revisionName, path, pathType } = getBrowseParamsFromPathParam(rawPath); return ( @@ -28,14 +33,12 @@ export default async function BrowsePage({ params: { path: _rawPath, domain } }: path={path} repoName={repoName} revisionName={revisionName} - domain={domain} /> ) : ( )} diff --git a/packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx b/packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx index d87eab85..0cfe720a 100644 --- a/packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx +++ b/packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx @@ -6,7 +6,6 @@ import { useHotkeys } from "react-hotkeys-hook"; import { useQuery } from "@tanstack/react-query"; import { unwrapServiceError } from "@/lib/utils"; import { FileTreeItem, getFiles } from "@/features/fileTree/actions"; -import { useDomain } from "@/hooks/useDomain"; import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"; import { useBrowseNavigation } from "../hooks/useBrowseNavigation"; import { useBrowseState } from "../hooks/useBrowseState"; @@ -28,7 +27,6 @@ type SearchResult = { export const FileSearchCommandDialog = () => { const { repoName, revisionName } = useBrowseParams(); - const domain = useDomain(); const { state: { isFileSearchOpen }, updateBrowseState } = useBrowseState(); const commandListRef = useRef(null); @@ -57,8 +55,8 @@ export const FileSearchCommandDialog = () => { }, [isFileSearchOpen]); const { data: files, isLoading, isError } = useQuery({ - queryKey: ['files', repoName, revisionName, domain], - queryFn: () => unwrapServiceError(getFiles({ repoName, revisionName: revisionName ?? 'HEAD' }, domain)), + queryKey: ['files', repoName, revisionName], + queryFn: () => unwrapServiceError(getFiles({ repoName, revisionName: revisionName ?? 'HEAD' })), enabled: isFileSearchOpen, }); diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts b/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts index 83780153..0d79170e 100644 --- a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts +++ b/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts @@ -1,3 +1,5 @@ +'use client'; + import { useRouter } from "next/navigation"; import { useDomain } from "@/hooks/useDomain"; import { useCallback } from "react"; @@ -13,15 +15,47 @@ export type BrowseHighlightRange = { export const HIGHLIGHT_RANGE_QUERY_PARAM = 'highlightRange'; -interface NavigateToPathOptions { +export interface GetBrowsePathProps { repoName: string; revisionName?: string; path: string; pathType: 'blob' | 'tree'; highlightRange?: BrowseHighlightRange; setBrowseState?: Partial; + domain: string; } +export const getBrowsePath = ({ + repoName, + revisionName = 'HEAD', + path, + pathType, + highlightRange, + setBrowseState, + domain, +}: GetBrowsePathProps) => { + const params = new URLSearchParams(); + + if (highlightRange) { + const { start, end } = highlightRange; + + if ('column' in start && 'column' in end) { + params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber}:${start.column},${end.lineNumber}:${end.column}`); + } else { + params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber},${end.lineNumber}`); + } + } + + if (setBrowseState) { + params.set(SET_BROWSE_STATE_QUERY_PARAM, JSON.stringify(setBrowseState)); + } + + const encodedPath = encodeURIComponent(path); + const browsePath = `/${domain}/browse/${repoName}@${revisionName}/-/${pathType}/${encodedPath}${params.size > 0 ? `?${params.toString()}` : ''}`; + return browsePath; +} + + export const useBrowseNavigation = () => { const router = useRouter(); const domain = useDomain(); @@ -33,24 +67,18 @@ export const useBrowseNavigation = () => { pathType, highlightRange, setBrowseState, - }: NavigateToPathOptions) => { - const params = new URLSearchParams(); + }: Omit) => { + const browsePath = getBrowsePath({ + repoName, + revisionName, + path, + pathType, + highlightRange, + setBrowseState, + domain, + }); - if (highlightRange) { - const { start, end } = highlightRange; - - if ('column' in start && 'column' in end) { - params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber}:${start.column},${end.lineNumber}:${end.column}`); - } else { - params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber},${end.lineNumber}`); - } - } - - if (setBrowseState) { - params.set(SET_BROWSE_STATE_QUERY_PARAM, JSON.stringify(setBrowseState)); - } - - router.push(`/${domain}/browse/${repoName}@${revisionName}/-/${pathType}/${path}?${params.toString()}`); + router.push(browsePath); }, [domain, router]); return { diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts b/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts new file mode 100644 index 00000000..fcf29be8 --- /dev/null +++ b/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts @@ -0,0 +1,32 @@ +'use client'; + +import { useMemo } from "react"; +import { getBrowsePath, GetBrowsePathProps } from "./useBrowseNavigation"; +import { useDomain } from "@/hooks/useDomain"; + +export const useBrowsePath = ({ + repoName, + revisionName, + path, + pathType, + highlightRange, + setBrowseState, +}: Omit) => { + const domain = useDomain(); + + const browsePath = useMemo(() => { + return getBrowsePath({ + repoName, + revisionName, + path, + pathType, + highlightRange, + setBrowseState, + domain, + }); + }, [repoName, revisionName, path, pathType, highlightRange, setBrowseState, domain]); + + return { + path: browsePath, + } +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/browse/hooks/utils.test.ts b/packages/web/src/app/[domain]/browse/hooks/utils.test.ts index bbf048b6..a50214c8 100644 --- a/packages/web/src/app/[domain]/browse/hooks/utils.test.ts +++ b/packages/web/src/app/[domain]/browse/hooks/utils.test.ts @@ -98,6 +98,26 @@ describe('getBrowseParamsFromPathParam', () => { pathType: 'blob', }); }); + + it('should decode paths with percent symbols in path', () => { + const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/-/blob/%25hello%25%2Fworld.c'); + expect(result).toEqual({ + repoName: 'github.com/sourcebot-dev/zoekt', + revisionName: 'HEAD', + path: '%hello%/world.c', + pathType: 'blob', + }); + }); + + it('should decode paths with @ symbol encoded', () => { + const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt%40HEAD/-/blob/file.txt'); + expect(result).toEqual({ + repoName: 'github.com/sourcebot-dev/zoekt', + revisionName: 'HEAD', + path: 'file.txt', + pathType: 'blob', + }); + }) }); describe('different revision formats', () => { diff --git a/packages/web/src/app/[domain]/browse/hooks/utils.ts b/packages/web/src/app/[domain]/browse/hooks/utils.ts index 307dd018..ba3214fb 100644 --- a/packages/web/src/app/[domain]/browse/hooks/utils.ts +++ b/packages/web/src/app/[domain]/browse/hooks/utils.ts @@ -5,7 +5,7 @@ export const getBrowseParamsFromPathParam = (pathParam: string) => { throw new Error(`Invalid browse pathname: "${pathParam}" - expected to contain "/-/(tree|blob)/" pattern`); } - const repoAndRevisionPart = pathParam.substring(0, sentinelIndex); + const repoAndRevisionPart = decodeURIComponent(pathParam.substring(0, sentinelIndex)); const lastAtIndex = repoAndRevisionPart.lastIndexOf('@'); const repoName = lastAtIndex === -1 ? repoAndRevisionPart : repoAndRevisionPart.substring(0, lastAtIndex); diff --git a/packages/web/src/app/[domain]/browse/layout.tsx b/packages/web/src/app/[domain]/browse/layout.tsx index f4c15c66..6807a38f 100644 --- a/packages/web/src/app/[domain]/browse/layout.tsx +++ b/packages/web/src/app/[domain]/browse/layout.tsx @@ -6,33 +6,33 @@ import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle import { BrowseStateProvider } from "./browseStateProvider"; import { FileTreePanel } from "@/features/fileTree/components/fileTreePanel"; import { TopBar } from "@/app/[domain]/components/topBar"; -import { Separator } from '@/components/ui/separator'; import { useBrowseParams } from "./hooks/useBrowseParams"; import { FileSearchCommandDialog } from "./components/fileSearchCommandDialog"; +import { useDomain } from "@/hooks/useDomain"; +import { SearchBar } from "../components/searchBar"; interface LayoutProps { children: React.ReactNode; - params: { - domain: string; - } } export default function Layout({ - children: codePreviewPanel, - params, + children, }: LayoutProps) { const { repoName, revisionName } = useBrowseParams(); + const domain = useDomain(); return (
-
- + - -
+ @@ -53,7 +53,7 @@ export default function Layout({ order={1} id="code-preview-panel" > - {codePreviewPanel} + {children} { + // @note: we are guaranteed to have a chatId because this component will only be + // mounted when on a /chat/[id] route. + const chatId = useChatId()!; + const [inputMessage, setInputMessage] = useState | undefined>(undefined); + const [chatState, setChatState] = useSessionStorage(SET_CHAT_STATE_SESSION_STORAGE_KEY, null); + + // Use the last user's last message to determine what repos and contexts we should select by default. + const lastUserMessage = messages.findLast((message) => message.role === "user"); + const defaultSelectedSearchScopes = lastUserMessage?.metadata?.selectedSearchScopes ?? []; + const [selectedSearchScopes, setSelectedSearchScopes] = useState(defaultSelectedSearchScopes); + + useEffect(() => { + if (!chatState) { + return; + } + + try { + setInputMessage(chatState.inputMessage); + setSelectedSearchScopes(chatState.selectedSearchScopes); + } catch { + console.error('Invalid chat state in session storage'); + } finally { + setChatState(null); + } + + }, [chatState, setChatState]); + + return ( + +
+ +
+
+ ) +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/chat/[id]/page.tsx b/packages/web/src/app/[domain]/chat/[id]/page.tsx new file mode 100644 index 00000000..d37076cd --- /dev/null +++ b/packages/web/src/app/[domain]/chat/[id]/page.tsx @@ -0,0 +1,91 @@ +import { getRepos, getSearchContexts } from '@/actions'; +import { getUserChatHistory, getConfiguredLanguageModelsInfo, getChatInfo } from '@/features/chat/actions'; +import { ServiceErrorException } from '@/lib/serviceError'; +import { isServiceError } from '@/lib/utils'; +import { ChatThreadPanel } from './components/chatThreadPanel'; +import { notFound } from 'next/navigation'; +import { StatusCodes } from 'http-status-codes'; +import { TopBar } from '../../components/topBar'; +import { ChatName } from '../components/chatName'; +import { auth } from '@/auth'; +import { AnimatedResizableHandle } from '@/components/ui/animatedResizableHandle'; +import { ChatSidePanel } from '../components/chatSidePanel'; +import { ResizablePanelGroup } from '@/components/ui/resizable'; + +interface PageProps { + params: Promise<{ + domain: string; + id: string; + }>; +} + +export default async function Page(props: PageProps) { + const params = await props.params; + const languageModels = await getConfiguredLanguageModelsInfo(); + const repos = await getRepos(); + const searchContexts = await getSearchContexts(params.domain); + const chatInfo = await getChatInfo({ chatId: params.id }, params.domain); + const session = await auth(); + const chatHistory = session ? await getUserChatHistory(params.domain) : []; + + if (isServiceError(chatHistory)) { + throw new ServiceErrorException(chatHistory); + } + + if (isServiceError(repos)) { + throw new ServiceErrorException(repos); + } + + if (isServiceError(searchContexts)) { + throw new ServiceErrorException(searchContexts); + } + + if (isServiceError(chatInfo)) { + if (chatInfo.statusCode === StatusCodes.NOT_FOUND) { + return notFound(); + } + + throw new ServiceErrorException(chatInfo); + } + + const { messages, name, visibility, isReadonly } = chatInfo; + + const indexedRepos = repos.filter((repo) => repo.indexedAt !== undefined); + + return ( + <> + +
+ / + +
+
+ + + + + + + ) +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/chat/components/chatName.tsx b/packages/web/src/app/[domain]/chat/components/chatName.tsx new file mode 100644 index 00000000..b305d3d7 --- /dev/null +++ b/packages/web/src/app/[domain]/chat/components/chatName.tsx @@ -0,0 +1,94 @@ +'use client'; + +import { useToast } from "@/components/hooks/use-toast"; +import { Badge } from "@/components/ui/badge"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { updateChatName } from "@/features/chat/actions"; +import { useDomain } from "@/hooks/useDomain"; +import { isServiceError } from "@/lib/utils"; +import { GlobeIcon } from "@radix-ui/react-icons"; +import { ChatVisibility } from "@sourcebot/db"; +import { LockIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useCallback, useState } from "react"; +import { RenameChatDialog } from "./renameChatDialog"; + +interface ChatNameProps { + name: string | null; + visibility: ChatVisibility; + id: string; + isReadonly: boolean; +} + +export const ChatName = ({ name, visibility, id, isReadonly }: ChatNameProps) => { + const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); + const { toast } = useToast(); + const domain = useDomain(); + const router = useRouter(); + + const onRenameChat = useCallback(async (name: string) => { + + const response = await updateChatName({ + chatId: id, + name: name, + }, domain); + + if (isServiceError(response)) { + toast({ + description: `❌ Failed to rename chat. Reason: ${response.message}` + }); + } else { + toast({ + description: `✅ Chat renamed successfully` + }); + router.refresh(); + } + }, [id, domain, toast, router]); + + return ( + <> +
+ + +

{ + setIsRenameDialogOpen(true); + }} + > + {name ?? 'Untitled chat'} +

+
+ + Rename chat + +
+ {visibility && ( + + +
+ + {visibility === ChatVisibility.PUBLIC ? ( + + ) : ( + + )} + {visibility === ChatVisibility.PUBLIC ? (isReadonly ? 'Public (Read-only)' : 'Public') : 'Private'} + +
+
+ + {visibility === ChatVisibility.PUBLIC ? `Anyone with the link can view this chat${!isReadonly ? ' and ask follow-up questions' : ''}.` : 'Only you can view and edit this chat.'} + +
+ )} +
+ + + ) +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/chat/components/chatSidePanel.tsx b/packages/web/src/app/[domain]/chat/components/chatSidePanel.tsx new file mode 100644 index 00000000..16e955f8 --- /dev/null +++ b/packages/web/src/app/[domain]/chat/components/chatSidePanel.tsx @@ -0,0 +1,264 @@ +'use client'; + +import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint"; +import { useToast } from "@/components/hooks/use-toast"; +import { Button } from "@/components/ui/button"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { ResizablePanel } from "@/components/ui/resizable"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { deleteChat, updateChatName } from "@/features/chat/actions"; +import { useDomain } from "@/hooks/useDomain"; +import { cn, isServiceError } from "@/lib/utils"; +import { CirclePlusIcon, EllipsisIcon, PencilIcon, TrashIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useCallback, useRef, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { + GoSidebarCollapse as ExpandIcon, +} from "react-icons/go"; +import { ImperativePanelHandle } from "react-resizable-panels"; +import { useChatId } from "../useChatId"; +import { RenameChatDialog } from "./renameChatDialog"; +import { DeleteChatDialog } from "./deleteChatDialog"; +import Link from "next/link"; + +interface ChatSidePanelProps { + order: number; + chatHistory: { + id: string; + name: string | null; + createdAt: Date; + }[]; + isAuthenticated: boolean; + isCollapsedInitially: boolean; +} + +export const ChatSidePanel = ({ + order, + chatHistory, + isAuthenticated, + isCollapsedInitially, +}: ChatSidePanelProps) => { + const domain = useDomain(); + const [isCollapsed, setIsCollapsed] = useState(isCollapsedInitially); + const sidePanelRef = useRef(null); + const router = useRouter(); + const { toast } = useToast(); + const chatId = useChatId(); + const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); + const [chatIdToRename, setChatIdToRename] = useState(null); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [chatIdToDelete, setChatIdToDelete] = useState(null); + + useHotkeys("mod+b", () => { + if (isCollapsed) { + sidePanelRef.current?.expand(); + } else { + sidePanelRef.current?.collapse(); + } + }, { + enableOnFormTags: true, + enableOnContentEditable: true, + description: "Toggle side panel", + }); + + const onRenameChat = useCallback(async (name: string, chatId: string) => { + if (!chatId) { + return; + } + + const response = await updateChatName({ + chatId, + name: name, + }, domain); + + if (isServiceError(response)) { + toast({ + description: `❌ Failed to rename chat. Reason: ${response.message}` + }); + } else { + toast({ + description: `✅ Chat renamed successfully` + }); + router.refresh(); + } + }, [router, toast, domain]); + + const onDeleteChat = useCallback(async (chatIdToDelete: string) => { + if (!chatIdToDelete) { + return; + } + + const response = await deleteChat({ chatId: chatIdToDelete }, domain); + + if (isServiceError(response)) { + toast({ + description: `❌ Failed to delete chat. Reason: ${response.message}` + }); + } else { + toast({ + description: `✅ Chat deleted successfully` + }); + + // If we just deleted the current chat, navigate to new chat + if (chatIdToDelete === chatId) { + router.push(`/${domain}/chat`); + } + + router.refresh(); + } + }, [chatId, router, toast, domain]); + + return ( + <> + setIsCollapsed(true)} + onExpand={() => setIsCollapsed(false)} + > +
+
+ +
+ +

Recent Chats

+
+ {!isAuthenticated ? ( +
+

+ + Sign in + to access your chat history. +

+
+ ) : chatHistory.length === 0 ? ( +
+

Recent chats will appear here.

+
+ ) : chatHistory.map((chat) => ( +
{ + router.push(`/${domain}/chat/${chat.id}`); + }} + > + {chat.name ?? 'Untitled chat'} + + + + + + { + e.stopPropagation(); + setChatIdToRename(chat.id); + setIsRenameDialogOpen(true); + }} + > + + Rename + + { + e.stopPropagation(); + setChatIdToDelete(chat.id); + setIsDeleteDialogOpen(true); + }} + > + + Delete + + + +
+ ))} +
+
+
+ +
+ {isCollapsed && ( +
+ + + + + + + + Open side panel + + +
+ )} + { + if (chatIdToRename) { + onRenameChat(name, chatIdToRename); + } + }} + currentName={chatHistory?.find((chat) => chat.id === chatIdToRename)?.name ?? ""} + /> + { + if (chatIdToDelete) { + onDeleteChat(chatIdToDelete); + } + }} + /> + + ) +} diff --git a/packages/web/src/app/[domain]/chat/components/deleteChatDialog.tsx b/packages/web/src/app/[domain]/chat/components/deleteChatDialog.tsx new file mode 100644 index 00000000..095f4054 --- /dev/null +++ b/packages/web/src/app/[domain]/chat/components/deleteChatDialog.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { useCallback } from "react"; + +interface DeleteChatDialogProps { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onDelete: () => void; +} + +export const DeleteChatDialog = ({ isOpen, onOpenChange, onDelete }: DeleteChatDialogProps) => { + const handleDelete = useCallback(() => { + onDelete(); + onOpenChange(false); + }, [onDelete, onOpenChange]); + + return ( + + + + Delete chat? + + The chat will be deleted and removed from your chat history. This action cannot be undone. + + + + + + + + + ); +}; + diff --git a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx new file mode 100644 index 00000000..91ae3f96 --- /dev/null +++ b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { ResizablePanel } from "@/components/ui/resizable"; +import { ChatBox } from "@/features/chat/components/chatBox"; +import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar"; +import { CustomSlateEditor } from "@/features/chat/customSlateEditor"; +import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread"; +import { LanguageModelInfo, SearchScope } from "@/features/chat/types"; +import { RepositoryQuery, SearchContextQuery } from "@/lib/types"; +import { useCallback, useState } from "react"; +import { Descendant } from "slate"; +import { useLocalStorage } from "usehooks-ts"; + +interface NewChatPanelProps { + languageModels: LanguageModelInfo[]; + repos: RepositoryQuery[]; + searchContexts: SearchContextQuery[]; + order: number; +} + +export const NewChatPanel = ({ + languageModels, + repos, + searchContexts, + order, +}: NewChatPanelProps) => { + const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage("selectedSearchScopes", [], { initializeWithValue: false }); + const { createNewChatThread, isLoading } = useCreateNewChatThread(); + const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); + + const onSubmit = useCallback((children: Descendant[]) => { + createNewChatThread(children, selectedSearchScopes); + }, [createNewChatThread, selectedSearchScopes]); + + + return ( + +
+

What can I help you understand?

+
+ + +
+ +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/chat/components/renameChatDialog.tsx b/packages/web/src/app/[domain]/chat/components/renameChatDialog.tsx new file mode 100644 index 00000000..e22277e1 --- /dev/null +++ b/packages/web/src/app/[domain]/chat/components/renameChatDialog.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +interface RenameChatDialogProps { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onRename: (name: string) => void; + currentName: string; +} + +export const RenameChatDialog = ({ isOpen, onOpenChange, onRename, currentName }: RenameChatDialogProps) => { + const formSchema = z.object({ + name: z.string().min(1), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + }, + }); + + useEffect(() => { + form.reset({ + name: currentName, + }); + }, [currentName, form]); + + const onSubmit = (data: z.infer) => { + onRename(data.name); + form.reset(); + onOpenChange(false); + } + + return ( + + + + Rename Chat + + {`Rename "${currentName ?? 'untitled chat'}" to a new name.`} + + +
+ { + event.stopPropagation(); + form.handleSubmit(onSubmit)(event); + }} + > + ( + + + + + + + )} + /> + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/chat/layout.tsx b/packages/web/src/app/[domain]/chat/layout.tsx new file mode 100644 index 00000000..e82ea91f --- /dev/null +++ b/packages/web/src/app/[domain]/chat/layout.tsx @@ -0,0 +1,18 @@ +import { NavigationGuardProvider } from 'next-navigation-guard'; + +interface LayoutProps { + children: React.ReactNode; +} + +export default async function Layout({ children }: LayoutProps) { + + return ( + // @note: we use a navigation guard here since we don't support resuming streams yet. + // @see: https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-message-persistence#resuming-ongoing-streams + +
+ {children} +
+
+ ) +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/chat/page.tsx b/packages/web/src/app/[domain]/chat/page.tsx new file mode 100644 index 00000000..8bdddf9e --- /dev/null +++ b/packages/web/src/app/[domain]/chat/page.tsx @@ -0,0 +1,64 @@ +import { getRepos, getSearchContexts } from "@/actions"; +import { getUserChatHistory, getConfiguredLanguageModelsInfo } from "@/features/chat/actions"; +import { ServiceErrorException } from "@/lib/serviceError"; +import { isServiceError } from "@/lib/utils"; +import { NewChatPanel } from "./components/newChatPanel"; +import { TopBar } from "../components/topBar"; +import { ResizablePanelGroup } from "@/components/ui/resizable"; +import { ChatSidePanel } from "./components/chatSidePanel"; +import { auth } from "@/auth"; +import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle"; + +interface PageProps { + params: Promise<{ + domain: string; + }>; +} + +export default async function Page(props: PageProps) { + const params = await props.params; + const languageModels = await getConfiguredLanguageModelsInfo(); + const repos = await getRepos(); + const searchContexts = await getSearchContexts(params.domain); + const session = await auth(); + const chatHistory = session ? await getUserChatHistory(params.domain) : []; + + if (isServiceError(chatHistory)) { + throw new ServiceErrorException(chatHistory); + } + + if (isServiceError(repos)) { + throw new ServiceErrorException(repos); + } + + if (isServiceError(searchContexts)) { + throw new ServiceErrorException(searchContexts); + } + + const indexedRepos = repos.filter((repo) => repo.indexedAt !== undefined); + + return ( + <> + + + + + + + + ) +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/chat/useChatId.ts b/packages/web/src/app/[domain]/chat/useChatId.ts new file mode 100644 index 00000000..b42aa2a4 --- /dev/null +++ b/packages/web/src/app/[domain]/chat/useChatId.ts @@ -0,0 +1,8 @@ +'use client'; + +import { useParams } from "next/navigation"; + +export const useChatId = (): string | undefined => { + const { id: chatId } = useParams<{ id: string }>(); + return chatId; +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/connectionCreationForms/bitbucketCloudConnectionCreationForm.tsx b/packages/web/src/app/[domain]/components/connectionCreationForms/bitbucketCloudConnectionCreationForm.tsx deleted file mode 100644 index 52c762fc..00000000 --- a/packages/web/src/app/[domain]/components/connectionCreationForms/bitbucketCloudConnectionCreationForm.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client'; - -import SharedConnectionCreationForm from "./sharedConnectionCreationForm"; -import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/connection.type"; -import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema"; -import { bitbucketCloudQuickActions } from "../../connections/quickActions"; - -interface BitbucketCloudConnectionCreationFormProps { - onCreated?: (id: number) => void; -} - -const additionalConfigValidation = (config: BitbucketConnectionConfig): { message: string, isValid: boolean } => { - const hasProjects = config.projects && config.projects.length > 0 && config.projects.some(p => p.trim().length > 0); - const hasRepos = config.repos && config.repos.length > 0 && config.repos.some(r => r.trim().length > 0); - const hasWorkspaces = config.workspaces && config.workspaces.length > 0 && config.workspaces.some(w => w.trim().length > 0); - - if (!hasProjects && !hasRepos && !hasWorkspaces) { - return { - message: "At least one project, repository, or workspace must be specified", - isValid: false, - } - } - - return { - message: "Valid", - isValid: true, - } -}; - -export const BitbucketCloudConnectionCreationForm = ({ onCreated }: BitbucketCloudConnectionCreationFormProps) => { - const defaultConfig: BitbucketConnectionConfig = { - type: 'bitbucket', - deploymentType: 'cloud', - } - - return ( - - type="bitbucket-cloud" - title="Create a Bitbucket Cloud connection" - defaultValues={{ - config: JSON.stringify(defaultConfig, null, 2), - }} - schema={bitbucketSchema} - additionalConfigValidation={additionalConfigValidation} - quickActions={bitbucketCloudQuickActions} - onCreated={onCreated} - /> - ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/connectionCreationForms/bitbucketDataCenterConnectionCreationForm.tsx b/packages/web/src/app/[domain]/components/connectionCreationForms/bitbucketDataCenterConnectionCreationForm.tsx deleted file mode 100644 index 5065de00..00000000 --- a/packages/web/src/app/[domain]/components/connectionCreationForms/bitbucketDataCenterConnectionCreationForm.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client'; - -import SharedConnectionCreationForm from "./sharedConnectionCreationForm"; -import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/connection.type"; -import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema"; -import { bitbucketDataCenterQuickActions } from "../../connections/quickActions"; - -interface BitbucketDataCenterConnectionCreationFormProps { - onCreated?: (id: number) => void; -} - -const additionalConfigValidation = (config: BitbucketConnectionConfig): { message: string, isValid: boolean } => { - const hasProjects = config.projects && config.projects.length > 0 && config.projects.some(p => p.trim().length > 0); - const hasRepos = config.repos && config.repos.length > 0 && config.repos.some(r => r.trim().length > 0); - - if (!hasProjects && !hasRepos) { - return { - message: "At least one project or repository must be specified", - isValid: false, - } - } - - return { - message: "Valid", - isValid: true, - } -}; - -export const BitbucketDataCenterConnectionCreationForm = ({ onCreated }: BitbucketDataCenterConnectionCreationFormProps) => { - const defaultConfig: BitbucketConnectionConfig = { - type: 'bitbucket', - deploymentType: 'server', - } - - return ( - - type="bitbucket-server" - title="Create a Bitbucket Data Center connection" - defaultValues={{ - config: JSON.stringify(defaultConfig, null, 2), - }} - schema={bitbucketSchema} - additionalConfigValidation={additionalConfigValidation} - quickActions={bitbucketDataCenterQuickActions} - onCreated={onCreated} - /> - ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/connectionCreationForms/gerritConnectionCreationForm.tsx b/packages/web/src/app/[domain]/components/connectionCreationForms/gerritConnectionCreationForm.tsx deleted file mode 100644 index 64b6b10b..00000000 --- a/packages/web/src/app/[domain]/components/connectionCreationForms/gerritConnectionCreationForm.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; - -import { GerritConnectionConfig } from "@sourcebot/schemas/v3/gerrit.type"; -import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema"; -import { gerritQuickActions } from "../../connections/quickActions"; -import SharedConnectionCreationForm from "./sharedConnectionCreationForm"; - -interface GerritConnectionCreationFormProps { - onCreated?: (id: number) => void; -} - -const additionalConfigValidation = (config: GerritConnectionConfig): { message: string, isValid: boolean } => { - const hasProjects = config.projects && config.projects.length > 0; - - if (!hasProjects) { - return { - message: "At least one project must be specified", - isValid: false, - } - } - - return { - message: "Valid", - isValid: true, - } -} - -export const GerritConnectionCreationForm = ({ onCreated }: GerritConnectionCreationFormProps) => { - const defaultConfig: GerritConnectionConfig = { - type: 'gerrit', - url: "https://gerrit.example.com" - } - - return ( - - type="gerrit" - title="Create a Gerrit connection" - defaultValues={{ - config: JSON.stringify(defaultConfig, null, 2), - }} - schema={gerritSchema} - quickActions={gerritQuickActions} - additionalConfigValidation={additionalConfigValidation} - onCreated={onCreated} - /> - ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/connectionCreationForms/giteaConnectionCreationForm.tsx b/packages/web/src/app/[domain]/components/connectionCreationForms/giteaConnectionCreationForm.tsx deleted file mode 100644 index f19c441c..00000000 --- a/packages/web/src/app/[domain]/components/connectionCreationForms/giteaConnectionCreationForm.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client'; - -import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type"; -import { giteaSchema } from "@sourcebot/schemas/v3/gitea.schema"; -import { giteaQuickActions } from "../../connections/quickActions"; -import SharedConnectionCreationForm from "./sharedConnectionCreationForm"; - -interface GiteaConnectionCreationFormProps { - onCreated?: (id: number) => void; -} - -const additionalConfigValidation = (config: GiteaConnectionConfig): { message: string, isValid: boolean } => { - const hasOrgs = config.orgs && config.orgs.length > 0 && config.orgs.some(o => o.trim().length > 0); - const hasUsers = config.users && config.users.length > 0 && config.users.some(u => u.trim().length > 0); - const hasRepos = config.repos && config.repos.length > 0 && config.repos.some(r => r.trim().length > 0); - - if (!hasOrgs && !hasUsers && !hasRepos) { - return { - message: "At least one organization, user, or repository must be specified", - isValid: false, - } - } - - return { - message: "Valid", - isValid: true, - } -} - -export const GiteaConnectionCreationForm = ({ onCreated }: GiteaConnectionCreationFormProps) => { - const defaultConfig: GiteaConnectionConfig = { - type: 'gitea', - } - - return ( - - type="gitea" - title="Create a Gitea connection" - defaultValues={{ - config: JSON.stringify(defaultConfig, null, 2), - }} - schema={giteaSchema} - quickActions={giteaQuickActions} - additionalConfigValidation={additionalConfigValidation} - onCreated={onCreated} - /> - ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/connectionCreationForms/githubConnectionCreationForm.tsx b/packages/web/src/app/[domain]/components/connectionCreationForms/githubConnectionCreationForm.tsx deleted file mode 100644 index 80446ab4..00000000 --- a/packages/web/src/app/[domain]/components/connectionCreationForms/githubConnectionCreationForm.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client'; - -import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type"; -import { githubSchema } from "@sourcebot/schemas/v3/github.schema"; -import { githubQuickActions } from "../../connections/quickActions"; -import SharedConnectionCreationForm from "./sharedConnectionCreationForm"; - -interface GitHubConnectionCreationFormProps { - onCreated?: (id: number) => void; -} - -const additionalConfigValidation = (config: GithubConnectionConfig): { message: string, isValid: boolean } => { - const hasRepos = config.repos && config.repos.length > 0 && config.repos.some(r => r.trim().length > 0); - const hasOrgs = config.orgs && config.orgs.length > 0 && config.orgs.some(o => o.trim().length > 0); - const hasUsers = config.users && config.users.length > 0 && config.users.some(u => u.trim().length > 0); - - if (!hasRepos && !hasOrgs && !hasUsers) { - return { - message: "At least one repository, organization, or user must be specified", - isValid: false, - } - } - - return { - message: "Valid", - isValid: true, - } -}; - -export const GitHubConnectionCreationForm = ({ onCreated }: GitHubConnectionCreationFormProps) => { - const defaultConfig: GithubConnectionConfig = { - type: 'github', - } - - return ( - - type="github" - title="Create a GitHub connection" - defaultValues={{ - config: JSON.stringify(defaultConfig, null, 2), - }} - schema={githubSchema} - additionalConfigValidation={additionalConfigValidation} - quickActions={githubQuickActions} - onCreated={onCreated} - /> - ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/connectionCreationForms/gitlabConnectionCreationForm.tsx b/packages/web/src/app/[domain]/components/connectionCreationForms/gitlabConnectionCreationForm.tsx deleted file mode 100644 index d21823f6..00000000 --- a/packages/web/src/app/[domain]/components/connectionCreationForms/gitlabConnectionCreationForm.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client'; - -import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"; -import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema"; -import { gitlabQuickActions } from "../../connections/quickActions"; -import SharedConnectionCreationForm from "./sharedConnectionCreationForm"; - -interface GitLabConnectionCreationFormProps { - onCreated?: (id: number) => void; -} - -const additionalConfigValidation = (config: GitlabConnectionConfig): { message: string, isValid: boolean } => { - const hasProjects = config.projects && config.projects.length > 0 && config.projects.some(p => p.trim().length > 0); - const hasUsers = config.users && config.users.length > 0 && config.users.some(u => u.trim().length > 0); - const hasGroups = config.groups && config.groups.length > 0 && config.groups.some(g => g.trim().length > 0); - - if (!hasProjects && !hasUsers && !hasGroups) { - return { - message: "At least one project, user, or group must be specified", - isValid: false, - } - } - - return { - message: "Valid", - isValid: true, - } -} - -export const GitLabConnectionCreationForm = ({ onCreated }: GitLabConnectionCreationFormProps) => { - const defaultConfig: GitlabConnectionConfig = { - type: 'gitlab', - } - - return ( - - type="gitlab" - title="Create a GitLab connection" - defaultValues={{ - config: JSON.stringify(defaultConfig, null, 2), - }} - schema={gitlabSchema} - quickActions={gitlabQuickActions} - additionalConfigValidation={additionalConfigValidation} - onCreated={onCreated} - /> - ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/connectionCreationForms/index.ts b/packages/web/src/app/[domain]/components/connectionCreationForms/index.ts deleted file mode 100644 index db4bcd0f..00000000 --- a/packages/web/src/app/[domain]/components/connectionCreationForms/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { GitHubConnectionCreationForm } from "./githubConnectionCreationForm"; -export { GitLabConnectionCreationForm } from "./gitlabConnectionCreationForm"; -export { GiteaConnectionCreationForm } from "./giteaConnectionCreationForm"; -export { GerritConnectionCreationForm } from "./gerritConnectionCreationForm"; -export { BitbucketCloudConnectionCreationForm } from "./bitbucketCloudConnectionCreationForm"; -export { BitbucketDataCenterConnectionCreationForm } from "./bitbucketDataCenterConnectionCreationForm"; \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/connectionCreationForms/secretCombobox.tsx b/packages/web/src/app/[domain]/components/connectionCreationForms/secretCombobox.tsx deleted file mode 100644 index 6bfe4baf..00000000 --- a/packages/web/src/app/[domain]/components/connectionCreationForms/secretCombobox.tsx +++ /dev/null @@ -1,163 +0,0 @@ -'use client'; - -import { getSecrets } from "@/actions"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { Separator } from "@/components/ui/separator"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { useDomain } from "@/hooks/useDomain"; -import { cn, CodeHostType, isDefined, isServiceError, unwrapServiceError } from "@/lib/utils"; -import { useQuery } from "@tanstack/react-query"; -import { Check, ChevronsUpDown, Loader2, PlusCircleIcon, TriangleAlert } from "lucide-react"; -import { useCallback, useState } from "react"; -import { ImportSecretDialog } from "../importSecretDialog"; - -interface SecretComboBoxProps { - isDisabled: boolean; - codeHostType: CodeHostType; - secretKey?: string; - onSecretChange: (secretKey: string) => void; -} - -export const SecretCombobox = ({ - isDisabled, - codeHostType, - secretKey, - onSecretChange, -}: SecretComboBoxProps) => { - const [searchFilter, setSearchFilter] = useState(""); - const domain = useDomain(); - const [isCreateSecretDialogOpen, setIsCreateSecretDialogOpen] = useState(false); - const captureEvent = useCaptureEvent(); - - const { data: secrets, isPending, isError, refetch } = useQuery({ - queryKey: ["secrets", domain], - queryFn: () => unwrapServiceError(getSecrets(domain)), - }); - - const onSecretCreated = useCallback((key: string) => { - onSecretChange(key); - refetch(); - }, [onSecretChange, refetch]); - - return ( - <> - - - - - - - {isPending ? ( -
- -
- ) : isError ? ( -

Failed to load secrets

- ) : secrets.length > 0 && ( - <> - - setSearchFilter(value)} - /> - - -

No secrets found

-

{`Your search term "${searchFilter}" did not match any secrets.`}

-
- - {secrets.map(({ key }) => ( - { - onSecretChange(key); - }} - > - {key} - - - ))} - -
-
- - - )} - -
-
- - - ) -} diff --git a/packages/web/src/app/[domain]/components/connectionCreationForms/sharedConnectionCreationForm.tsx b/packages/web/src/app/[domain]/components/connectionCreationForms/sharedConnectionCreationForm.tsx deleted file mode 100644 index 859d87da..00000000 --- a/packages/web/src/app/[domain]/components/connectionCreationForms/sharedConnectionCreationForm.tsx +++ /dev/null @@ -1,239 +0,0 @@ - -'use client'; - -import { checkIfSecretExists, createConnection } from "@/actions"; -import { ConnectionIcon } from "@/app/[domain]/connections/components/connectionIcon"; -import { createZodConnectionConfigValidator } from "@/app/[domain]/connections/utils"; -import { useToast } from "@/components/hooks/use-toast"; -import { Button } from "@/components/ui/button"; -import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { CodeHostType, isServiceError, isAuthSupportedForCodeHost } from "@/lib/utils"; -import { cn } from "@/lib/utils"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Schema } from "ajv"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; -import ConfigEditor, { isConfigValidJson, onQuickAction, QuickActionFn } from "../configEditor"; -import { useDomain } from "@/hooks/useDomain"; -import { InfoIcon, Loader2 } from "lucide-react"; -import { ReactCodeMirrorRef } from "@uiw/react-codemirror"; -import { SecretCombobox } from "./secretCombobox"; -import strings from "@/lib/strings"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; - -interface SharedConnectionCreationFormProps { - type: CodeHostType; - defaultValues: { - name?: string; - config: string; - }; - title: string; - schema: Schema; - quickActions?: { - name: string; - fn: QuickActionFn; - }[], - className?: string; - onCreated?: (id: number) => void; - additionalConfigValidation?: (config: T) => { message: string, isValid: boolean }; -} - - -export default function SharedConnectionCreationForm({ - type, - defaultValues, - title, - schema, - quickActions, - className, - onCreated, - additionalConfigValidation -}: SharedConnectionCreationFormProps) { - const { toast } = useToast(); - const domain = useDomain(); - const editorRef = useRef(null); - const captureEvent = useCaptureEvent(); - const formSchema = useMemo(() => { - return z.object({ - name: z.string().min(1), - config: createZodConnectionConfigValidator(schema, additionalConfigValidation), - secretKey: z.string().optional().refine(async (secretKey) => { - if (!secretKey) { - return true; - } - - return checkIfSecretExists(secretKey, domain); - }, { message: "Secret not found" }), - }); - }, [schema, domain, additionalConfigValidation]); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: defaultValues, - }); - const { isSubmitting } = form.formState; - - const onSubmit = useCallback(async (data: z.infer) => { - const response = await createConnection(data.name, type, data.config, domain); - if (isServiceError(response)) { - toast({ - description: `❌ Failed to create connection. Reason: ${response.message}` - }); - captureEvent('wa_create_connection_fail', { - type: type, - error: response.message, - }); - } else { - toast({ - description: `✅ Connection created successfully.` - }); - captureEvent('wa_create_connection_success', { - type: type, - }); - onCreated?.(response.id); - } - }, [domain, toast, type, onCreated, captureEvent]); - - const onConfigChange = useCallback((value: string) => { - form.setValue("config", value); - const isValid = isConfigValidJson(value); - setIsSecretsDisabled(!isValid); - if (isValid) { - const configJson = JSON.parse(value); - if (configJson.token?.secret !== undefined) { - form.setValue("secretKey", configJson.token.secret); - } else { - form.setValue("secretKey", undefined); - } - } - }, [form]); - - // Run onConfigChange on mount to set the initial secret key - useEffect(() => { - onConfigChange(defaultValues.config); - }, [defaultValues, onConfigChange]); - - const [isSecretsDisabled, setIsSecretsDisabled] = useState(false); - - return ( -
-
-
- -

{title}

-
- - Connections are used to specify what repositories you want Sourcebot to sync. - -
-
- -
- ( - - Display Name - This is the {`connection's`} display name within Sourcebot. - - - - - - )} - /> - {isAuthSupportedForCodeHost(type) && ( - ( - - Secret (optional) - {strings.createSecretDescription} - - { - const view = editorRef.current?.view; - if (!view) { - return; - } - - onQuickAction( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (previous: any) => { - return { - ...previous, - token: { - secret: secretKey, - } - } - }, - form.getValues("config"), - view, - { - focusEditor: false - } - ); - }} - /> - - - - )} - /> - )} - { - return ( - - Configuration - {strings.connectionConfigDescription} - - - ref={editorRef} - type={type} - value={value} - onChange={onConfigChange} - actions={quickActions ?? []} - schema={schema} - /> - - - - ) - }} - /> -
-
- -
-
- -
- ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/copyIconButton.tsx b/packages/web/src/app/[domain]/components/copyIconButton.tsx new file mode 100644 index 00000000..4c2b0996 --- /dev/null +++ b/packages/web/src/app/[domain]/components/copyIconButton.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { CheckCircle2, Copy } from "lucide-react"; +import { useCallback, useState } from "react"; + +interface CopyIconButtonProps { + onCopy: () => boolean; + className?: string; +} + +export const CopyIconButton = ({ onCopy, className }: CopyIconButtonProps) => { + const [copied, setCopied] = useState(false); + + const onClick = useCallback(() => { + const success = onCopy(); + if (success) { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }, [onCopy]); + + return ( + + ) +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/errorNavIndicator.tsx b/packages/web/src/app/[domain]/components/errorNavIndicator.tsx index 2f024a61..4f7810aa 100644 --- a/packages/web/src/app/[domain]/components/errorNavIndicator.tsx +++ b/packages/web/src/app/[domain]/components/errorNavIndicator.tsx @@ -10,8 +10,8 @@ import { env } from "@/env.mjs"; import { useQuery } from "@tanstack/react-query"; import { ConnectionSyncStatus, RepoIndexingStatus } from "@sourcebot/db"; import { getConnections } from "@/actions"; -import { getRepos } from "@/actions"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { getRepos } from "@/app/api/(client)/client"; export const ErrorNavIndicator = () => { const domain = useDomain(); @@ -19,7 +19,7 @@ export const ErrorNavIndicator = () => { const { data: repos, isPending: isPendingRepos, isError: isErrorRepos } = useQuery({ queryKey: ['repos', domain], - queryFn: () => unwrapServiceError(getRepos(domain)), + queryFn: () => unwrapServiceError(getRepos()), select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.FAILED), refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS, }); @@ -106,24 +106,20 @@ export const ErrorNavIndicator = () => { {repos .slice(0, 10) - .filter(item => item.linkedConnections.length > 0) // edge case: don't show repos that are orphaned and awaiting gc. .map(repo => ( - // Link to the first connection for the repo - captureEvent('wa_error_nav_job_pressed', {})}> -
- - - {repo.repoName} - - - {repo.repoName} - - -
- +
+ + + {repo.repoName} + + + {repo.repoName} + + +
))}
{repos.length > 10 && ( diff --git a/packages/web/src/app/[domain]/components/githubStarToast.tsx b/packages/web/src/app/[domain]/components/githubStarToast.tsx new file mode 100644 index 00000000..2e482ff1 --- /dev/null +++ b/packages/web/src/app/[domain]/components/githubStarToast.tsx @@ -0,0 +1,84 @@ +'use client'; + +import { useToast } from "@/components/hooks/use-toast"; +import { ToastAction } from "@/components/ui/toast"; +import { useEffect } from "react"; +import { GitHubLogoIcon } from "@radix-ui/react-icons"; +import { captureEvent } from "@/hooks/useCaptureEvent"; + +const POPUP_SHOWN_COOKIE = "github_popup_shown"; +const POPUP_START_TIME_COOKIE = "github_popup_start_time"; +const POPUP_DELAY_S = 60; +const SOURCEBOT_GITHUB_URL = "https://github.com/sourcebot-dev/sourcebot"; + +function getCookie(name: string): string | null { + if (typeof document === "undefined") return null; + + const cookies = document.cookie.split(';').map(cookie => cookie.trim()); + const targetCookie = cookies.find(cookie => cookie.startsWith(`${name}=`)); + + if (!targetCookie) return null; + + return targetCookie.substring(`${name}=`.length); +} + +function setCookie(name: string, value: string, days: number = 365) { + if (typeof document === "undefined") return; + + try { + const expires = new Date(); + expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000)); + document.cookie = `${name}=${value}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`; + } catch (error) { + console.warn('Failed to set GitHub popup cookie:', error); + } +} + +export const GitHubStarToast = () => { + const { toast } = useToast(); + + useEffect(() => { + const hasShownPopup = getCookie(POPUP_SHOWN_COOKIE); + const startTime = getCookie(POPUP_START_TIME_COOKIE); + + if (hasShownPopup) { + return; + } + + const currentTime = Date.now(); + if (!startTime) { + setCookie(POPUP_START_TIME_COOKIE, currentTime.toString()); + return; + } + + const elapsed = currentTime - parseInt(startTime, 10); + if (elapsed >= (POPUP_DELAY_S * 1000)) { + toast({ + title: "Star us on GitHub ❤️", + description: "If you've found Sourcebot useful, please consider starring us on GitHub. Your support means a lot!", + duration: 15 * 1000, + action: ( +
+ { + captureEvent('wa_github_star_toast_clicked', {}); + window.open(SOURCEBOT_GITHUB_URL, "_blank"); + }} + > +
+ + Sourcebot +
+
+
+ ) + }); + + captureEvent('wa_github_star_toast_displayed', {}); + setCookie(POPUP_SHOWN_COOKIE, "true"); + } + }, [toast]); + + return null; +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx new file mode 100644 index 00000000..327cd297 --- /dev/null +++ b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx @@ -0,0 +1,107 @@ +'use client'; + +import { Separator } from "@/components/ui/separator"; +import { ChatBox } from "@/features/chat/components/chatBox"; +import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar"; +import { LanguageModelInfo, SearchScope } from "@/features/chat/types"; +import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread"; +import { RepositoryQuery, SearchContextQuery } from "@/lib/types"; +import { useCallback, useState } from "react"; +import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar"; +import { useLocalStorage } from "usehooks-ts"; +import { DemoExamples } from "@/types"; +import { AskSourcebotDemoCards } from "./askSourcebotDemoCards"; +import { AgenticSearchTutorialDialog } from "./agenticSearchTutorialDialog"; +import { setAgenticSearchTutorialDismissedCookie } from "@/actions"; +import { RepositorySnapshot } from "./repositorySnapshot"; + +interface AgenticSearchProps { + searchModeSelectorProps: SearchModeSelectorProps; + languageModels: LanguageModelInfo[]; + repos: RepositoryQuery[]; + searchContexts: SearchContextQuery[]; + chatHistory: { + id: string; + createdAt: Date; + name: string | null; + }[]; + demoExamples: DemoExamples | undefined; + isTutorialDismissed: boolean; +} + +export const AgenticSearch = ({ + searchModeSelectorProps, + languageModels, + repos, + searchContexts, + demoExamples, + isTutorialDismissed, +}: AgenticSearchProps) => { + const { createNewChatThread, isLoading } = useCreateNewChatThread(); + const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage("selectedSearchScopes", [], { initializeWithValue: false }); + const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); + + const [isTutorialOpen, setIsTutorialOpen] = useState(!isTutorialDismissed); + const onTutorialDismissed = useCallback(() => { + setIsTutorialOpen(false); + setAgenticSearchTutorialDismissedCookie(true); + }, []); + + return ( +
+
+ { + createNewChatThread(children, selectedSearchScopes); + }} + className="min-h-[50px]" + isRedirecting={isLoading} + languageModels={languageModels} + selectedSearchScopes={selectedSearchScopes} + searchContexts={searchContexts} + onContextSelectorOpenChanged={setIsContextSelectorOpen} + /> + +
+
+ + +
+
+
+ +
+ +
+ +
+ +
+ + {demoExamples && ( + + )} + + {isTutorialOpen && ( + + )} +
+ ) +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/homepage/agenticSearchTutorialDialog.tsx b/packages/web/src/app/[domain]/components/homepage/agenticSearchTutorialDialog.tsx new file mode 100644 index 00000000..a44d6735 --- /dev/null +++ b/packages/web/src/app/[domain]/components/homepage/agenticSearchTutorialDialog.tsx @@ -0,0 +1,339 @@ +"use client" + +import { Button } from "@/components/ui/button" +import { Dialog, DialogContent } from "@/components/ui/dialog" +import { ModelProviderLogo } from "@/features/chat/components/chatBox/modelProviderLogo" +import { cn } from "@/lib/utils" +import mentionsDemo from "@/public/ask_sb_tutorial_at_mentions.png" +import citationsDemo from "@/public/ask_sb_tutorial_citations.png" +import searchScopeDemo from "@/public/ask_sb_tutorial_search_scope.png" +import logoDarkSmall from "@/public/sb_logo_dark_small.png" +import { useQuery } from "@tanstack/react-query" +import { + ArrowLeftRightIcon, + AtSignIcon, + BookMarkedIcon, + BookTextIcon, + ChevronLeft, + ChevronRight, + CircleCheckIcon, + FileIcon, + FolderIcon, + GitCommitHorizontalIcon, + LibraryBigIcon, + ScanSearchIcon, + StarIcon, + TicketIcon, +} from "lucide-react" +import Image from "next/image" +import Link from "next/link" +import { useState } from "react" + +interface AgenticSearchTutorialDialogProps { + onClose: () => void +} + + +// Star button component that fetches GitHub star count +const GitHubStarButton = () => { + const { data: starCount, isLoading, isError } = useQuery({ + queryKey: ['github-stars', 'sourcebot-dev/sourcebot'], + queryFn: async () => { + const response = await fetch('https://api.github.com/repos/sourcebot-dev/sourcebot') + if (!response.ok) { + throw new Error('Failed to fetch star count') + } + const data = await response.json() + return data.stargazers_count as number; + }, + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 30 * 60 * 1000, // 30 minutes + retry: 3, + retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), + }) + + const formatStarCount = (count: number) => { + if (count >= 1000) { + return `${(count / 1000).toFixed(1)}k` + } + return count.toString() + } + + return ( + + ) +} + + +const tutorialSteps = [ + { + leftContent: ( +
+
+

+ Ask Sourcebot. +

+

+ Ask questions about your entire codebase in natural language. + Get back responses grounded in code with inline citations. +

+

+ Ask Sourcebot is an agentic search tool that can answer questions about your codebase by searching, reading files, navigating references, and more. Supports any compatible LLM. +

+
+
+
+ + + + + + + + + +
+
+
+ ), + rightContent: ( +
) } diff --git a/packages/web/src/app/[domain]/components/homepage/toolbar.tsx b/packages/web/src/app/[domain]/components/homepage/toolbar.tsx new file mode 100644 index 00000000..cc9c65e0 --- /dev/null +++ b/packages/web/src/app/[domain]/components/homepage/toolbar.tsx @@ -0,0 +1,157 @@ +'use client'; + +import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { MessageCircleIcon, SearchIcon, TriangleAlert } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; + +export type SearchMode = "precise" | "agentic"; + +const PRECISE_SEARCH_DOCS_URL = "https://docs.sourcebot.dev/docs/features/search/overview"; +// @tood: point this to the actual docs page +const AGENTIC_SEARCH_DOCS_URL = "https://docs.sourcebot.dev/docs/features/ask/overview"; + +export interface SearchModeSelectorProps { + searchMode: SearchMode; + isAgenticSearchEnabled: boolean; + onSearchModeChange: (searchMode: SearchMode) => void; + className?: string; +} + +export const SearchModeSelector = ({ + searchMode, + isAgenticSearchEnabled, + onSearchModeChange, + className, +}: SearchModeSelectorProps) => { + const [focusedSearchMode, setFocusedSearchMode] = useState(searchMode); + + return ( +
+ +
+ ) +} + + diff --git a/packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx b/packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx index 7791d83f..bb5912ea 100644 --- a/packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx +++ b/packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx @@ -153,7 +153,6 @@ export const LightweightCodeHighlighter = memo((prop )} - - - Search - - + + Search + - - - Repositories - - + + Repositories + {isAuthenticated && ( <> {env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === undefined && ( - - - Agents - - + + Agents + )} - - - Connections - - + + Connections + - - - Settings - - + + Settings + )} diff --git a/packages/web/src/app/[domain]/components/pathHeader.tsx b/packages/web/src/app/[domain]/components/pathHeader.tsx index 5118392f..18806937 100644 --- a/packages/web/src/app/[domain]/components/pathHeader.tsx +++ b/packages/web/src/app/[domain]/components/pathHeader.tsx @@ -1,11 +1,10 @@ 'use client'; -import { getCodeHostInfoForRepo } from "@/lib/utils"; +import { cn, getCodeHostInfoForRepo } from "@/lib/utils"; import { LaptopIcon } from "@radix-ui/react-icons"; -import clsx from "clsx"; import Image from "next/image"; -import { useBrowseNavigation } from "../browse/hooks/useBrowseNavigation"; -import { Copy, CheckCircle2, ChevronRight, MoreHorizontal } from "lucide-react"; +import { getBrowsePath } from "../browse/hooks/useBrowseNavigation"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; import { useCallback, useState, useMemo, useRef, useEffect } from "react"; import { useToast } from "@/components/hooks/use-toast"; import { @@ -14,6 +13,10 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; +import { CopyIconButton } from "./copyIconButton"; +import Link from "next/link"; +import { useDomain } from "@/hooks/useDomain"; interface FileHeaderProps { path: string; @@ -30,6 +33,9 @@ interface FileHeaderProps { }, branchDisplayName?: string; branchDisplayTitle?: string; + isCodeHostIconVisible?: boolean; + isFileIconVisible?: boolean; + repoNameClassName?: string; } interface BreadcrumbSegment { @@ -49,6 +55,9 @@ export const PathHeader = ({ branchDisplayName, branchDisplayTitle, pathType = 'blob', + isCodeHostIconVisible = true, + isFileIconVisible = true, + repoNameClassName, }: FileHeaderProps) => { const info = getCodeHostInfoForRepo({ name: repo.name, @@ -57,30 +66,28 @@ export const PathHeader = ({ webUrl: repo.webUrl, }); - const { navigateToPath } = useBrowseNavigation(); const { toast } = useToast(); - const [copied, setCopied] = useState(false); - const containerRef = useRef(null); const breadcrumbsRef = useRef(null); const [visibleSegmentCount, setVisibleSegmentCount] = useState(null); - + const domain = useDomain(); + // Create breadcrumb segments from file path const breadcrumbSegments = useMemo(() => { const pathParts = path.split('/').filter(Boolean); const segments: BreadcrumbSegment[] = []; - + let currentPath = ''; pathParts.forEach((part, index) => { currentPath = currentPath ? `${currentPath}/${part}` : part; const isLastSegment = index === pathParts.length - 1; - + // Calculate highlight range for this segment if it exists let segmentHighlight: { from: number; to: number } | undefined; if (pathHighlightRange) { const segmentStart = path.indexOf(part, currentPath.length - part.length); const segmentEnd = segmentStart + part.length; - + // Check if highlight overlaps with this segment if (pathHighlightRange.from < segmentEnd && pathHighlightRange.to > segmentStart) { segmentHighlight = { @@ -89,7 +96,7 @@ export const PathHeader = ({ }; } } - + segments.push({ name: part, fullPath: currentPath, @@ -97,7 +104,7 @@ export const PathHeader = ({ highlightRange: segmentHighlight }); }); - + return segments; }, [path, pathHighlightRange]); @@ -105,10 +112,10 @@ export const PathHeader = ({ useEffect(() => { const measureSegments = () => { if (!containerRef.current || !breadcrumbsRef.current) return; - + const containerWidth = containerRef.current.offsetWidth; const availableWidth = containerWidth - 175; // Reserve space for copy button and padding - + // Create a temporary element to measure segment widths const tempElement = document.createElement('div'); tempElement.style.position = 'absolute'; @@ -116,17 +123,17 @@ export const PathHeader = ({ tempElement.style.whiteSpace = 'nowrap'; tempElement.className = 'font-mono text-sm'; document.body.appendChild(tempElement); - + let totalWidth = 0; let visibleCount = breadcrumbSegments.length; - + // Start from the end (most important segments) and work backwards for (let i = breadcrumbSegments.length - 1; i >= 0; i--) { const segment = breadcrumbSegments[i]; tempElement.textContent = segment.name; const segmentWidth = tempElement.offsetWidth; const separatorWidth = i < breadcrumbSegments.length - 1 ? 16 : 0; // ChevronRight width - + if (totalWidth + segmentWidth + separatorWidth > availableWidth && i > 0) { // If adding this segment would overflow and it's not the last segment visibleCount = breadcrumbSegments.length - i; @@ -136,21 +143,21 @@ export const PathHeader = ({ } break; } - + totalWidth += segmentWidth + separatorWidth; } - + document.body.removeChild(tempElement); setVisibleSegmentCount(visibleCount); }; measureSegments(); - + const resizeObserver = new ResizeObserver(measureSegments); if (containerRef.current) { resizeObserver.observe(containerRef.current); } - + return () => resizeObserver.disconnect(); }, [breadcrumbSegments]); @@ -170,21 +177,10 @@ export const PathHeader = ({ const onCopyPath = useCallback(() => { navigator.clipboard.writeText(path); - setCopied(true); toast({ description: "✅ Copied to clipboard" }); - setTimeout(() => setCopied(false), 1500); + return true; }, [path, toast]); - const onBreadcrumbClick = useCallback((segment: BreadcrumbSegment) => { - navigateToPath({ - repoName: repo.name, - path: segment.fullPath, - pathType: segment.isLastSegment ? pathType : 'tree', - revisionName: branchDisplayName, - }); - }, [repo.name, branchDisplayName, navigateToPath, pathType]); - - const renderSegmentWithHighlight = (segment: BreadcrumbSegment) => { if (!segment.highlightRange) { return segment.name; @@ -204,28 +200,34 @@ export const PathHeader = ({ return (
- {info?.icon ? ( - - {info.codeHostName} - - ) : ( - + {isCodeHostIconVisible && ( + <> + {info?.icon ? ( + + {info.codeHostName} + + ) : ( + + )} + )} -
navigateToPath({ + + {info?.displayName} -
+ {branchDisplayName && (

{hiddenSegments.map((segment) => ( - onBreadcrumbClick(segment)} - className="font-mono text-sm cursor-pointer" > - {renderSegmentWithHighlight(segment)} - + + {renderSegmentWithHighlight(segment)} + + ))} @@ -269,32 +279,33 @@ export const PathHeader = ({ )} {visibleSegments.map((segment, index) => (

- + )} + onBreadcrumbClick(segment)} + href={getBrowsePath({ + repoName: repo.name, + path: segment.fullPath, + pathType: segment.isLastSegment ? pathType : 'tree', + revisionName: branchDisplayName, + domain, + })} > {renderSegmentWithHighlight(segment)} - + {index < visibleSegments.length - 1 && ( )}
))}
- +
) diff --git a/packages/web/src/app/[domain]/components/progressNavIndicator.tsx b/packages/web/src/app/[domain]/components/progressNavIndicator.tsx index 7d79d7bc..f9e0d8cb 100644 --- a/packages/web/src/app/[domain]/components/progressNavIndicator.tsx +++ b/packages/web/src/app/[domain]/components/progressNavIndicator.tsx @@ -1,6 +1,5 @@ "use client"; -import { getRepos } from "@/actions"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; import useCaptureEvent from "@/hooks/useCaptureEvent"; import { useDomain } from "@/hooks/useDomain"; @@ -10,14 +9,15 @@ import { RepoIndexingStatus } from "@prisma/client"; import { useQuery } from "@tanstack/react-query"; import { Loader2Icon } from "lucide-react"; import Link from "next/link"; +import { getRepos } from "@/app/api/(client)/client"; export const ProgressNavIndicator = () => { const domain = useDomain(); const captureEvent = useCaptureEvent(); const { data: inProgressRepos, isPending, isError } = useQuery({ - queryKey: ['repos', domain], - queryFn: () => unwrapServiceError(getRepos(domain)), + queryKey: ['repos'], + queryFn: () => unwrapServiceError(getRepos()), select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.IN_INDEX_QUEUE || repo.repoIndexingStatus === RepoIndexingStatus.INDEXING), refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS, }); @@ -50,17 +50,13 @@ export const ProgressNavIndicator = () => {
{ inProgressRepos.slice(0, 10) - .filter(item => item.linkedConnections.length > 0) // edge case: don't show repos that are orphaned and awaiting gc. .map(item => ( - // Link to the first connection for the repo - captureEvent('wa_progress_nav_job_pressed', {})}> -
- {item.repoName} -
- +
+ {item.repoName} +
) )} {inProgressRepos.length > 10 && ( diff --git a/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx b/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx index d6fec7d8..6dbd47cd 100644 --- a/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx +++ b/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx @@ -78,8 +78,8 @@ const searchBarContainerVariants = cva( { variants: { size: { - default: "h-10", - sm: "h-8" + default: "min-h-10", + sm: "min-h-8" } }, defaultVariants: { @@ -168,6 +168,7 @@ export const SearchBar = ({ keymap.of(searchBarKeymap), history(), zoekt(), + EditorView.lineWrapping, EditorView.updateListener.of(update => { if (update.selectionSet) { const selection = update.state.selection.main; @@ -259,7 +260,7 @@ export const SearchBar = ({ /> { diff --git a/packages/web/src/app/[domain]/components/searchBar/searchSuggestionsBox.tsx b/packages/web/src/app/[domain]/components/searchBar/searchSuggestionsBox.tsx index 6b208c0e..eb167640 100644 --- a/packages/web/src/app/[domain]/components/searchBar/searchSuggestionsBox.tsx +++ b/packages/web/src/app/[domain]/components/searchBar/searchSuggestionsBox.tsx @@ -334,6 +334,7 @@ const SearchSuggestionsBox = forwardRef(({ } if (e.key === 'ArrowUp') { + e.preventDefault(); e.stopPropagation(); setHighlightedSuggestionIndex((curIndex) => { return curIndex <= 0 ? suggestions.length - 1 : curIndex - 1; @@ -341,6 +342,7 @@ const SearchSuggestionsBox = forwardRef(({ } if (e.key === 'ArrowDown') { + e.preventDefault(); e.stopPropagation(); setHighlightedSuggestionIndex((curIndex) => { return curIndex >= suggestions.length - 1 ? 0 : curIndex + 1; diff --git a/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts b/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts index 04c2514d..a1f9c453 100644 --- a/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts +++ b/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts @@ -19,7 +19,7 @@ import { VscSymbolVariable } from "react-icons/vsc"; import { useSearchHistory } from "@/hooks/useSearchHistory"; -import { getDisplayTime, isServiceError } from "@/lib/utils"; +import { getDisplayTime, isServiceError, unwrapServiceError } from "@/lib/utils"; import { useDomain } from "@/hooks/useDomain"; @@ -37,12 +37,12 @@ export const useSuggestionsData = ({ }: Props) => { const domain = useDomain(); const { data: repoSuggestions, isLoading: _isLoadingRepos } = useQuery({ - queryKey: ["repoSuggestions", domain], - queryFn: () => getRepos(domain), + queryKey: ["repoSuggestions"], + queryFn: () => unwrapServiceError(getRepos()), select: (data): Suggestion[] => { - return data.repos + return data .map(r => ({ - value: r.name, + value: r.repoName, })); }, enabled: suggestionMode === "repo", diff --git a/packages/web/src/app/[domain]/components/settingsDropdown.tsx b/packages/web/src/app/[domain]/components/settingsDropdown.tsx index 4f3350f6..377d0ef7 100644 --- a/packages/web/src/app/[domain]/components/settingsDropdown.tsx +++ b/packages/web/src/app/[domain]/components/settingsDropdown.tsx @@ -46,7 +46,7 @@ export const SettingsDropdown = ({ const { theme: _theme, setTheme } = useTheme(); const [keymapType, setKeymapType] = useKeymapType(); - const { data: session, update } = useSession(); + const { data: session } = useSession(); const domain = useDomain(); const theme = useMemo(() => { @@ -67,14 +67,7 @@ export const SettingsDropdown = ({ }, [theme]); return ( - // Was hitting a bug with invite code login where the first time the user signs in, the settingsDropdown doesn't have a valid session. To fix this - // we can simply update the session everytime the settingsDropdown is opened. This isn't a super frequent operation and updating the session is low cost, - // so this is a simple solution to the problem. - { - if (isOpen) { - update(); - } - }}> + -
- - -
- ) -} diff --git a/packages/web/src/app/login/components/loginForm.tsx b/packages/web/src/app/login/components/loginForm.tsx index ff0e555c..1d1eb5e3 100644 --- a/packages/web/src/app/login/components/loginForm.tsx +++ b/packages/web/src/app/login/components/loginForm.tsx @@ -5,14 +5,9 @@ import { Card } from "@/components/ui/card"; import { SourcebotLogo } from "@/app/components/sourcebotLogo"; import { AuthMethodSelector } from "@/app/components/authMethodSelector"; import useCaptureEvent from "@/hooks/useCaptureEvent"; -import DemoCard from "@/app/login/components/demoCard"; import Link from "next/link"; -import { env } from "@/env.mjs"; import type { AuthProvider } from "@/lib/authProviders"; -const TERMS_OF_SERVICE_URL = "https://sourcebot.dev/terms"; -const PRIVACY_POLICY_URL = "https://sourcebot.dev/privacy"; - interface LoginFormProps { callbackUrl?: string; error?: string; @@ -72,11 +67,6 @@ export const LoginForm = ({ callbackUrl, error, providers, context }: LoginFormP {context === "login" ? "Sign in to your account" : "Create a new account"} - {env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined && ( -
- -
- )} {error && (
@@ -102,9 +92,6 @@ export const LoginForm = ({ callbackUrl, error, providers, context }: LoginFormP }

- {env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined && ( -

By signing in, you agree to the Terms of Service and Privacy Policy.

- )}
) } diff --git a/packages/web/src/app/login/page.tsx b/packages/web/src/app/login/page.tsx index 48e5031a..f83ac939 100644 --- a/packages/web/src/app/login/page.tsx +++ b/packages/web/src/app/login/page.tsx @@ -10,13 +10,14 @@ import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; const logger = createLogger('login-page'); interface LoginProps { - searchParams: { + searchParams: Promise<{ callbackUrl?: string; error?: string; - } + }> } -export default async function Login({ searchParams }: LoginProps) { +export default async function Login(props: LoginProps) { + const searchParams = await props.searchParams; logger.info("Login page loaded"); const session = await auth(); if (session) { diff --git a/packages/web/src/app/onboard/page.tsx b/packages/web/src/app/onboard/page.tsx index fbc746d9..b446c15f 100644 --- a/packages/web/src/app/onboard/page.tsx +++ b/packages/web/src/app/onboard/page.tsx @@ -1,4 +1,5 @@ import type React from "react" +import Link from "next/link" import { Card, CardContent } from "@/components/ui/card" import { Button } from "@/components/ui/button" @@ -14,13 +15,13 @@ import { prisma } from "@/prisma"; import { OrgRole } from "@sourcebot/db"; import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch"; import { redirect } from "next/navigation"; -import { BetweenHorizontalStart, GitBranchIcon, LockIcon } from "lucide-react"; +import { BetweenHorizontalStart, Brain, GitBranchIcon, LockIcon } from "lucide-react"; import { hasEntitlement } from "@sourcebot/shared"; import { env } from "@/env.mjs"; import { GcpIapAuth } from "@/app/[domain]/components/gcpIapAuth"; interface OnboardingProps { - searchParams?: { step?: string }; + searchParams?: Promise<{ step?: string }>; } interface OnboardingStep { @@ -38,7 +39,8 @@ interface ResourceCard { icon?: React.ReactNode } -export default async function Onboarding({ searchParams }: OnboardingProps) { +export default async function Onboarding(props: OnboardingProps) { + const searchParams = await props.searchParams; const providers = getAuthProviders(); const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); const session = await auth(); @@ -87,6 +89,13 @@ export default async function Onboarding({ searchParams }: OnboardingProps) { href: "https://docs.sourcebot.dev/docs/connections/overview", icon: , }, + { + id: "language-models", + title: "Language Models", + description: "Learn how to configure your language model providers to start using Ask Sourcebot", + href: "https://docs.sourcebot.dev/docs/configuration/language-model-providers", + icon: , + }, { id: "authentication-system", title: "Authentication System", @@ -111,7 +120,7 @@ export default async function Onboarding({ searchParams }: OnboardingProps) { component: (
), @@ -163,7 +172,7 @@ export default async function Onboarding({ searchParams }: OnboardingProps) {
), diff --git a/packages/web/src/app/redeem/page.tsx b/packages/web/src/app/redeem/page.tsx index 3513e5b2..77c482f2 100644 --- a/packages/web/src/app/redeem/page.tsx +++ b/packages/web/src/app/redeem/page.tsx @@ -9,12 +9,13 @@ import { getOrgFromDomain } from '@/data/org'; import { SINGLE_TENANT_ORG_DOMAIN } from '@/lib/constants'; interface RedeemPageProps { - searchParams: { + searchParams: Promise<{ invite_id?: string; - }; + }>; } -export default async function RedeemPage({ searchParams }: RedeemPageProps) { +export default async function RedeemPage(props: RedeemPageProps) { + const searchParams = await props.searchParams; const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); if (!org || !org.isOnboarded) { return redirect("/onboard"); diff --git a/packages/web/src/app/signup/page.tsx b/packages/web/src/app/signup/page.tsx index cabc47a0..dc920596 100644 --- a/packages/web/src/app/signup/page.tsx +++ b/packages/web/src/app/signup/page.tsx @@ -10,13 +10,14 @@ import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; const logger = createLogger('signup-page'); interface LoginProps { - searchParams: { + searchParams: Promise<{ callbackUrl?: string; error?: string; - } + }> } -export default async function Signup({ searchParams }: LoginProps) { +export default async function Signup(props: LoginProps) { + const searchParams = await props.searchParams; const session = await auth(); if (session) { logger.info("Session found in signup page, redirecting to home"); diff --git a/packages/web/src/components/ui/accordion.tsx b/packages/web/src/components/ui/accordion.tsx new file mode 100644 index 00000000..24c788c2 --- /dev/null +++ b/packages/web/src/components/ui/accordion.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/packages/web/src/components/ui/animatedResizableHandle.tsx b/packages/web/src/components/ui/animatedResizableHandle.tsx index c09635c4..db4be0df 100644 --- a/packages/web/src/components/ui/animatedResizableHandle.tsx +++ b/packages/web/src/components/ui/animatedResizableHandle.tsx @@ -1,11 +1,16 @@ 'use client'; +import { cn } from "@/lib/utils"; import { ResizableHandle } from "./resizable"; -export const AnimatedResizableHandle = () => { +interface AnimatedResizableHandleProps { + className?: string; +} + +export const AnimatedResizableHandle = ({ className }: AnimatedResizableHandleProps) => { return ( ) } \ No newline at end of file diff --git a/packages/web/src/components/ui/collapsible.tsx b/packages/web/src/components/ui/collapsible.tsx new file mode 100644 index 00000000..9fa48946 --- /dev/null +++ b/packages/web/src/components/ui/collapsible.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/packages/web/src/components/ui/data-table.tsx b/packages/web/src/components/ui/data-table.tsx index 94f83b56..ce99592c 100644 --- a/packages/web/src/components/ui/data-table.tsx +++ b/packages/web/src/components/ui/data-table.tsx @@ -22,14 +22,13 @@ import { import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import * as React from "react" -import { PlusIcon } from "lucide-react" -import { env } from "@/env.mjs" interface DataTableProps { columns: ColumnDef[] data: TData[] searchKey: string - searchPlaceholder?: string + searchPlaceholder?: string, + headerActions?: React.ReactNode, } export function DataTable({ @@ -37,6 +36,8 @@ export function DataTable({ data, searchKey, searchPlaceholder, + headerActions, + }: DataTableProps) { const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState( @@ -75,18 +76,7 @@ export function DataTable({ Show a button on the demo site that allows users to add new repositories by updating the demo-site-config.json file and opening a PR. */} - {env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo" && ( - - )} + {headerActions}
diff --git a/packages/web/src/components/ui/dialog.tsx b/packages/web/src/components/ui/dialog.tsx index 4d013a60..de2b9d97 100644 --- a/packages/web/src/components/ui/dialog.tsx +++ b/packages/web/src/components/ui/dialog.tsx @@ -31,8 +31,10 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName const DialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + closeButtonClassName?: string + } +>(({ className, children, closeButtonClassName, ...props }, ref) => ( {children} - + Close diff --git a/packages/web/src/components/ui/scroll-area.tsx b/packages/web/src/components/ui/scroll-area.tsx index 0b4a48d8..be0b2d8f 100644 --- a/packages/web/src/components/ui/scroll-area.tsx +++ b/packages/web/src/components/ui/scroll-area.tsx @@ -14,7 +14,8 @@ const ScrollArea = React.forwardRef< className={cn("relative overflow-hidden", className)} {...props} > - + {/* @see: https://github.com/radix-ui/primitives/issues/926#issuecomment-1447283516 */} + {children} diff --git a/packages/web/src/components/ui/select.tsx b/packages/web/src/components/ui/select.tsx index cbe5a36b..f28b0b86 100644 --- a/packages/web/src/components/ui/select.tsx +++ b/packages/web/src/components/ui/select.tsx @@ -129,7 +129,7 @@ const SelectItem = React.forwardRef< - {children} + {children} )) SelectItem.displayName = SelectPrimitive.Item.displayName diff --git a/packages/web/src/components/ui/textarea.tsx b/packages/web/src/components/ui/textarea.tsx new file mode 100644 index 00000000..4d858bb6 --- /dev/null +++ b/packages/web/src/components/ui/textarea.tsx @@ -0,0 +1,22 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Textarea = React.forwardRef< + HTMLTextAreaElement, + React.ComponentProps<"textarea"> +>(({ className, ...props }, ref) => { + return ( +