npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

opencode-web-plugin

v0.1.1

Published

Multi-provider web search and content fetching for OpenCode v2

Readme

opencode-web-plugin

opencode-web-plugin is an OpenCode v2 plugin that exposes two directly callable tools:

  • web.search searches the public web through the first configured search provider.
  • web.fetch extracts readable content from an HTTP or HTTPS URL through the first configured fetch provider.

The plugin uses provider REST APIs directly, has no provider SDK dependencies, never performs a native webpage fetch, and does not include a browser, crawler, cache, or telemetry.

OpenCode v2's Plugin API is currently beta. This package targets the current @opencode-ai/plugin@next API.

Providers

| Provider | Search | Fetch | Environment | |---|---:|---:|---| | Tavily | ✓ | ✓ | TAVILY_API_KEY | | Exa | ✓ | ✓ | EXA_API_KEY | | Brave Search | ✓ | — | BRAVE_SEARCH_API_KEY | | Serper | ✓ | — | SERPER_API_KEY | | Jina | ✓ | ✓ | JINA_API_KEY |

Jina is considered configured only when JINA_API_KEY is present, including for Reader requests.

Installation

Set at least one provider API key:

export TAVILY_API_KEY=tvly-xxx

Then add the package to opencode.json or opencode.jsonc:

{
  "$schema": "https://opencode.ai/config.json",
  "plugins": ["opencode-web-plugin"]
}

OpenCode will register web.search and web.fetch as direct, non-CodeMode tools. API keys are resolved only when a tool executes, so the plugin still loads when no keys are present or when only one capability is configured.

For local development, build the package and reference its absolute directory or a relative path from the configuration file:

{
  "plugins": ["/absolute/path/to/opencode-web-plugin"]
}

Provider selection

The default order is:

search: tavily → exa → brave → serper → jina
fetch:  jina → tavily → exa

Selection skips providers that do not support the requested capability or whose configured environment variable is absent. Once a provider is selected, an authentication failure, rate limit, timeout, network failure, or upstream error is returned immediately. Runtime failures never trigger another provider, preventing unexpected requests and billing.

Examples:

  • With only EXA_API_KEY, both tools use Exa.
  • With only BRAVE_SEARCH_API_KEY, web.search works and web.fetch reports CONFIG_ERROR when called.
  • With TAVILY_API_KEY but no JINA_API_KEY, web.fetch uses Tavily Extract.

Configuration

All options are optional. These are the effective defaults:

{
  "plugins": [
    {
      "package": "opencode-web-plugin",
      "options": {
        "search": {
          "providerOrder": ["tavily", "exa", "brave", "serper", "jina"],
          "maxResults": 5,
          "timeoutMs": 15000
        },
        "fetch": {
          "providerOrder": ["jina", "tavily", "exa"],
          "maxChars": 50000,
          "timeoutMs": 30000
        },
        "providers": {
          "tavily": { "apiKeyEnv": "TAVILY_API_KEY" },
          "exa": { "apiKeyEnv": "EXA_API_KEY" },
          "brave": { "apiKeyEnv": "BRAVE_SEARCH_API_KEY" },
          "serper": { "apiKeyEnv": "SERPER_API_KEY" },
          "jina": { "apiKeyEnv": "JINA_API_KEY", "region": "global" }
        }
      }
    }
  ]
}

To change an environment variable name without putting the secret in OpenCode configuration:

{
  "plugins": [
    {
      "package": "opencode-web-plugin",
      "options": {
        "providers": {
          "exa": { "apiKeyEnv": "MY_EXA_API_KEY" }
        }
      }
    }
  ]
}

The plugin will read process.env.MY_EXA_API_KEY at tool execution time. Never place the actual key in opencode.json.

To use Jina's Reader and Search endpoints optimized for access from mainland China:

{
  "plugins": [
    {
      "package": "opencode-web-plugin",
      "options": {
        "providers": {
          "jina": { "region": "cn" }
        }
      }
    }
  ]
}

The default "global" region uses r.jina.ai and s.jina.ai. The "cn" region uses the API-compatible r.jinaai.cn and s.jinaai.cn endpoints. API keys and authentication are unchanged.

To restrict both capabilities to Exa:

{
  "plugins": [
    {
      "package": "opencode-web-plugin",
      "options": {
        "search": { "providerOrder": ["exa"] },
        "fetch": { "providerOrder": ["exa"] }
      }
    }
  ]
}

Tool contracts

web.search

Input:

interface WebSearchInput {
  query: string
  maxResults?: number // 1–20; defaults to configured search.maxResults
}

Structured output:

interface WebSearchOutput {
  provider: string
  results: Array<{
    rank: number
    title: string
    url: string
    snippet: string
    publishedAt?: string
  }>
}

Every snippet is capped at 1,500 characters. maxResults is an upper bound; Jina Search may return at most its fixed result count.

web.fetch

Input:

interface WebFetchInput {
  url: string // http:// or https:// only
  maxChars?: number // 1,000–100,000; defaults to configured fetch.maxChars
}

Structured output:

interface WebFetchOutput {
  provider: string
  url: string
  title?: string
  author?: string
  publishedAt?: string
  content: string
  truncated: boolean
}

Content is always locally capped after provider normalization, even when the provider supports its own limit.

Safety and errors

Model-visible output clearly labels search results and fetched pages as untrusted external content. Page text is data, never plugin configuration or instructions. The plugin does not follow links found in returned content.

Errors use one of these stable codes:

CONFIG_ERROR
INVALID_INPUT
INVALID_URL
TIMEOUT
AUTH_ERROR
RATE_LIMITED
UPSTREAM_ERROR
PARSE_ERROR

Provider response bodies, request headers, and API keys are never embedded in normalized errors.

Development

Requires Bun and Node.js 20 or newer:

bun install
bun run typecheck
bun test
bun run build

Tests mock fetch; they do not access the internet and do not require real provider keys.

Releasing

Releases must be created from a clean, up-to-date main branch. Install and authenticate the GitHub CLI before the first release:

gh auth login

Choose the appropriate semantic-version increment and run one command:

bun run release:patch # 0.1.0 -> 0.1.1
bun run release:minor # 0.1.0 -> 0.2.0
bun run release:major # 0.1.0 -> 1.0.0

The release command runs all checks, updates package.json, creates a version commit and vX.Y.Z tag, atomically pushes both, and creates a GitHub Release. GitHub generates the release title and notes from changes since the nearest previous tag. Publishing the GitHub Release triggers the npm publish workflow.

If GitHub Release creation fails after the push succeeds, the script prints a gh release create command that resumes the final step without bumping the version again.

Provider API mappings

  • Tavily Search: maxResultsmax_results; contentsnippet.
  • Tavily Extract: one URL in urls; raw_contentcontent.
  • Exa Search: maxResultsnumResults; contents.highlights → joined snippet.
  • Exa Contents: URL in ids; text.maxCharacters receives the requested limit.
  • Brave Search: maxResultscount; descriptionsnippet.
  • Serper: only organic results are normalized and locally sliced.
  • Jina Search: JSON mode via s.jina.ai; full result content is reduced to a bounded snippet.
  • Jina Reader: JSON mode via r.jina.ai; response timestamps are not assumed to be publication dates.

Known limitations

  • There is no native HTML fetch or parsing fallback.
  • There is no runtime provider failover or retry.
  • Jina anonymous Reader access is intentionally not used.
  • Provider metadata absent from the upstream response remains absent; the plugin does not infer dates, authors, or titles.
  • Provider APIs and the OpenCode v2 Plugin API can change; adapters may need updates as beta contracts evolve.