Skip to content

feat(core): add integration-backed search#35558

Open
nexxeln wants to merge 7 commits into
v2from
search-integration
Open

feat(core): add integration-backed search#35558
nexxeln wants to merge 7 commits into
v2from
search-integration

Conversation

@nexxeln

@nexxeln nexxeln commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

  • add a Location-scoped Search module backed by Integration search implementations
  • persist first-use provider selection to the global opencode.json while preserving JSONC comments
  • migrate Exa and Parallel out of the websearch tool into first-party integration plugins
  • onboard provider choice inline and support changing/authenticating the default from the TUI connection dialog
  • expose atomic search provider registration to Effect and Promise plugins
  • expose search query and provider-selection APIs through the generated SDKs

Testing

  • bun turbo typecheck
  • focused Core Search, Integration, Config, Form, plugin, and websearch tests
  • Schema, Protocol, Client, SDK, and TUI package tests/typechecks
  • Core migration consistency check

Example search provider plugin

A search provider is one scoped Integration definition. This example adds Brave Search from .opencode/plugin/brave-search.ts:

import { define } from "@opencode-ai/plugin/v2/effect"
import { Effect, Schema, Scope } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"

const BraveResponse = Schema.Struct({
  web: Schema.optional(
    Schema.Struct({
      results: Schema.Array(
        Schema.Struct({
          title: Schema.String,
          url: Schema.String,
          description: Schema.optional(Schema.String),
        }),
      ),
    }),
  ),
})

export default define<HttpClient.HttpClient | Scope.Scope>({
  id: "brave-search",
  effect: Effect.fn("BraveSearch.Plugin")(function* (ctx) {
    const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient)

    yield* ctx.integration.register({
      id: "brave",
      name: "Brave Search",
      methods: [
        { type: "key", label: "API key" },
        { type: "env", names: ["BRAVE_SEARCH_API_KEY"] },
      ],
      search: {
        connection: "required",
        execute: (input, context) =>
          Effect.gen(function* () {
            if (context.credential?.type !== "key") return yield* Effect.fail(new Error("Missing Brave API key"))

            const url = new URL("https://api.search.brave.com/res/v1/web/search")
            url.searchParams.set("q", input.query)
            url.searchParams.set("count", String(input.numResults ?? 8))

            const response = yield* HttpClientRequest.get(url.toString()).pipe(
              HttpClientRequest.acceptJson,
              HttpClientRequest.setHeader("X-Subscription-Token", context.credential.key),
              http.execute,
              Effect.flatMap(HttpClientResponse.schemaBodyJson(BraveResponse)),
            )
            const results = response.web?.results ?? []
            return {
              text: results
                .map((result) => `## [${result.title}](${result.url})${result.description ? `\n${result.description}` : ""}`)
                .join("\n\n"),
              metadata: response,
            }
          }),
      },
    })
  }),
})

The provider automatically appears in first-use selection and /connect; the key method stores a credential, while the env method resolves BRAVE_SEARCH_API_KEY without persistence. integration.transform(...) remains available for plugins that augment an existing Integration, but search execution is registered atomically through integration.register(...).

Part 1 of #35038.

nexxeln added 6 commits July 6, 2026 19:46
# Conflicts:
#	packages/client/test/promise.test.ts
#	packages/core/schema.json
#	packages/core/src/database/migration.gen.ts
#	packages/core/src/tool/websearch.ts
#	packages/sdk-next/src/index.ts
#	packages/sdk/js/src/v2/gen/types.gen.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant