{"owner":"Gitlawb","repo":"openclaude","hasSkills":true,"totalSkillsCount":2,"totalTokensCount":6252,"categories":["root-instruction","anthropic-skill"],"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","src/tools/WebSearchTool/README_SEARCH_PROVIDERS.md"],"skills":{"AGENTS.md":"# AGENTS.md - AI Agent Coding Guide\n\nThis guide is for AI coding agents working in the OpenClaude repository. Read it before changing code, and also follow [CONTRIBUTING.md](CONTRIBUTING.md) for contributor policy, PR expectations, review follow-up, and project scope.\n\n## Project Snapshot\n\nOpenClaude is a coding-agent CLI for cloud and local model providers. It supports OpenAI-compatible APIs, Anthropic, Gemini, DeepSeek, Ollama, MCP, local backends, slash commands, tools, agents, and a React/Ink terminal UI.\n\nThe installed CLI runs on Node.js `>=22.0.0`. Bun is used for source builds, scripts, dependency management, and tests.\n\n## Work Style\n\n- Keep changes focused on one problem.\n- Prefer existing patterns in the file or nearby module.\n- Avoid unrelated formatting, renames, dependency changes, or broad rewrites.\n- Add or update tests when behavior changes.\n- Update docs when setup, commands, provider behavior, or user-facing behavior changes.\n- For new features, larger refactors, dependencies, or runtime changes, follow the issue-first guidance in [CONTRIBUTING.md](CONTRIBUTING.md).\n- Keep PR branches current with `main` using the synchronization and guarded-push workflow in [CONTRIBUTING.md § Keep Your Branch Current](CONTRIBUTING.md#keep-your-branch-current). Rebase whenever resuming work or pushing follow-up fixes, but never overwrite remote PR-head updates with an unguarded force-push.\n- Run the authoritative local pre-push validation contract defined in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation) before every push to a PR, not just the first one. CI adds clean-runner and supported-Node-matrix coverage that is not practical to reproduce in one local shell.\n\n## Stack And Conventions\n\n- TypeScript with strict mode and ESM imports.\n- React + Ink for terminal UI.\n- Bun lockfile and Bun scripts for development workflows.\n- Node runtime for the built CLI.\n\nCommon libraries and patterns:\n\n- `chalk` for terminal color.\n- `commander` for CLI argument parsing.\n- `execa` for child processes.\n- Existing service, provider, settings, permission, and UI patterns over new abstractions.\n\n## Repository Map\n\n- `src/commands/` - slash and CLI command implementations.\n- `src/components/` - React/Ink UI components.\n- `src/services/` - API, MCP, OAuth, wiki, voice, and other service integrations.\n- `src/tools/` - tool implementations.\n- `src/utils/` - shared utilities.\n- `src/integrations/` - provider and model integration metadata.\n- `src/entrypoints/` - CLI, MCP, SDK, and generated public types.\n- `src/tasks/` - local, remote, workflow, and monitor task handling.\n- `docs/integrations/` - provider integration guidance.\n- `web/` - documentation website.\n\n## Validation\n\nThe authoritative local pre-push validation contract lives in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation) and must be run before every push to a PR, including follow-up fixes during review. It covers the same command families as `.github/workflows/pr-checks.yml`; CI remains authoritative for clean-runner, supported-Node-matrix, and platform-specific coverage. The lists below are for narrowing checks while you iterate; they do not replace the pre-push contract.\n\nCore checks:\n\n```bash\nbun install\nbun run build\nbun run smoke\nbun run check\nbun run typecheck\nbun run typecheck:type-tests\n```\n\nFocused checks:\n\n```bash\nbun test ./path/to/test-file.test.ts\nbun run test:provider\nbun run test:provider-recommendation\n```\n\nWeb checks, when changes can affect the site:\n\n```bash\nbun run web:typecheck\nbun run web:build\n```\n\nWebsite release notes live on GitHub Releases; do not add a manually maintained release-notes data source to the static site.\n\nDiagnostics and PR hygiene:\n\n```bash\nbun run doctor:runtime\n```\n\nFor PR intent scanning, use the canonical upstream fetch and explicit-ref invocation in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation); the scanner's default `origin/main` base is not portable to fork checkouts.\n\n## Provider Changes\n\nWhen modifying provider behavior:\n\n1. Start with `docs/integrations/overview.md`.\n2. Use the relevant how-to guide under `docs/integrations/how-to/`.\n3. Check existing provider implementations before adding a new pattern.\n4. Test the exact provider/model path you changed when possible.\n5. Avoid breaking third-party providers while fixing first-party behavior.\n\n## Things To Avoid\n\n- Do not change the Node runtime or Bun development workflow without prior maintainer agreement.\n- Do not add new Python code, Python provider paths, or Python dependencies without explicit maintainer approval.\n- Do not introduce dependencies without clear project benefit.\n- Do not skip tests for behavior changes.\n- Do not silently change provider tags; maintainers control them during review.\n- Do not ignore CodeRabbit or maintainer feedback; address it before requesting more review. Before applying an automated review suggestion, verify it does not pull the PR away from its stated scope or intent — decline out-of-scope suggestions with justification, or ask a maintainer when unsure. Never silently ignore findings.\n- Do not push commits with failing, incomplete, or unrun local checks unless an exception in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation) applies. Verify pre-existing failures against the current PR base and document the evidence in the PR; PR-owned failures must still be fixed.\n- Do not submit a PR whose description still contains template placeholder text; fill in every section of the [PR template](.github/pull_request_template.md) for the actual change.\n- Do not surface-patch recurring review findings; repeated fix requests usually indicate a core design issue — investigate and fix the root cause instead of the reported symptom.\n- Do not add a manually maintained release-notes data source to the static site; link to GitHub Releases instead.\n","src/tools/WebSearchTool/README_SEARCH_PROVIDERS.md":"# Web Search Providers\n\nOpenClaude supports multiple search backends through a provider adapter system.\n\n**Exa is the default search backend.** With no configuration at all, web search\nruns on Exa's keyless free tier. Add an `EXA_API_KEY` for higher limits; any other\nbackend you configure with a key is used before the free tier.\n\n## Configure with `/search`\n\nThe `/search` command sets everything below without editing env vars:\n\n| Command | Effect |\n|---|---|\n| `/search` | Pick a backend from a list that shows which ones have keys |\n| `/search <backend>` | Select a backend directly (`auto`, `exa`, `tavily`, `brave`, …); prompts for its key if needed |\n| `/search key [backend]` | Add or replace an API key in a hidden input (defaults to `exa`) |\n| `/search remove-key [backend]` | Remove a saved API key (defaults to `exa`) |\n| `/search status` | Show which backend handles searches and what it falls back to |\n| `/search test [query]` | Run a test search through the configured backends |\n\nSelections are saved as `WEB_SEARCH_PROVIDER` and the backend's key variable in\nthe `env` block of `~/.openclaude.json` (readable only by you — the same file\n`/provider` uses), and take effect immediately. Keys are never accepted on the\ncommand line.\n\nPrecedence: values saved by `/search` override the same variables exported in\nyour shell, and are overridden by an `env` entry for that variable in any\n`settings.json` (user, project, `--settings`, or managed policy) — `/search`\ntells you when that happens. `/search remove-key` only removes keys that\n`/search` saved; a key that comes from your shell or a settings file is reported\ninstead.\n\n## Supported Providers\n\n| Provider | Env Var | Auth Header | Method |\n|---|---|---|---|\n| Custom API | `WEB_SEARCH_API` | Configurable | GET/POST |\n| Ollama | Active Ollama route or `OLLAMA_API_KEY` | Local sign-in / `Authorization: Bearer` | POST |\n| SearXNG | `WEB_PROVIDER=searxng` | — | GET |\n| Google | `WEB_PROVIDER=google` + `GOOGLE_CSE_ID` | *(query param `?key=`)* | GET |\n| Brave (preset) | `WEB_PROVIDER=brave` | `X-Subscription-Token` | GET |\n| Brave (adapter) | `BRAVE_API_KEY` | `X-Subscription-Token` | GET |\n| SerpAPI | `WEB_PROVIDER=serpapi` | `Authorization: Bearer` | GET |\n| Firecrawl | `FIRECRAWL_API_KEY` | Internal | SDK |\n| Tavily | `TAVILY_API_KEY` | `Authorization: Bearer` | POST |\n| Exa (default) | `EXA_API_KEY` | `x-api-key` | POST |\n| Exa free tier | *(none — zero-config default)* | — | POST |\n| AnySearch (explicit only) | `ANYSEARCH_API_KEY` (optional) | `Authorization: Bearer` when set | POST |\n| You.com | `YOU_API_KEY` | `X-API-Key` | GET |\n| Jina | `JINA_API_KEY` | `Authorization: Bearer` | GET |\n| Bing | `BING_API_KEY` | `Ocp-Apim-Subscription-Key` | GET |\n| Mojeek | `MOJEEK_API_KEY` | `Authorization: Bearer` | GET |\n| Linkup | `LINKUP_API_KEY` | `Authorization: Bearer` | POST |\n| DuckDuckGo | *(none — last-resort fallback)* | — | SDK |\n\n## Quick Start\n\n```bash\n# Exa (default) — works with no setup on the free tier; add a key for higher limits\nexport EXA_API_KEY=your-exa-key\n\n# Tavily (fast, RAG-ready)\nexport TAVILY_API_KEY=tvly-your-key\n\n# Brave (independent index, good free tier)\nexport BRAVE_API_KEY=your-brave-key\n\n# Bing\nexport BING_API_KEY=your-bing-key\n\n# Self-hosted SearXNG (free, private)\nexport WEB_PROVIDER=searxng\nexport WEB_SEARCH_API=https://search.example.com/search\n```\n\n## Provider Selection Mode\n\n`WEB_SEARCH_PROVIDER` controls fallback behavior:\n\n| Mode | Behavior |\n|---|---|\n| `auto` (default) | Try all configured providers in order, fall through on failure |\n| `ollama` | Local signed-in Ollama, then hosted Ollama when `OLLAMA_API_KEY` is set; throws if both fail |\n| `tavily` | Tavily only — throws on failure |\n| `exa` | Exa only — keyed API when `EXA_API_KEY` is set, otherwise the free tier; throws on failure |\n| `anysearch` | AnySearch only — anonymous without a key, Bearer authentication with one; throws on failure |\n| `brave` | Brave only — throws on failure |\n| `custom` | Custom API only — throws on failure. **Not in the auto chain** — must be explicitly selected |\n| `firecrawl` | Firecrawl only — throws on failure |\n| `ddg` | DuckDuckGo only — throws on failure |\n| `native` | Anthropic native / Codex only |\n\n**Auto mode priority:** exa → ollama → firecrawl → tavily → you → jina → brave → bing → mojeek → linkup → exa free tier → ddg\n\nEach entry is only tried when it is configured (has its key or route). Keyed Exa\nleads the chain; any other keyed backend still runs before the keyless Exa free\ntier, and DuckDuckGo is the last resort.\n\nIn `auto` mode, providers with built-in web search (Anthropic first-party,\nVertex, Foundry, Codex) keep using it. Set `WEB_SEARCH_PROVIDER=exa` to use Exa\nthere too.\n\n> **Note:** The `custom` provider is excluded from the `auto` chain. It is only used when `WEB_SEARCH_PROVIDER=custom` is explicitly set. This prevents the generic outbound provider from silently becoming the default backend.\n\nAnySearch is also explicit-only: selecting it does not change the existing `auto` chain.\n\n```bash\n# Fail loudly if Tavily is down (don't silently switch backends)\nexport WEB_SEARCH_PROVIDER=tavily\n\n# Try everything, fall through gracefully\nexport WEB_SEARCH_PROVIDER=auto\n```\n\n## Built-in Provider Timeout\n\nBuilt-in adapter providers use a 15s request timeout so `auto` mode can fall through when a backend stalls. Override it with:\n\n```bash\nexport WEB_SEARCH_TIMEOUT_SEC=30\n```\n\nInvalid, fractional, zero, negative, or very large values fall back to 15s. Custom API providers keep their separate `WEB_CUSTOM_TIMEOUT_SEC` setting because self-hosted endpoints may need different budgets.\n\n## Provider Request & Response Formats\n\n### AnySearch\n\nRun `/search anysearch` to use its general web search without a key, or `/search key anysearch` to add an optional key through hidden input. Anonymous access has lower service limits. `/search remove-key anysearch` removes a key saved by `/search`; keys from the shell or `settings.json` remain controlled by those sources.\n\n`ANYSEARCH_MAX_RESULTS` accepts an integer from 1 to 10 (default 10). Invalid values use 10. General search sends `query` and `max_results` to `POST https://api.anysearch.com/v1/search`. Results come from `data.results`; OpenClaude reports HTTP and business-code failures separately from an empty result list. A key, when present, is sent only as `Authorization: Bearer <key>` to that endpoint.\n\n```json\n{\"query\":\"search terms\",\"max_results\":10}\n```\n\n### Ollama\n\nWhen the active route resolves to Ollama, OpenClaude first calls the configured server's signed-in proxy:\n\n```text\nPOST http://localhost:11434/api/experimental/web_search\nContent-Type: application/json\n\n{\"query\":\"search terms\",\"max_results\":10}\n```\n\nIf that request fails and `OLLAMA_API_KEY` is set, it retries the hosted API with `Authorization: Bearer $OLLAMA_API_KEY`. The key is never sent to the local server.\n\n```text\nPOST https://ollama.com/api/web_search\nAuthorization: Bearer $OLLAMA_API_KEY\nContent-Type: application/json\n```\n\nBoth endpoints return structured `results` containing `title`, `url`, and `content`. Use `WEB_SEARCH_PROVIDER=ollama` to select only this local-to-hosted Ollama chain; in `auto` mode, another configured search backend is tried if Ollama search fails.\n\n### Tavily\n\n```bash\nexport TAVILY_API_KEY=tvly-your-key\n```\n\n**Request:**\n```\nPOST https://api.tavily.com/search\nAuthorization: Bearer tvly-your-key\nContent-Type: application/json\n\n{\"query\": \"search terms\", \"max_results\": 10, \"include_answer\": false}\n```\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"content\": \"Full text snippet from the page...\",\n      \"score\": 0.95\n    }\n  ]\n}\n```\n\n### Exa (default)\n\n```bash\nexport EXA_API_KEY=your-exa-key   # optional — the free tier works without it\n```\n\n| Env var | Default | Effect |\n|---|---|---|\n| `EXA_API_KEY` | — | Use the keyed Exa API (higher limits) |\n| `EXA_SEARCH_TYPE` | `auto` | `auto`, `instant`, `fast`, `deep-lite`, `deep`, or `deep-reasoning` (keyed API only; the deep types may need a higher `WEB_SEARCH_TIMEOUT_SEC`) |\n| `EXA_NUM_RESULTS` | `15` | Results per search, 1–50 (keyed API and free tier) |\n| `EXA_FREE_TIER` | on | Set to `0` to never use the keyless free tier |\n\n**Request:**\n```\nPOST https://api.exa.ai/search\nx-api-key: your-exa-key\nContent-Type: application/json\n\n{\"query\": \"search terms\", \"numResults\": 15, \"type\": \"auto\", \"contents\": {\"highlights\": true}}\n```\n\n`allowed_domains` / `blocked_domains` are sent as `includeDomains` / `excludeDomains`.\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"highlights\": [\"The most query-relevant excerpt...\", \"Another excerpt...\"],\n      \"highlightScores\": [0.91, 0.84]\n    }\n  ]\n}\n```\n\n### Exa Free Tier (Zero-Config Default)\n\nNo configuration needed. When no `EXA_API_KEY` is set, OpenClaude calls Exa's\nhosted endpoint, which serves a free tier with a per-IP requests-per-second limit\nand a daily quota. Concurrent searches are spaced automatically. When the limit is\nhit, `auto` mode falls through to DuckDuckGo and skips the free tier for the next\nfew minutes; in `exa` mode (and `/search test`) the error tells you to add a key\nwith `/search key exa`.\n\n**Request** (MCP streamable HTTP, JSON-RPC):\n```\nPOST https://mcp.exa.ai/mcp\nContent-Type: application/json\nAccept: application/json, text/event-stream\nx-exa-source: openclaude\n\n{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\",\n \"params\": {\"name\": \"web_search_exa\",\n            \"arguments\": {\"query\": \"search terms\", \"objective\": \"...\", \"numResults\": 15}}}\n```\n\nThe reply is plain text (`Title:` / `URL:` / `Highlights:` blocks) inside an SSE\n`message` event. `allowed_domains` become `site:` operators in the query, and both\ndomain lists are also enforced on the parsed results.\n\nOpt out (searches never go to Exa without a key):\n\n```bash\nexport EXA_FREE_TIER=0\n```\n\n### You.com\n\n```bash\nexport YOU_API_KEY=your-you-key\n```\n\n**Request:**\n```\nGET https://api.ydc-index.io/v1/search?query=search+terms\nX-API-Key: your-you-key\n```\n\n**Response:**\n```json\n{\n  \"results\": {\n    \"web\": [\n      {\n        \"title\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"snippets\": [\"First snippet from the page...\", \"Second snippet...\"],\n        \"description\": \"Page description\"\n      }\n    ]\n  }\n}\n```\n\n### Jina\n\n```bash\nexport JINA_API_KEY=your-jina-key\n```\n\n**Request:**\n```\nGET https://s.jina.ai/?q=search+terms\nAuthorization: Bearer your-jina-key\nAccept: application/json\n```\n\n**Response:**\n```json\n{\n  \"data\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"description\": \"Snippet from the page...\"\n    }\n  ]\n}\n```\n\n### Bing\n\n```bash\nexport BING_API_KEY=your-bing-key\n```\n\n**Request:**\n```\nGET https://api.bing.microsoft.com/v7.0/search?q=search+terms&count=10\nOcp-Apim-Subscription-Key: your-bing-key\n```\n\n**Response:**\n```json\n{\n  \"webPages\": {\n    \"value\": [\n      {\n        \"name\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"snippet\": \"A short excerpt from the page...\",\n        \"displayUrl\": \"example.com/page\"\n      }\n    ]\n  }\n}\n```\n\n### Mojeek\n\n```bash\nexport MOJEEK_API_KEY=your-mojeek-key\n```\n\n**Request:**\n```\nGET https://www.mojeek.com/search?q=search+terms&fmt=json\nAuthorization: Bearer your-mojeek-key\n```\n\n**Response:**\n```json\n{\n  \"response\": {\n    \"results\": [\n      {\n        \"title\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"snippet\": \"Excerpt from the page...\"\n      }\n    ]\n  }\n}\n```\n\n### Linkup\n\n```bash\nexport LINKUP_API_KEY=your-linkup-key\n```\n\n**Request:**\n```\nPOST https://api.linkup.so/v1/search\nAuthorization: Bearer your-linkup-key\nContent-Type: application/json\n\n{\"q\": \"search terms\", \"search_type\": \"standard\"}\n```\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"name\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"snippet\": \"A short description of the result...\"\n    }\n  ]\n}\n```\n\n### SearXNG (Built-in Preset)\n\n```bash\nexport WEB_PROVIDER=searxng\nexport WEB_SEARCH_API=https://search.example.com/search\n```\n\n**Request:**\n```\nGET https://search.example.com/search?q=search+terms\n```\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"content\": \"Snippet from the page...\",\n      \"engine\": \"google\"\n    }\n  ]\n}\n```\n\n### Google Custom Search (Built-in Preset)\n\n> ⚠️ **Sunset 2027-01-01.** Google has announced the Custom Search JSON API\n> will be discontinued and is closed to new customers. Use Brave/Tavily/Exa\n> for new setups.\n\n```bash\nexport WEB_PROVIDER=google\nexport WEB_KEY=your-google-api-key\nexport GOOGLE_CSE_ID=your-programmable-search-engine-id\n```\n\n`GOOGLE_CSE_ID` is the `cx` value from your Programmable Search Engine — both\nthe API key and the engine ID are required.\n\n**Request:**\n```\nGET https://www.googleapis.com/customsearch/v1?q=search+terms&key=your-google-api-key&cx=your-engine-id\n```\n\n**Response:**\n```json\n{\n  \"items\": [\n    {\n      \"title\": \"Result Title\",\n      \"link\": \"https://example.com/page\",\n      \"snippet\": \"A short excerpt...\",\n      \"displayLink\": \"example.com\"\n    }\n  ]\n}\n```\n\n### Brave (First-Class Adapter)\n\nThe recommended way to use Brave — auto-detected, joins the auto fallback chain.\n\n```bash\nexport BRAVE_API_KEY=your-brave-key\n```\n\n**Request:**\n```\nGET https://api.search.brave.com/res/v1/web/search?q=search+terms&count=15\nX-Subscription-Token: your-brave-key\n```\n\n### Brave (Built-in Preset, alternative)\n\nFor users who prefer the generic preset path. Functionally equivalent to the\nadapter above; either works.\n\n```bash\nexport WEB_PROVIDER=brave\nexport WEB_KEY=your-brave-key\n```\n\n**Response:**\n```json\n{\n  \"web\": {\n    \"results\": [\n      {\n        \"title\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"description\": \"Page description...\"\n      }\n    ]\n  }\n}\n```\n\n### SerpAPI (Built-in Preset)\n\n```bash\nexport WEB_PROVIDER=serpapi\nexport WEB_KEY=your-serpapi-key\n```\n\n**Request:**\n```\nGET https://serpapi.com/search.json?q=search+terms\nAuthorization: Bearer your-serpapi-key\n```\n\n**Response:**\n```json\n{\n  \"organic_results\": [\n    {\n      \"title\": \"Result Title\",\n      \"link\": \"https://example.com/page\",\n      \"snippet\": \"A short excerpt...\",\n      \"displayed_link\": \"example.com\"\n    }\n  ]\n}\n```\n\n### DuckDuckGo (Last-Resort Fallback)\n\nNo configuration needed. Uses the `duck-duck-scrape` npm package. In `auto` mode it\nruns only after every configured backend (including the Exa free tier) has failed.\n\n```bash\n# Set as explicit-only backend\nexport WEB_SEARCH_PROVIDER=ddg\n```\n\n---\n\n## Custom API Configuration\n\n### Standard GET\n\n```\nGET https://api.example.com/search?q=hello\n```\n\n```bash\nexport WEB_SEARCH_API=https://api.example.com/search\nexport WEB_QUERY_PARAM=q\n```\n\n### Query in URL Path\n\n```\nGET https://api.example.com/v2/search/hello\n```\n\n```bash\nexport WEB_URL_TEMPLATE=https://api.example.com/v2/search/{query}\n```\n\n### POST with Custom Body\n\n```\nPOST https://api.example.com/v1/query\nContent-Type: application/json\n\n{\"input\": {\"text\": \"hello\"}}\n```\n\n```bash\nexport WEB_SEARCH_API=https://api.example.com/v1/query\nexport WEB_METHOD=POST\nexport WEB_BODY_TEMPLATE='{\"input\":{\"text\":\"{query}\"}}'\n```\n\n### Extra Static Params\n\n```bash\nexport WEB_PARAMS='{\"lang\":\"en\",\"count\":\"10\"}'\n```\n\n## Auth\n\nAPI keys are sent in HTTP headers, **never** in query strings.\n\n```bash\n# Default: Authorization: Bearer <key>\nexport WEB_KEY=your-key\n\n# Custom header\nexport WEB_AUTH_HEADER=X-Api-Key\nexport WEB_AUTH_SCHEME=\"\"\n\n# Extra headers\nexport WEB_HEADERS=\"X-Tenant: acme; Accept: application/json\"\n```\n\n## Response Parsing\n\nThe tool auto-detects many response formats:\n\n```jsonc\n{ \"results\": [{ \"title\": \"...\", \"url\": \"...\" }] }     // flat array\n{ \"items\": [{ \"title\": \"...\", \"link\": \"...\" }] }       // Google-style\n{ \"results\": { \"engine\": [{ \"title\": \"...\", \"url\": \"...\" }] } }  // nested map\n[{ \"title\": \"...\", \"url\": \"...\" }]                      // bare array\n```\n\nField name aliases: `title`/`headline`/`name`, `url`/`link`/`href`, `description`/`snippet`/`content`\n\nFor deeply nested responses:\n```bash\nexport WEB_JSON_PATH=response.payload.results\n```\n\n## Retry\n\nFailed custom-provider requests (network errors, 5xx) are retried once after 500ms. Client errors (4xx) are not retried. Custom requests have a default 120s timeout.\n\n## Custom Provider Security Guardrails\n\nThe custom provider enforces the following guardrails by default:\n\n| Guardrail | Default | Override |\n|-----------|---------|----------|\n| HTTPS-only | ✅ | `WEB_CUSTOM_ALLOW_HTTP=true` |\n| Block private IPs / localhost | ✅ | `WEB_CUSTOM_ALLOW_PRIVATE=true` |\n| Header allowlist | ✅ | `WEB_CUSTOM_ALLOW_ARBITRARY_HEADERS=true` |\n| Max POST body | 300 KB | `WEB_CUSTOM_MAX_BODY_KB=<kb>` |\n| Request timeout | 120s | `WEB_CUSTOM_TIMEOUT_SEC=<seconds>` |\n| Audit log (one-time warning) | ✅ | — |\n\n### Self-hosted SearXNG example\n\n```bash\nexport WEB_PROVIDER=searxng\nexport WEB_SEARCH_API=https://search.mydomain.com/search\nexport WEB_CUSTOM_ALLOW_PRIVATE=true   # needed if SearXNG is on a private IP\n```\n\n### Header allowlist\n\nBy default only these headers are permitted:\n`accept`, `accept-encoding`, `accept-language`, `authorization`, `cache-control`, `content-type`, `if-modified-since`, `if-none-match`, `ocp-apim-subscription-key`, `user-agent`, `x-api-key`, `x-subscription-token`, `x-tenant-id`\n\n## Adding a Provider\n\n1. Create `providers/myprovider.ts`:\n\n```typescript\nimport type { SearchInput, SearchProvider } from './types.js'\nimport { applyDomainFilters, type ProviderOutput } from './types.js'\n\nexport const myProvider: SearchProvider = {\n  name: 'myprovider',\n  isConfigured() { return Boolean(process.env.MYPROVIDER_API_KEY) },\n  async search(input: SearchInput): Promise<ProviderOutput> {\n    const start = performance.now()\n    // ... call API, map to SearchHit[] ...\n    return {\n      hits: applyDomainFilters(hits, input),\n      providerName: 'myprovider',\n      durationSeconds: (performance.now() - start) / 1000,\n    }\n  },\n}\n```\n\n2. Register in `providers/index.ts` — add import and push to `ALL_PROVIDERS`.\n"},"files":{"AGENTS.md":"# AGENTS.md - AI Agent Coding Guide\n\nThis guide is for AI coding agents working in the OpenClaude repository. Read it before changing code, and also follow [CONTRIBUTING.md](CONTRIBUTING.md) for contributor policy, PR expectations, review follow-up, and project scope.\n\n## Project Snapshot\n\nOpenClaude is a coding-agent CLI for cloud and local model providers. It supports OpenAI-compatible APIs, Anthropic, Gemini, DeepSeek, Ollama, MCP, local backends, slash commands, tools, agents, and a React/Ink terminal UI.\n\nThe installed CLI runs on Node.js `>=22.0.0`. Bun is used for source builds, scripts, dependency management, and tests.\n\n## Work Style\n\n- Keep changes focused on one problem.\n- Prefer existing patterns in the file or nearby module.\n- Avoid unrelated formatting, renames, dependency changes, or broad rewrites.\n- Add or update tests when behavior changes.\n- Update docs when setup, commands, provider behavior, or user-facing behavior changes.\n- For new features, larger refactors, dependencies, or runtime changes, follow the issue-first guidance in [CONTRIBUTING.md](CONTRIBUTING.md).\n- Keep PR branches current with `main` using the synchronization and guarded-push workflow in [CONTRIBUTING.md § Keep Your Branch Current](CONTRIBUTING.md#keep-your-branch-current). Rebase whenever resuming work or pushing follow-up fixes, but never overwrite remote PR-head updates with an unguarded force-push.\n- Run the authoritative local pre-push validation contract defined in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation) before every push to a PR, not just the first one. CI adds clean-runner and supported-Node-matrix coverage that is not practical to reproduce in one local shell.\n\n## Stack And Conventions\n\n- TypeScript with strict mode and ESM imports.\n- React + Ink for terminal UI.\n- Bun lockfile and Bun scripts for development workflows.\n- Node runtime for the built CLI.\n\nCommon libraries and patterns:\n\n- `chalk` for terminal color.\n- `commander` for CLI argument parsing.\n- `execa` for child processes.\n- Existing service, provider, settings, permission, and UI patterns over new abstractions.\n\n## Repository Map\n\n- `src/commands/` - slash and CLI command implementations.\n- `src/components/` - React/Ink UI components.\n- `src/services/` - API, MCP, OAuth, wiki, voice, and other service integrations.\n- `src/tools/` - tool implementations.\n- `src/utils/` - shared utilities.\n- `src/integrations/` - provider and model integration metadata.\n- `src/entrypoints/` - CLI, MCP, SDK, and generated public types.\n- `src/tasks/` - local, remote, workflow, and monitor task handling.\n- `docs/integrations/` - provider integration guidance.\n- `web/` - documentation website.\n\n## Validation\n\nThe authoritative local pre-push validation contract lives in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation) and must be run before every push to a PR, including follow-up fixes during review. It covers the same command families as `.github/workflows/pr-checks.yml`; CI remains authoritative for clean-runner, supported-Node-matrix, and platform-specific coverage. The lists below are for narrowing checks while you iterate; they do not replace the pre-push contract.\n\nCore checks:\n\n```bash\nbun install\nbun run build\nbun run smoke\nbun run check\nbun run typecheck\nbun run typecheck:type-tests\n```\n\nFocused checks:\n\n```bash\nbun test ./path/to/test-file.test.ts\nbun run test:provider\nbun run test:provider-recommendation\n```\n\nWeb checks, when changes can affect the site:\n\n```bash\nbun run web:typecheck\nbun run web:build\n```\n\nWebsite release notes live on GitHub Releases; do not add a manually maintained release-notes data source to the static site.\n\nDiagnostics and PR hygiene:\n\n```bash\nbun run doctor:runtime\n```\n\nFor PR intent scanning, use the canonical upstream fetch and explicit-ref invocation in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation); the scanner's default `origin/main` base is not portable to fork checkouts.\n\n## Provider Changes\n\nWhen modifying provider behavior:\n\n1. Start with `docs/integrations/overview.md`.\n2. Use the relevant how-to guide under `docs/integrations/how-to/`.\n3. Check existing provider implementations before adding a new pattern.\n4. Test the exact provider/model path you changed when possible.\n5. Avoid breaking third-party providers while fixing first-party behavior.\n\n## Things To Avoid\n\n- Do not change the Node runtime or Bun development workflow without prior maintainer agreement.\n- Do not add new Python code, Python provider paths, or Python dependencies without explicit maintainer approval.\n- Do not introduce dependencies without clear project benefit.\n- Do not skip tests for behavior changes.\n- Do not silently change provider tags; maintainers control them during review.\n- Do not ignore CodeRabbit or maintainer feedback; address it before requesting more review. Before applying an automated review suggestion, verify it does not pull the PR away from its stated scope or intent — decline out-of-scope suggestions with justification, or ask a maintainer when unsure. Never silently ignore findings.\n- Do not push commits with failing, incomplete, or unrun local checks unless an exception in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation) applies. Verify pre-existing failures against the current PR base and document the evidence in the PR; PR-owned failures must still be fixed.\n- Do not submit a PR whose description still contains template placeholder text; fill in every section of the [PR template](.github/pull_request_template.md) for the actual change.\n- Do not surface-patch recurring review findings; repeated fix requests usually indicate a core design issue — investigate and fix the root cause instead of the reported symptom.\n- Do not add a manually maintained release-notes data source to the static site; link to GitHub Releases instead.\n","src/tools/WebSearchTool/README_SEARCH_PROVIDERS.md":"# Web Search Providers\n\nOpenClaude supports multiple search backends through a provider adapter system.\n\n**Exa is the default search backend.** With no configuration at all, web search\nruns on Exa's keyless free tier. Add an `EXA_API_KEY` for higher limits; any other\nbackend you configure with a key is used before the free tier.\n\n## Configure with `/search`\n\nThe `/search` command sets everything below without editing env vars:\n\n| Command | Effect |\n|---|---|\n| `/search` | Pick a backend from a list that shows which ones have keys |\n| `/search <backend>` | Select a backend directly (`auto`, `exa`, `tavily`, `brave`, …); prompts for its key if needed |\n| `/search key [backend]` | Add or replace an API key in a hidden input (defaults to `exa`) |\n| `/search remove-key [backend]` | Remove a saved API key (defaults to `exa`) |\n| `/search status` | Show which backend handles searches and what it falls back to |\n| `/search test [query]` | Run a test search through the configured backends |\n\nSelections are saved as `WEB_SEARCH_PROVIDER` and the backend's key variable in\nthe `env` block of `~/.openclaude.json` (readable only by you — the same file\n`/provider` uses), and take effect immediately. Keys are never accepted on the\ncommand line.\n\nPrecedence: values saved by `/search` override the same variables exported in\nyour shell, and are overridden by an `env` entry for that variable in any\n`settings.json` (user, project, `--settings`, or managed policy) — `/search`\ntells you when that happens. `/search remove-key` only removes keys that\n`/search` saved; a key that comes from your shell or a settings file is reported\ninstead.\n\n## Supported Providers\n\n| Provider | Env Var | Auth Header | Method |\n|---|---|---|---|\n| Custom API | `WEB_SEARCH_API` | Configurable | GET/POST |\n| Ollama | Active Ollama route or `OLLAMA_API_KEY` | Local sign-in / `Authorization: Bearer` | POST |\n| SearXNG | `WEB_PROVIDER=searxng` | — | GET |\n| Google | `WEB_PROVIDER=google` + `GOOGLE_CSE_ID` | *(query param `?key=`)* | GET |\n| Brave (preset) | `WEB_PROVIDER=brave` | `X-Subscription-Token` | GET |\n| Brave (adapter) | `BRAVE_API_KEY` | `X-Subscription-Token` | GET |\n| SerpAPI | `WEB_PROVIDER=serpapi` | `Authorization: Bearer` | GET |\n| Firecrawl | `FIRECRAWL_API_KEY` | Internal | SDK |\n| Tavily | `TAVILY_API_KEY` | `Authorization: Bearer` | POST |\n| Exa (default) | `EXA_API_KEY` | `x-api-key` | POST |\n| Exa free tier | *(none — zero-config default)* | — | POST |\n| AnySearch (explicit only) | `ANYSEARCH_API_KEY` (optional) | `Authorization: Bearer` when set | POST |\n| You.com | `YOU_API_KEY` | `X-API-Key` | GET |\n| Jina | `JINA_API_KEY` | `Authorization: Bearer` | GET |\n| Bing | `BING_API_KEY` | `Ocp-Apim-Subscription-Key` | GET |\n| Mojeek | `MOJEEK_API_KEY` | `Authorization: Bearer` | GET |\n| Linkup | `LINKUP_API_KEY` | `Authorization: Bearer` | POST |\n| DuckDuckGo | *(none — last-resort fallback)* | — | SDK |\n\n## Quick Start\n\n```bash\n# Exa (default) — works with no setup on the free tier; add a key for higher limits\nexport EXA_API_KEY=your-exa-key\n\n# Tavily (fast, RAG-ready)\nexport TAVILY_API_KEY=tvly-your-key\n\n# Brave (independent index, good free tier)\nexport BRAVE_API_KEY=your-brave-key\n\n# Bing\nexport BING_API_KEY=your-bing-key\n\n# Self-hosted SearXNG (free, private)\nexport WEB_PROVIDER=searxng\nexport WEB_SEARCH_API=https://search.example.com/search\n```\n\n## Provider Selection Mode\n\n`WEB_SEARCH_PROVIDER` controls fallback behavior:\n\n| Mode | Behavior |\n|---|---|\n| `auto` (default) | Try all configured providers in order, fall through on failure |\n| `ollama` | Local signed-in Ollama, then hosted Ollama when `OLLAMA_API_KEY` is set; throws if both fail |\n| `tavily` | Tavily only — throws on failure |\n| `exa` | Exa only — keyed API when `EXA_API_KEY` is set, otherwise the free tier; throws on failure |\n| `anysearch` | AnySearch only — anonymous without a key, Bearer authentication with one; throws on failure |\n| `brave` | Brave only — throws on failure |\n| `custom` | Custom API only — throws on failure. **Not in the auto chain** — must be explicitly selected |\n| `firecrawl` | Firecrawl only — throws on failure |\n| `ddg` | DuckDuckGo only — throws on failure |\n| `native` | Anthropic native / Codex only |\n\n**Auto mode priority:** exa → ollama → firecrawl → tavily → you → jina → brave → bing → mojeek → linkup → exa free tier → ddg\n\nEach entry is only tried when it is configured (has its key or route). Keyed Exa\nleads the chain; any other keyed backend still runs before the keyless Exa free\ntier, and DuckDuckGo is the last resort.\n\nIn `auto` mode, providers with built-in web search (Anthropic first-party,\nVertex, Foundry, Codex) keep using it. Set `WEB_SEARCH_PROVIDER=exa` to use Exa\nthere too.\n\n> **Note:** The `custom` provider is excluded from the `auto` chain. It is only used when `WEB_SEARCH_PROVIDER=custom` is explicitly set. This prevents the generic outbound provider from silently becoming the default backend.\n\nAnySearch is also explicit-only: selecting it does not change the existing `auto` chain.\n\n```bash\n# Fail loudly if Tavily is down (don't silently switch backends)\nexport WEB_SEARCH_PROVIDER=tavily\n\n# Try everything, fall through gracefully\nexport WEB_SEARCH_PROVIDER=auto\n```\n\n## Built-in Provider Timeout\n\nBuilt-in adapter providers use a 15s request timeout so `auto` mode can fall through when a backend stalls. Override it with:\n\n```bash\nexport WEB_SEARCH_TIMEOUT_SEC=30\n```\n\nInvalid, fractional, zero, negative, or very large values fall back to 15s. Custom API providers keep their separate `WEB_CUSTOM_TIMEOUT_SEC` setting because self-hosted endpoints may need different budgets.\n\n## Provider Request & Response Formats\n\n### AnySearch\n\nRun `/search anysearch` to use its general web search without a key, or `/search key anysearch` to add an optional key through hidden input. Anonymous access has lower service limits. `/search remove-key anysearch` removes a key saved by `/search`; keys from the shell or `settings.json` remain controlled by those sources.\n\n`ANYSEARCH_MAX_RESULTS` accepts an integer from 1 to 10 (default 10). Invalid values use 10. General search sends `query` and `max_results` to `POST https://api.anysearch.com/v1/search`. Results come from `data.results`; OpenClaude reports HTTP and business-code failures separately from an empty result list. A key, when present, is sent only as `Authorization: Bearer <key>` to that endpoint.\n\n```json\n{\"query\":\"search terms\",\"max_results\":10}\n```\n\n### Ollama\n\nWhen the active route resolves to Ollama, OpenClaude first calls the configured server's signed-in proxy:\n\n```text\nPOST http://localhost:11434/api/experimental/web_search\nContent-Type: application/json\n\n{\"query\":\"search terms\",\"max_results\":10}\n```\n\nIf that request fails and `OLLAMA_API_KEY` is set, it retries the hosted API with `Authorization: Bearer $OLLAMA_API_KEY`. The key is never sent to the local server.\n\n```text\nPOST https://ollama.com/api/web_search\nAuthorization: Bearer $OLLAMA_API_KEY\nContent-Type: application/json\n```\n\nBoth endpoints return structured `results` containing `title`, `url`, and `content`. Use `WEB_SEARCH_PROVIDER=ollama` to select only this local-to-hosted Ollama chain; in `auto` mode, another configured search backend is tried if Ollama search fails.\n\n### Tavily\n\n```bash\nexport TAVILY_API_KEY=tvly-your-key\n```\n\n**Request:**\n```\nPOST https://api.tavily.com/search\nAuthorization: Bearer tvly-your-key\nContent-Type: application/json\n\n{\"query\": \"search terms\", \"max_results\": 10, \"include_answer\": false}\n```\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"content\": \"Full text snippet from the page...\",\n      \"score\": 0.95\n    }\n  ]\n}\n```\n\n### Exa (default)\n\n```bash\nexport EXA_API_KEY=your-exa-key   # optional — the free tier works without it\n```\n\n| Env var | Default | Effect |\n|---|---|---|\n| `EXA_API_KEY` | — | Use the keyed Exa API (higher limits) |\n| `EXA_SEARCH_TYPE` | `auto` | `auto`, `instant`, `fast`, `deep-lite`, `deep`, or `deep-reasoning` (keyed API only; the deep types may need a higher `WEB_SEARCH_TIMEOUT_SEC`) |\n| `EXA_NUM_RESULTS` | `15` | Results per search, 1–50 (keyed API and free tier) |\n| `EXA_FREE_TIER` | on | Set to `0` to never use the keyless free tier |\n\n**Request:**\n```\nPOST https://api.exa.ai/search\nx-api-key: your-exa-key\nContent-Type: application/json\n\n{\"query\": \"search terms\", \"numResults\": 15, \"type\": \"auto\", \"contents\": {\"highlights\": true}}\n```\n\n`allowed_domains` / `blocked_domains` are sent as `includeDomains` / `excludeDomains`.\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"highlights\": [\"The most query-relevant excerpt...\", \"Another excerpt...\"],\n      \"highlightScores\": [0.91, 0.84]\n    }\n  ]\n}\n```\n\n### Exa Free Tier (Zero-Config Default)\n\nNo configuration needed. When no `EXA_API_KEY` is set, OpenClaude calls Exa's\nhosted endpoint, which serves a free tier with a per-IP requests-per-second limit\nand a daily quota. Concurrent searches are spaced automatically. When the limit is\nhit, `auto` mode falls through to DuckDuckGo and skips the free tier for the next\nfew minutes; in `exa` mode (and `/search test`) the error tells you to add a key\nwith `/search key exa`.\n\n**Request** (MCP streamable HTTP, JSON-RPC):\n```\nPOST https://mcp.exa.ai/mcp\nContent-Type: application/json\nAccept: application/json, text/event-stream\nx-exa-source: openclaude\n\n{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\",\n \"params\": {\"name\": \"web_search_exa\",\n            \"arguments\": {\"query\": \"search terms\", \"objective\": \"...\", \"numResults\": 15}}}\n```\n\nThe reply is plain text (`Title:` / `URL:` / `Highlights:` blocks) inside an SSE\n`message` event. `allowed_domains` become `site:` operators in the query, and both\ndomain lists are also enforced on the parsed results.\n\nOpt out (searches never go to Exa without a key):\n\n```bash\nexport EXA_FREE_TIER=0\n```\n\n### You.com\n\n```bash\nexport YOU_API_KEY=your-you-key\n```\n\n**Request:**\n```\nGET https://api.ydc-index.io/v1/search?query=search+terms\nX-API-Key: your-you-key\n```\n\n**Response:**\n```json\n{\n  \"results\": {\n    \"web\": [\n      {\n        \"title\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"snippets\": [\"First snippet from the page...\", \"Second snippet...\"],\n        \"description\": \"Page description\"\n      }\n    ]\n  }\n}\n```\n\n### Jina\n\n```bash\nexport JINA_API_KEY=your-jina-key\n```\n\n**Request:**\n```\nGET https://s.jina.ai/?q=search+terms\nAuthorization: Bearer your-jina-key\nAccept: application/json\n```\n\n**Response:**\n```json\n{\n  \"data\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"description\": \"Snippet from the page...\"\n    }\n  ]\n}\n```\n\n### Bing\n\n```bash\nexport BING_API_KEY=your-bing-key\n```\n\n**Request:**\n```\nGET https://api.bing.microsoft.com/v7.0/search?q=search+terms&count=10\nOcp-Apim-Subscription-Key: your-bing-key\n```\n\n**Response:**\n```json\n{\n  \"webPages\": {\n    \"value\": [\n      {\n        \"name\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"snippet\": \"A short excerpt from the page...\",\n        \"displayUrl\": \"example.com/page\"\n      }\n    ]\n  }\n}\n```\n\n### Mojeek\n\n```bash\nexport MOJEEK_API_KEY=your-mojeek-key\n```\n\n**Request:**\n```\nGET https://www.mojeek.com/search?q=search+terms&fmt=json\nAuthorization: Bearer your-mojeek-key\n```\n\n**Response:**\n```json\n{\n  \"response\": {\n    \"results\": [\n      {\n        \"title\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"snippet\": \"Excerpt from the page...\"\n      }\n    ]\n  }\n}\n```\n\n### Linkup\n\n```bash\nexport LINKUP_API_KEY=your-linkup-key\n```\n\n**Request:**\n```\nPOST https://api.linkup.so/v1/search\nAuthorization: Bearer your-linkup-key\nContent-Type: application/json\n\n{\"q\": \"search terms\", \"search_type\": \"standard\"}\n```\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"name\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"snippet\": \"A short description of the result...\"\n    }\n  ]\n}\n```\n\n### SearXNG (Built-in Preset)\n\n```bash\nexport WEB_PROVIDER=searxng\nexport WEB_SEARCH_API=https://search.example.com/search\n```\n\n**Request:**\n```\nGET https://search.example.com/search?q=search+terms\n```\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"content\": \"Snippet from the page...\",\n      \"engine\": \"google\"\n    }\n  ]\n}\n```\n\n### Google Custom Search (Built-in Preset)\n\n> ⚠️ **Sunset 2027-01-01.** Google has announced the Custom Search JSON API\n> will be discontinued and is closed to new customers. Use Brave/Tavily/Exa\n> for new setups.\n\n```bash\nexport WEB_PROVIDER=google\nexport WEB_KEY=your-google-api-key\nexport GOOGLE_CSE_ID=your-programmable-search-engine-id\n```\n\n`GOOGLE_CSE_ID` is the `cx` value from your Programmable Search Engine — both\nthe API key and the engine ID are required.\n\n**Request:**\n```\nGET https://www.googleapis.com/customsearch/v1?q=search+terms&key=your-google-api-key&cx=your-engine-id\n```\n\n**Response:**\n```json\n{\n  \"items\": [\n    {\n      \"title\": \"Result Title\",\n      \"link\": \"https://example.com/page\",\n      \"snippet\": \"A short excerpt...\",\n      \"displayLink\": \"example.com\"\n    }\n  ]\n}\n```\n\n### Brave (First-Class Adapter)\n\nThe recommended way to use Brave — auto-detected, joins the auto fallback chain.\n\n```bash\nexport BRAVE_API_KEY=your-brave-key\n```\n\n**Request:**\n```\nGET https://api.search.brave.com/res/v1/web/search?q=search+terms&count=15\nX-Subscription-Token: your-brave-key\n```\n\n### Brave (Built-in Preset, alternative)\n\nFor users who prefer the generic preset path. Functionally equivalent to the\nadapter above; either works.\n\n```bash\nexport WEB_PROVIDER=brave\nexport WEB_KEY=your-brave-key\n```\n\n**Response:**\n```json\n{\n  \"web\": {\n    \"results\": [\n      {\n        \"title\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"description\": \"Page description...\"\n      }\n    ]\n  }\n}\n```\n\n### SerpAPI (Built-in Preset)\n\n```bash\nexport WEB_PROVIDER=serpapi\nexport WEB_KEY=your-serpapi-key\n```\n\n**Request:**\n```\nGET https://serpapi.com/search.json?q=search+terms\nAuthorization: Bearer your-serpapi-key\n```\n\n**Response:**\n```json\n{\n  \"organic_results\": [\n    {\n      \"title\": \"Result Title\",\n      \"link\": \"https://example.com/page\",\n      \"snippet\": \"A short excerpt...\",\n      \"displayed_link\": \"example.com\"\n    }\n  ]\n}\n```\n\n### DuckDuckGo (Last-Resort Fallback)\n\nNo configuration needed. Uses the `duck-duck-scrape` npm package. In `auto` mode it\nruns only after every configured backend (including the Exa free tier) has failed.\n\n```bash\n# Set as explicit-only backend\nexport WEB_SEARCH_PROVIDER=ddg\n```\n\n---\n\n## Custom API Configuration\n\n### Standard GET\n\n```\nGET https://api.example.com/search?q=hello\n```\n\n```bash\nexport WEB_SEARCH_API=https://api.example.com/search\nexport WEB_QUERY_PARAM=q\n```\n\n### Query in URL Path\n\n```\nGET https://api.example.com/v2/search/hello\n```\n\n```bash\nexport WEB_URL_TEMPLATE=https://api.example.com/v2/search/{query}\n```\n\n### POST with Custom Body\n\n```\nPOST https://api.example.com/v1/query\nContent-Type: application/json\n\n{\"input\": {\"text\": \"hello\"}}\n```\n\n```bash\nexport WEB_SEARCH_API=https://api.example.com/v1/query\nexport WEB_METHOD=POST\nexport WEB_BODY_TEMPLATE='{\"input\":{\"text\":\"{query}\"}}'\n```\n\n### Extra Static Params\n\n```bash\nexport WEB_PARAMS='{\"lang\":\"en\",\"count\":\"10\"}'\n```\n\n## Auth\n\nAPI keys are sent in HTTP headers, **never** in query strings.\n\n```bash\n# Default: Authorization: Bearer <key>\nexport WEB_KEY=your-key\n\n# Custom header\nexport WEB_AUTH_HEADER=X-Api-Key\nexport WEB_AUTH_SCHEME=\"\"\n\n# Extra headers\nexport WEB_HEADERS=\"X-Tenant: acme; Accept: application/json\"\n```\n\n## Response Parsing\n\nThe tool auto-detects many response formats:\n\n```jsonc\n{ \"results\": [{ \"title\": \"...\", \"url\": \"...\" }] }     // flat array\n{ \"items\": [{ \"title\": \"...\", \"link\": \"...\" }] }       // Google-style\n{ \"results\": { \"engine\": [{ \"title\": \"...\", \"url\": \"...\" }] } }  // nested map\n[{ \"title\": \"...\", \"url\": \"...\" }]                      // bare array\n```\n\nField name aliases: `title`/`headline`/`name`, `url`/`link`/`href`, `description`/`snippet`/`content`\n\nFor deeply nested responses:\n```bash\nexport WEB_JSON_PATH=response.payload.results\n```\n\n## Retry\n\nFailed custom-provider requests (network errors, 5xx) are retried once after 500ms. Client errors (4xx) are not retried. Custom requests have a default 120s timeout.\n\n## Custom Provider Security Guardrails\n\nThe custom provider enforces the following guardrails by default:\n\n| Guardrail | Default | Override |\n|-----------|---------|----------|\n| HTTPS-only | ✅ | `WEB_CUSTOM_ALLOW_HTTP=true` |\n| Block private IPs / localhost | ✅ | `WEB_CUSTOM_ALLOW_PRIVATE=true` |\n| Header allowlist | ✅ | `WEB_CUSTOM_ALLOW_ARBITRARY_HEADERS=true` |\n| Max POST body | 300 KB | `WEB_CUSTOM_MAX_BODY_KB=<kb>` |\n| Request timeout | 120s | `WEB_CUSTOM_TIMEOUT_SEC=<seconds>` |\n| Audit log (one-time warning) | ✅ | — |\n\n### Self-hosted SearXNG example\n\n```bash\nexport WEB_PROVIDER=searxng\nexport WEB_SEARCH_API=https://search.mydomain.com/search\nexport WEB_CUSTOM_ALLOW_PRIVATE=true   # needed if SearXNG is on a private IP\n```\n\n### Header allowlist\n\nBy default only these headers are permitted:\n`accept`, `accept-encoding`, `accept-language`, `authorization`, `cache-control`, `content-type`, `if-modified-since`, `if-none-match`, `ocp-apim-subscription-key`, `user-agent`, `x-api-key`, `x-subscription-token`, `x-tenant-id`\n\n## Adding a Provider\n\n1. Create `providers/myprovider.ts`:\n\n```typescript\nimport type { SearchInput, SearchProvider } from './types.js'\nimport { applyDomainFilters, type ProviderOutput } from './types.js'\n\nexport const myProvider: SearchProvider = {\n  name: 'myprovider',\n  isConfigured() { return Boolean(process.env.MYPROVIDER_API_KEY) },\n  async search(input: SearchInput): Promise<ProviderOutput> {\n    const start = performance.now()\n    // ... call API, map to SearchHit[] ...\n    return {\n      hits: applyDomainFilters(hits, input),\n      providerName: 'myprovider',\n      durationSeconds: (performance.now() - start) / 1000,\n    }\n  },\n}\n```\n\n2. Register in `providers/index.ts` — add import and push to `ALL_PROVIDERS`.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/Gitlawb/openclaude/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md - AI Agent Coding Guide\n\nThis guide is for AI coding agents working in the OpenClaude repository. Read it before changing code, and also follow [CONTRIBUTING.md](CONTRIBUTING.md) for contributor policy, PR expectations, review follow-up, and project scope.\n\n## Project Snapshot\n\nOpenClaude is a coding-agent CLI for cloud and local model providers. It supports OpenAI-compatible APIs, Anthropic, Gemini, DeepSeek, Ollama, MCP, local backends, slash commands, tools, agents, and a React/Ink terminal UI.\n\nThe installed CLI runs on Node.js `>=22.0.0`. Bun is used for source builds, scripts, dependency management, and tests.\n\n## Work Style\n\n- Keep changes focused on one problem.\n- Prefer existing patterns in the file or nearby module.\n- Avoid unrelated formatting, renames, dependency changes, or broad rewrites.\n- Add or update tests when behavior changes.\n- Update docs when setup, commands, provider behavior, or user-facing behavior changes.\n- For new features, larger refactors, dependencies, or runtime changes, follow the issue-first guidance in [CONTRIBUTING.md](CONTRIBUTING.md).\n- Keep PR branches current with `main` using the synchronization and guarded-push workflow in [CONTRIBUTING.md § Keep Your Branch Current](CONTRIBUTING.md#keep-your-branch-current). Rebase whenever resuming work or pushing follow-up fixes, but never overwrite remote PR-head updates with an unguarded force-push.\n- Run the authoritative local pre-push validation contract defined in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation) before every push to a PR, not just the first one. CI adds clean-runner and supported-Node-matrix coverage that is not practical to reproduce in one local shell.\n\n## Stack And Conventions\n\n- TypeScript with strict mode and ESM imports.\n- React + Ink for terminal UI.\n- Bun lockfile and Bun scripts for development workflows.\n- Node runtime for the built CLI.\n\nCommon libraries and patterns:\n\n- `chalk` for terminal color.\n- `commander` for CLI argument parsing.\n- `execa` for child processes.\n- Existing service, provider, settings, permission, and UI patterns over new abstractions.\n\n## Repository Map\n\n- `src/commands/` - slash and CLI command implementations.\n- `src/components/` - React/Ink UI components.\n- `src/services/` - API, MCP, OAuth, wiki, voice, and other service integrations.\n- `src/tools/` - tool implementations.\n- `src/utils/` - shared utilities.\n- `src/integrations/` - provider and model integration metadata.\n- `src/entrypoints/` - CLI, MCP, SDK, and generated public types.\n- `src/tasks/` - local, remote, workflow, and monitor task handling.\n- `docs/integrations/` - provider integration guidance.\n- `web/` - documentation website.\n\n## Validation\n\nThe authoritative local pre-push validation contract lives in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation) and must be run before every push to a PR, including follow-up fixes during review. It covers the same command families as `.github/workflows/pr-checks.yml`; CI remains authoritative for clean-runner, supported-Node-matrix, and platform-specific coverage. The lists below are for narrowing checks while you iterate; they do not replace the pre-push contract.\n\nCore checks:\n\n```bash\nbun install\nbun run build\nbun run smoke\nbun run check\nbun run typecheck\nbun run typecheck:type-tests\n```\n\nFocused checks:\n\n```bash\nbun test ./path/to/test-file.test.ts\nbun run test:provider\nbun run test:provider-recommendation\n```\n\nWeb checks, when changes can affect the site:\n\n```bash\nbun run web:typecheck\nbun run web:build\n```\n\nWebsite release notes live on GitHub Releases; do not add a manually maintained release-notes data source to the static site.\n\nDiagnostics and PR hygiene:\n\n```bash\nbun run doctor:runtime\n```\n\nFor PR intent scanning, use the canonical upstream fetch and explicit-ref invocation in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation); the scanner's default `origin/main` base is not portable to fork checkouts.\n\n## Provider Changes\n\nWhen modifying provider behavior:\n\n1. Start with `docs/integrations/overview.md`.\n2. Use the relevant how-to guide under `docs/integrations/how-to/`.\n3. Check existing provider implementations before adding a new pattern.\n4. Test the exact provider/model path you changed when possible.\n5. Avoid breaking third-party providers while fixing first-party behavior.\n\n## Things To Avoid\n\n- Do not change the Node runtime or Bun development workflow without prior maintainer agreement.\n- Do not add new Python code, Python provider paths, or Python dependencies without explicit maintainer approval.\n- Do not introduce dependencies without clear project benefit.\n- Do not skip tests for behavior changes.\n- Do not silently change provider tags; maintainers control them during review.\n- Do not ignore CodeRabbit or maintainer feedback; address it before requesting more review. Before applying an automated review suggestion, verify it does not pull the PR away from its stated scope or intent — decline out-of-scope suggestions with justification, or ask a maintainer when unsure. Never silently ignore findings.\n- Do not push commits with failing, incomplete, or unrun local checks unless an exception in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation) applies. Verify pre-existing failures against the current PR base and document the evidence in the PR; PR-owned failures must still be fixed.\n- Do not submit a PR whose description still contains template placeholder text; fill in every section of the [PR template](.github/pull_request_template.md) for the actual change.\n- Do not surface-patch recurring review findings; repeated fix requests usually indicate a core design issue — investigate and fix the root cause instead of the reported symptom.\n- Do not add a manually maintained release-notes data source to the static site; link to GitHub Releases instead.\n","isInternal":false,"tokens":1293,"sizeBytes":5874},{"name":"README_SEARCH_PROVIDERS.md","path":"src/tools/WebSearchTool/README_SEARCH_PROVIDERS.md","rawUrl":"https://raw.githubusercontent.com/Gitlawb/openclaude/HEAD/src/tools/WebSearchTool/README_SEARCH_PROVIDERS.md","title":"WebSearchTool Skill","category":"anthropic-skill","format":"markdown","content":"# Web Search Providers\n\nOpenClaude supports multiple search backends through a provider adapter system.\n\n**Exa is the default search backend.** With no configuration at all, web search\nruns on Exa's keyless free tier. Add an `EXA_API_KEY` for higher limits; any other\nbackend you configure with a key is used before the free tier.\n\n## Configure with `/search`\n\nThe `/search` command sets everything below without editing env vars:\n\n| Command | Effect |\n|---|---|\n| `/search` | Pick a backend from a list that shows which ones have keys |\n| `/search <backend>` | Select a backend directly (`auto`, `exa`, `tavily`, `brave`, …); prompts for its key if needed |\n| `/search key [backend]` | Add or replace an API key in a hidden input (defaults to `exa`) |\n| `/search remove-key [backend]` | Remove a saved API key (defaults to `exa`) |\n| `/search status` | Show which backend handles searches and what it falls back to |\n| `/search test [query]` | Run a test search through the configured backends |\n\nSelections are saved as `WEB_SEARCH_PROVIDER` and the backend's key variable in\nthe `env` block of `~/.openclaude.json` (readable only by you — the same file\n`/provider` uses), and take effect immediately. Keys are never accepted on the\ncommand line.\n\nPrecedence: values saved by `/search` override the same variables exported in\nyour shell, and are overridden by an `env` entry for that variable in any\n`settings.json` (user, project, `--settings`, or managed policy) — `/search`\ntells you when that happens. `/search remove-key` only removes keys that\n`/search` saved; a key that comes from your shell or a settings file is reported\ninstead.\n\n## Supported Providers\n\n| Provider | Env Var | Auth Header | Method |\n|---|---|---|---|\n| Custom API | `WEB_SEARCH_API` | Configurable | GET/POST |\n| Ollama | Active Ollama route or `OLLAMA_API_KEY` | Local sign-in / `Authorization: Bearer` | POST |\n| SearXNG | `WEB_PROVIDER=searxng` | — | GET |\n| Google | `WEB_PROVIDER=google` + `GOOGLE_CSE_ID` | *(query param `?key=`)* | GET |\n| Brave (preset) | `WEB_PROVIDER=brave` | `X-Subscription-Token` | GET |\n| Brave (adapter) | `BRAVE_API_KEY` | `X-Subscription-Token` | GET |\n| SerpAPI | `WEB_PROVIDER=serpapi` | `Authorization: Bearer` | GET |\n| Firecrawl | `FIRECRAWL_API_KEY` | Internal | SDK |\n| Tavily | `TAVILY_API_KEY` | `Authorization: Bearer` | POST |\n| Exa (default) | `EXA_API_KEY` | `x-api-key` | POST |\n| Exa free tier | *(none — zero-config default)* | — | POST |\n| AnySearch (explicit only) | `ANYSEARCH_API_KEY` (optional) | `Authorization: Bearer` when set | POST |\n| You.com | `YOU_API_KEY` | `X-API-Key` | GET |\n| Jina | `JINA_API_KEY` | `Authorization: Bearer` | GET |\n| Bing | `BING_API_KEY` | `Ocp-Apim-Subscription-Key` | GET |\n| Mojeek | `MOJEEK_API_KEY` | `Authorization: Bearer` | GET |\n| Linkup | `LINKUP_API_KEY` | `Authorization: Bearer` | POST |\n| DuckDuckGo | *(none — last-resort fallback)* | — | SDK |\n\n## Quick Start\n\n```bash\n# Exa (default) — works with no setup on the free tier; add a key for higher limits\nexport EXA_API_KEY=your-exa-key\n\n# Tavily (fast, RAG-ready)\nexport TAVILY_API_KEY=tvly-your-key\n\n# Brave (independent index, good free tier)\nexport BRAVE_API_KEY=your-brave-key\n\n# Bing\nexport BING_API_KEY=your-bing-key\n\n# Self-hosted SearXNG (free, private)\nexport WEB_PROVIDER=searxng\nexport WEB_SEARCH_API=https://search.example.com/search\n```\n\n## Provider Selection Mode\n\n`WEB_SEARCH_PROVIDER` controls fallback behavior:\n\n| Mode | Behavior |\n|---|---|\n| `auto` (default) | Try all configured providers in order, fall through on failure |\n| `ollama` | Local signed-in Ollama, then hosted Ollama when `OLLAMA_API_KEY` is set; throws if both fail |\n| `tavily` | Tavily only — throws on failure |\n| `exa` | Exa only — keyed API when `EXA_API_KEY` is set, otherwise the free tier; throws on failure |\n| `anysearch` | AnySearch only — anonymous without a key, Bearer authentication with one; throws on failure |\n| `brave` | Brave only — throws on failure |\n| `custom` | Custom API only — throws on failure. **Not in the auto chain** — must be explicitly selected |\n| `firecrawl` | Firecrawl only — throws on failure |\n| `ddg` | DuckDuckGo only — throws on failure |\n| `native` | Anthropic native / Codex only |\n\n**Auto mode priority:** exa → ollama → firecrawl → tavily → you → jina → brave → bing → mojeek → linkup → exa free tier → ddg\n\nEach entry is only tried when it is configured (has its key or route). Keyed Exa\nleads the chain; any other keyed backend still runs before the keyless Exa free\ntier, and DuckDuckGo is the last resort.\n\nIn `auto` mode, providers with built-in web search (Anthropic first-party,\nVertex, Foundry, Codex) keep using it. Set `WEB_SEARCH_PROVIDER=exa` to use Exa\nthere too.\n\n> **Note:** The `custom` provider is excluded from the `auto` chain. It is only used when `WEB_SEARCH_PROVIDER=custom` is explicitly set. This prevents the generic outbound provider from silently becoming the default backend.\n\nAnySearch is also explicit-only: selecting it does not change the existing `auto` chain.\n\n```bash\n# Fail loudly if Tavily is down (don't silently switch backends)\nexport WEB_SEARCH_PROVIDER=tavily\n\n# Try everything, fall through gracefully\nexport WEB_SEARCH_PROVIDER=auto\n```\n\n## Built-in Provider Timeout\n\nBuilt-in adapter providers use a 15s request timeout so `auto` mode can fall through when a backend stalls. Override it with:\n\n```bash\nexport WEB_SEARCH_TIMEOUT_SEC=30\n```\n\nInvalid, fractional, zero, negative, or very large values fall back to 15s. Custom API providers keep their separate `WEB_CUSTOM_TIMEOUT_SEC` setting because self-hosted endpoints may need different budgets.\n\n## Provider Request & Response Formats\n\n### AnySearch\n\nRun `/search anysearch` to use its general web search without a key, or `/search key anysearch` to add an optional key through hidden input. Anonymous access has lower service limits. `/search remove-key anysearch` removes a key saved by `/search`; keys from the shell or `settings.json` remain controlled by those sources.\n\n`ANYSEARCH_MAX_RESULTS` accepts an integer from 1 to 10 (default 10). Invalid values use 10. General search sends `query` and `max_results` to `POST https://api.anysearch.com/v1/search`. Results come from `data.results`; OpenClaude reports HTTP and business-code failures separately from an empty result list. A key, when present, is sent only as `Authorization: Bearer <key>` to that endpoint.\n\n```json\n{\"query\":\"search terms\",\"max_results\":10}\n```\n\n### Ollama\n\nWhen the active route resolves to Ollama, OpenClaude first calls the configured server's signed-in proxy:\n\n```text\nPOST http://localhost:11434/api/experimental/web_search\nContent-Type: application/json\n\n{\"query\":\"search terms\",\"max_results\":10}\n```\n\nIf that request fails and `OLLAMA_API_KEY` is set, it retries the hosted API with `Authorization: Bearer $OLLAMA_API_KEY`. The key is never sent to the local server.\n\n```text\nPOST https://ollama.com/api/web_search\nAuthorization: Bearer $OLLAMA_API_KEY\nContent-Type: application/json\n```\n\nBoth endpoints return structured `results` containing `title`, `url`, and `content`. Use `WEB_SEARCH_PROVIDER=ollama` to select only this local-to-hosted Ollama chain; in `auto` mode, another configured search backend is tried if Ollama search fails.\n\n### Tavily\n\n```bash\nexport TAVILY_API_KEY=tvly-your-key\n```\n\n**Request:**\n```\nPOST https://api.tavily.com/search\nAuthorization: Bearer tvly-your-key\nContent-Type: application/json\n\n{\"query\": \"search terms\", \"max_results\": 10, \"include_answer\": false}\n```\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"content\": \"Full text snippet from the page...\",\n      \"score\": 0.95\n    }\n  ]\n}\n```\n\n### Exa (default)\n\n```bash\nexport EXA_API_KEY=your-exa-key   # optional — the free tier works without it\n```\n\n| Env var | Default | Effect |\n|---|---|---|\n| `EXA_API_KEY` | — | Use the keyed Exa API (higher limits) |\n| `EXA_SEARCH_TYPE` | `auto` | `auto`, `instant`, `fast`, `deep-lite`, `deep`, or `deep-reasoning` (keyed API only; the deep types may need a higher `WEB_SEARCH_TIMEOUT_SEC`) |\n| `EXA_NUM_RESULTS` | `15` | Results per search, 1–50 (keyed API and free tier) |\n| `EXA_FREE_TIER` | on | Set to `0` to never use the keyless free tier |\n\n**Request:**\n```\nPOST https://api.exa.ai/search\nx-api-key: your-exa-key\nContent-Type: application/json\n\n{\"query\": \"search terms\", \"numResults\": 15, \"type\": \"auto\", \"contents\": {\"highlights\": true}}\n```\n\n`allowed_domains` / `blocked_domains` are sent as `includeDomains` / `excludeDomains`.\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"highlights\": [\"The most query-relevant excerpt...\", \"Another excerpt...\"],\n      \"highlightScores\": [0.91, 0.84]\n    }\n  ]\n}\n```\n\n### Exa Free Tier (Zero-Config Default)\n\nNo configuration needed. When no `EXA_API_KEY` is set, OpenClaude calls Exa's\nhosted endpoint, which serves a free tier with a per-IP requests-per-second limit\nand a daily quota. Concurrent searches are spaced automatically. When the limit is\nhit, `auto` mode falls through to DuckDuckGo and skips the free tier for the next\nfew minutes; in `exa` mode (and `/search test`) the error tells you to add a key\nwith `/search key exa`.\n\n**Request** (MCP streamable HTTP, JSON-RPC):\n```\nPOST https://mcp.exa.ai/mcp\nContent-Type: application/json\nAccept: application/json, text/event-stream\nx-exa-source: openclaude\n\n{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\",\n \"params\": {\"name\": \"web_search_exa\",\n            \"arguments\": {\"query\": \"search terms\", \"objective\": \"...\", \"numResults\": 15}}}\n```\n\nThe reply is plain text (`Title:` / `URL:` / `Highlights:` blocks) inside an SSE\n`message` event. `allowed_domains` become `site:` operators in the query, and both\ndomain lists are also enforced on the parsed results.\n\nOpt out (searches never go to Exa without a key):\n\n```bash\nexport EXA_FREE_TIER=0\n```\n\n### You.com\n\n```bash\nexport YOU_API_KEY=your-you-key\n```\n\n**Request:**\n```\nGET https://api.ydc-index.io/v1/search?query=search+terms\nX-API-Key: your-you-key\n```\n\n**Response:**\n```json\n{\n  \"results\": {\n    \"web\": [\n      {\n        \"title\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"snippets\": [\"First snippet from the page...\", \"Second snippet...\"],\n        \"description\": \"Page description\"\n      }\n    ]\n  }\n}\n```\n\n### Jina\n\n```bash\nexport JINA_API_KEY=your-jina-key\n```\n\n**Request:**\n```\nGET https://s.jina.ai/?q=search+terms\nAuthorization: Bearer your-jina-key\nAccept: application/json\n```\n\n**Response:**\n```json\n{\n  \"data\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"description\": \"Snippet from the page...\"\n    }\n  ]\n}\n```\n\n### Bing\n\n```bash\nexport BING_API_KEY=your-bing-key\n```\n\n**Request:**\n```\nGET https://api.bing.microsoft.com/v7.0/search?q=search+terms&count=10\nOcp-Apim-Subscription-Key: your-bing-key\n```\n\n**Response:**\n```json\n{\n  \"webPages\": {\n    \"value\": [\n      {\n        \"name\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"snippet\": \"A short excerpt from the page...\",\n        \"displayUrl\": \"example.com/page\"\n      }\n    ]\n  }\n}\n```\n\n### Mojeek\n\n```bash\nexport MOJEEK_API_KEY=your-mojeek-key\n```\n\n**Request:**\n```\nGET https://www.mojeek.com/search?q=search+terms&fmt=json\nAuthorization: Bearer your-mojeek-key\n```\n\n**Response:**\n```json\n{\n  \"response\": {\n    \"results\": [\n      {\n        \"title\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"snippet\": \"Excerpt from the page...\"\n      }\n    ]\n  }\n}\n```\n\n### Linkup\n\n```bash\nexport LINKUP_API_KEY=your-linkup-key\n```\n\n**Request:**\n```\nPOST https://api.linkup.so/v1/search\nAuthorization: Bearer your-linkup-key\nContent-Type: application/json\n\n{\"q\": \"search terms\", \"search_type\": \"standard\"}\n```\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"name\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"snippet\": \"A short description of the result...\"\n    }\n  ]\n}\n```\n\n### SearXNG (Built-in Preset)\n\n```bash\nexport WEB_PROVIDER=searxng\nexport WEB_SEARCH_API=https://search.example.com/search\n```\n\n**Request:**\n```\nGET https://search.example.com/search?q=search+terms\n```\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"content\": \"Snippet from the page...\",\n      \"engine\": \"google\"\n    }\n  ]\n}\n```\n\n### Google Custom Search (Built-in Preset)\n\n> ⚠️ **Sunset 2027-01-01.** Google has announced the Custom Search JSON API\n> will be discontinued and is closed to new customers. Use Brave/Tavily/Exa\n> for new setups.\n\n```bash\nexport WEB_PROVIDER=google\nexport WEB_KEY=your-google-api-key\nexport GOOGLE_CSE_ID=your-programmable-search-engine-id\n```\n\n`GOOGLE_CSE_ID` is the `cx` value from your Programmable Search Engine — both\nthe API key and the engine ID are required.\n\n**Request:**\n```\nGET https://www.googleapis.com/customsearch/v1?q=search+terms&key=your-google-api-key&cx=your-engine-id\n```\n\n**Response:**\n```json\n{\n  \"items\": [\n    {\n      \"title\": \"Result Title\",\n      \"link\": \"https://example.com/page\",\n      \"snippet\": \"A short excerpt...\",\n      \"displayLink\": \"example.com\"\n    }\n  ]\n}\n```\n\n### Brave (First-Class Adapter)\n\nThe recommended way to use Brave — auto-detected, joins the auto fallback chain.\n\n```bash\nexport BRAVE_API_KEY=your-brave-key\n```\n\n**Request:**\n```\nGET https://api.search.brave.com/res/v1/web/search?q=search+terms&count=15\nX-Subscription-Token: your-brave-key\n```\n\n### Brave (Built-in Preset, alternative)\n\nFor users who prefer the generic preset path. Functionally equivalent to the\nadapter above; either works.\n\n```bash\nexport WEB_PROVIDER=brave\nexport WEB_KEY=your-brave-key\n```\n\n**Response:**\n```json\n{\n  \"web\": {\n    \"results\": [\n      {\n        \"title\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"description\": \"Page description...\"\n      }\n    ]\n  }\n}\n```\n\n### SerpAPI (Built-in Preset)\n\n```bash\nexport WEB_PROVIDER=serpapi\nexport WEB_KEY=your-serpapi-key\n```\n\n**Request:**\n```\nGET https://serpapi.com/search.json?q=search+terms\nAuthorization: Bearer your-serpapi-key\n```\n\n**Response:**\n```json\n{\n  \"organic_results\": [\n    {\n      \"title\": \"Result Title\",\n      \"link\": \"https://example.com/page\",\n      \"snippet\": \"A short excerpt...\",\n      \"displayed_link\": \"example.com\"\n    }\n  ]\n}\n```\n\n### DuckDuckGo (Last-Resort Fallback)\n\nNo configuration needed. Uses the `duck-duck-scrape` npm package. In `auto` mode it\nruns only after every configured backend (including the Exa free tier) has failed.\n\n```bash\n# Set as explicit-only backend\nexport WEB_SEARCH_PROVIDER=ddg\n```\n\n---\n\n## Custom API Configuration\n\n### Standard GET\n\n```\nGET https://api.example.com/search?q=hello\n```\n\n```bash\nexport WEB_SEARCH_API=https://api.example.com/search\nexport WEB_QUERY_PARAM=q\n```\n\n### Query in URL Path\n\n```\nGET https://api.example.com/v2/search/hello\n```\n\n```bash\nexport WEB_URL_TEMPLATE=https://api.example.com/v2/search/{query}\n```\n\n### POST with Custom Body\n\n```\nPOST https://api.example.com/v1/query\nContent-Type: application/json\n\n{\"input\": {\"text\": \"hello\"}}\n```\n\n```bash\nexport WEB_SEARCH_API=https://api.example.com/v1/query\nexport WEB_METHOD=POST\nexport WEB_BODY_TEMPLATE='{\"input\":{\"text\":\"{query}\"}}'\n```\n\n### Extra Static Params\n\n```bash\nexport WEB_PARAMS='{\"lang\":\"en\",\"count\":\"10\"}'\n```\n\n## Auth\n\nAPI keys are sent in HTTP headers, **never** in query strings.\n\n```bash\n# Default: Authorization: Bearer <key>\nexport WEB_KEY=your-key\n\n# Custom header\nexport WEB_AUTH_HEADER=X-Api-Key\nexport WEB_AUTH_SCHEME=\"\"\n\n# Extra headers\nexport WEB_HEADERS=\"X-Tenant: acme; Accept: application/json\"\n```\n\n## Response Parsing\n\nThe tool auto-detects many response formats:\n\n```jsonc\n{ \"results\": [{ \"title\": \"...\", \"url\": \"...\" }] }     // flat array\n{ \"items\": [{ \"title\": \"...\", \"link\": \"...\" }] }       // Google-style\n{ \"results\": { \"engine\": [{ \"title\": \"...\", \"url\": \"...\" }] } }  // nested map\n[{ \"title\": \"...\", \"url\": \"...\" }]                      // bare array\n```\n\nField name aliases: `title`/`headline`/`name`, `url`/`link`/`href`, `description`/`snippet`/`content`\n\nFor deeply nested responses:\n```bash\nexport WEB_JSON_PATH=response.payload.results\n```\n\n## Retry\n\nFailed custom-provider requests (network errors, 5xx) are retried once after 500ms. Client errors (4xx) are not retried. Custom requests have a default 120s timeout.\n\n## Custom Provider Security Guardrails\n\nThe custom provider enforces the following guardrails by default:\n\n| Guardrail | Default | Override |\n|-----------|---------|----------|\n| HTTPS-only | ✅ | `WEB_CUSTOM_ALLOW_HTTP=true` |\n| Block private IPs / localhost | ✅ | `WEB_CUSTOM_ALLOW_PRIVATE=true` |\n| Header allowlist | ✅ | `WEB_CUSTOM_ALLOW_ARBITRARY_HEADERS=true` |\n| Max POST body | 300 KB | `WEB_CUSTOM_MAX_BODY_KB=<kb>` |\n| Request timeout | 120s | `WEB_CUSTOM_TIMEOUT_SEC=<seconds>` |\n| Audit log (one-time warning) | ✅ | — |\n\n### Self-hosted SearXNG example\n\n```bash\nexport WEB_PROVIDER=searxng\nexport WEB_SEARCH_API=https://search.mydomain.com/search\nexport WEB_CUSTOM_ALLOW_PRIVATE=true   # needed if SearXNG is on a private IP\n```\n\n### Header allowlist\n\nBy default only these headers are permitted:\n`accept`, `accept-encoding`, `accept-language`, `authorization`, `cache-control`, `content-type`, `if-modified-since`, `if-none-match`, `ocp-apim-subscription-key`, `user-agent`, `x-api-key`, `x-subscription-token`, `x-tenant-id`\n\n## Adding a Provider\n\n1. Create `providers/myprovider.ts`:\n\n```typescript\nimport type { SearchInput, SearchProvider } from './types.js'\nimport { applyDomainFilters, type ProviderOutput } from './types.js'\n\nexport const myProvider: SearchProvider = {\n  name: 'myprovider',\n  isConfigured() { return Boolean(process.env.MYPROVIDER_API_KEY) },\n  async search(input: SearchInput): Promise<ProviderOutput> {\n    const start = performance.now()\n    // ... call API, map to SearchHit[] ...\n    return {\n      hits: applyDomainFilters(hits, input),\n      providerName: 'myprovider',\n      durationSeconds: (performance.now() - start) / 1000,\n    }\n  },\n}\n```\n\n2. Register in `providers/index.ts` — add import and push to `ALL_PROVIDERS`.\n","isInternal":false,"tokens":4959,"sizeBytes":18324}],"systemPromptSnippet":"<yakaai_skills repo=\"Gitlawb/openclaude\">\n<!-- File: AGENTS.md (Tokens: ~1293 | Category: root-instruction) -->\n# AGENTS.md - AI Agent Coding Guide\n\nThis guide is for AI coding agents working in the OpenClaude repository. Read it before changing code, and also follow [CONTRIBUTING.md](CONTRIBUTING.md) for contributor policy, PR expectations, review follow-up, and project scope.\n\n## Project Snapshot\n\nOpenClaude is a coding-agent CLI for cloud and local model providers. It supports OpenAI-compatible APIs, Anthropic, Gemini, DeepSeek, Ollama, MCP, local backends, slash commands, tools, agents, and a React/Ink terminal UI.\n\nThe installed CLI runs on Node.js `>=22.0.0`. Bun is used for source builds, scripts, dependency management, and tests.\n\n## Work Style\n\n- Keep changes focused on one problem.\n- Prefer existing patterns in the file or nearby module.\n- Avoid unrelated formatting, renames, dependency changes, or broad rewrites.\n- Add or update tests when behavior changes.\n- Update docs when setup, commands, provider behavior, or user-facing behavior changes.\n- For new features, larger refactors, dependencies, or runtime changes, follow the issue-first guidance in [CONTRIBUTING.md](CONTRIBUTING.md).\n- Keep PR branches current with `main` using the synchronization and guarded-push workflow in [CONTRIBUTING.md § Keep Your Branch Current](CONTRIBUTING.md#keep-your-branch-current). Rebase whenever resuming work or pushing follow-up fixes, but never overwrite remote PR-head updates with an unguarded force-push.\n- Run the authoritative local pre-push validation contract defined in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation) before every push to a PR, not just the first one. CI adds clean-runner and supported-Node-matrix coverage that is not practical to reproduce in one local shell.\n\n## Stack And Conventions\n\n- TypeScript with strict mode and ESM imports.\n- React + Ink for terminal UI.\n- Bun lockfile and Bun scripts for development workflows.\n- Node runtime for the built CLI.\n\nCommon libraries and patterns:\n\n- `chalk` for terminal color.\n- `commander` for CLI argument parsing.\n- `execa` for child processes.\n- Existing service, provider, settings, permission, and UI patterns over new abstractions.\n\n## Repository Map\n\n- `src/commands/` - slash and CLI command implementations.\n- `src/components/` - React/Ink UI components.\n- `src/services/` - API, MCP, OAuth, wiki, voice, and other service integrations.\n- `src/tools/` - tool implementations.\n- `src/utils/` - shared utilities.\n- `src/integrations/` - provider and model integration metadata.\n- `src/entrypoints/` - CLI, MCP, SDK, and generated public types.\n- `src/tasks/` - local, remote, workflow, and monitor task handling.\n- `docs/integrations/` - provider integration guidance.\n- `web/` - documentation website.\n\n## Validation\n\nThe authoritative local pre-push validation contract lives in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation) and must be run before every push to a PR, including follow-up fixes during review. It covers the same command families as `.github/workflows/pr-checks.yml`; CI remains authoritative for clean-runner, supported-Node-matrix, and platform-specific coverage. The lists below are for narrowing checks while you iterate; they do not replace the pre-push contract.\n\nCore checks:\n\n```bash\nbun install\nbun run build\nbun run smoke\nbun run check\nbun run typecheck\nbun run typecheck:type-tests\n```\n\nFocused checks:\n\n```bash\nbun test ./path/to/test-file.test.ts\nbun run test:provider\nbun run test:provider-recommendation\n```\n\nWeb checks, when changes can affect the site:\n\n```bash\nbun run web:typecheck\nbun run web:build\n```\n\nWebsite release notes live on GitHub Releases; do not add a manually maintained release-notes data source to the static site.\n\nDiagnostics and PR hygiene:\n\n```bash\nbun run doctor:runtime\n```\n\nFor PR intent scanning, use the canonical upstream fetch and explicit-ref invocation in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation); the scanner's default `origin/main` base is not portable to fork checkouts.\n\n## Provider Changes\n\nWhen modifying provider behavior:\n\n1. Start with `docs/integrations/overview.md`.\n2. Use the relevant how-to guide under `docs/integrations/how-to/`.\n3. Check existing provider implementations before adding a new pattern.\n4. Test the exact provider/model path you changed when possible.\n5. Avoid breaking third-party providers while fixing first-party behavior.\n\n## Things To Avoid\n\n- Do not change the Node runtime or Bun development workflow without prior maintainer agreement.\n- Do not add new Python code, Python provider paths, or Python dependencies without explicit maintainer approval.\n- Do not introduce dependencies without clear project benefit.\n- Do not skip tests for behavior changes.\n- Do not silently change provider tags; maintainers control them during review.\n- Do not ignore CodeRabbit or maintainer feedback; address it before requesting more review. Before applying an automated review suggestion, verify it does not pull the PR away from its stated scope or intent — decline out-of-scope suggestions with justification, or ask a maintainer when unsure. Never silently ignore findings.\n- Do not push commits with failing, incomplete, or unrun local checks unless an exception in [CONTRIBUTING.md § Validation](CONTRIBUTING.md#validation) applies. Verify pre-existing failures against the current PR base and document the evidence in the PR; PR-owned failures must still be fixed.\n- Do not submit a PR whose description still contains template placeholder text; fill in every section of the [PR template](.github/pull_request_template.md) for the actual change.\n- Do not surface-patch recurring review findings; repeated fix requests usually indicate a core design issue — investigate and fix the root cause instead of the reported symptom.\n- Do not add a manually maintained release-notes data source to the static site; link to GitHub Releases instead.\n\n<!-- File: src/tools/WebSearchTool/README_SEARCH_PROVIDERS.md (Tokens: ~4959 | Category: anthropic-skill) -->\n# Web Search Providers\n\nOpenClaude supports multiple search backends through a provider adapter system.\n\n**Exa is the default search backend.** With no configuration at all, web search\nruns on Exa's keyless free tier. Add an `EXA_API_KEY` for higher limits; any other\nbackend you configure with a key is used before the free tier.\n\n## Configure with `/search`\n\nThe `/search` command sets everything below without editing env vars:\n\n| Command | Effect |\n|---|---|\n| `/search` | Pick a backend from a list that shows which ones have keys |\n| `/search <backend>` | Select a backend directly (`auto`, `exa`, `tavily`, `brave`, …); prompts for its key if needed |\n| `/search key [backend]` | Add or replace an API key in a hidden input (defaults to `exa`) |\n| `/search remove-key [backend]` | Remove a saved API key (defaults to `exa`) |\n| `/search status` | Show which backend handles searches and what it falls back to |\n| `/search test [query]` | Run a test search through the configured backends |\n\nSelections are saved as `WEB_SEARCH_PROVIDER` and the backend's key variable in\nthe `env` block of `~/.openclaude.json` (readable only by you — the same file\n`/provider` uses), and take effect immediately. Keys are never accepted on the\ncommand line.\n\nPrecedence: values saved by `/search` override the same variables exported in\nyour shell, and are overridden by an `env` entry for that variable in any\n`settings.json` (user, project, `--settings`, or managed policy) — `/search`\ntells you when that happens. `/search remove-key` only removes keys that\n`/search` saved; a key that comes from your shell or a settings file is reported\ninstead.\n\n## Supported Providers\n\n| Provider | Env Var | Auth Header | Method |\n|---|---|---|---|\n| Custom API | `WEB_SEARCH_API` | Configurable | GET/POST |\n| Ollama | Active Ollama route or `OLLAMA_API_KEY` | Local sign-in / `Authorization: Bearer` | POST |\n| SearXNG | `WEB_PROVIDER=searxng` | — | GET |\n| Google | `WEB_PROVIDER=google` + `GOOGLE_CSE_ID` | *(query param `?key=`)* | GET |\n| Brave (preset) | `WEB_PROVIDER=brave` | `X-Subscription-Token` | GET |\n| Brave (adapter) | `BRAVE_API_KEY` | `X-Subscription-Token` | GET |\n| SerpAPI | `WEB_PROVIDER=serpapi` | `Authorization: Bearer` | GET |\n| Firecrawl | `FIRECRAWL_API_KEY` | Internal | SDK |\n| Tavily | `TAVILY_API_KEY` | `Authorization: Bearer` | POST |\n| Exa (default) | `EXA_API_KEY` | `x-api-key` | POST |\n| Exa free tier | *(none — zero-config default)* | — | POST |\n| AnySearch (explicit only) | `ANYSEARCH_API_KEY` (optional) | `Authorization: Bearer` when set | POST |\n| You.com | `YOU_API_KEY` | `X-API-Key` | GET |\n| Jina | `JINA_API_KEY` | `Authorization: Bearer` | GET |\n| Bing | `BING_API_KEY` | `Ocp-Apim-Subscription-Key` | GET |\n| Mojeek | `MOJEEK_API_KEY` | `Authorization: Bearer` | GET |\n| Linkup | `LINKUP_API_KEY` | `Authorization: Bearer` | POST |\n| DuckDuckGo | *(none — last-resort fallback)* | — | SDK |\n\n## Quick Start\n\n```bash\n# Exa (default) — works with no setup on the free tier; add a key for higher limits\nexport EXA_API_KEY=your-exa-key\n\n# Tavily (fast, RAG-ready)\nexport TAVILY_API_KEY=tvly-your-key\n\n# Brave (independent index, good free tier)\nexport BRAVE_API_KEY=your-brave-key\n\n# Bing\nexport BING_API_KEY=your-bing-key\n\n# Self-hosted SearXNG (free, private)\nexport WEB_PROVIDER=searxng\nexport WEB_SEARCH_API=https://search.example.com/search\n```\n\n## Provider Selection Mode\n\n`WEB_SEARCH_PROVIDER` controls fallback behavior:\n\n| Mode | Behavior |\n|---|---|\n| `auto` (default) | Try all configured providers in order, fall through on failure |\n| `ollama` | Local signed-in Ollama, then hosted Ollama when `OLLAMA_API_KEY` is set; throws if both fail |\n| `tavily` | Tavily only — throws on failure |\n| `exa` | Exa only — keyed API when `EXA_API_KEY` is set, otherwise the free tier; throws on failure |\n| `anysearch` | AnySearch only — anonymous without a key, Bearer authentication with one; throws on failure |\n| `brave` | Brave only — throws on failure |\n| `custom` | Custom API only — throws on failure. **Not in the auto chain** — must be explicitly selected |\n| `firecrawl` | Firecrawl only — throws on failure |\n| `ddg` | DuckDuckGo only — throws on failure |\n| `native` | Anthropic native / Codex only |\n\n**Auto mode priority:** exa → ollama → firecrawl → tavily → you → jina → brave → bing → mojeek → linkup → exa free tier → ddg\n\nEach entry is only tried when it is configured (has its key or route). Keyed Exa\nleads the chain; any other keyed backend still runs before the keyless Exa free\ntier, and DuckDuckGo is the last resort.\n\nIn `auto` mode, providers with built-in web search (Anthropic first-party,\nVertex, Foundry, Codex) keep using it. Set `WEB_SEARCH_PROVIDER=exa` to use Exa\nthere too.\n\n> **Note:** The `custom` provider is excluded from the `auto` chain. It is only used when `WEB_SEARCH_PROVIDER=custom` is explicitly set. This prevents the generic outbound provider from silently becoming the default backend.\n\nAnySearch is also explicit-only: selecting it does not change the existing `auto` chain.\n\n```bash\n# Fail loudly if Tavily is down (don't silently switch backends)\nexport WEB_SEARCH_PROVIDER=tavily\n\n# Try everything, fall through gracefully\nexport WEB_SEARCH_PROVIDER=auto\n```\n\n## Built-in Provider Timeout\n\nBuilt-in adapter providers use a 15s request timeout so `auto` mode can fall through when a backend stalls. Override it with:\n\n```bash\nexport WEB_SEARCH_TIMEOUT_SEC=30\n```\n\nInvalid, fractional, zero, negative, or very large values fall back to 15s. Custom API providers keep their separate `WEB_CUSTOM_TIMEOUT_SEC` setting because self-hosted endpoints may need different budgets.\n\n## Provider Request & Response Formats\n\n### AnySearch\n\nRun `/search anysearch` to use its general web search without a key, or `/search key anysearch` to add an optional key through hidden input. Anonymous access has lower service limits. `/search remove-key anysearch` removes a key saved by `/search`; keys from the shell or `settings.json` remain controlled by those sources.\n\n`ANYSEARCH_MAX_RESULTS` accepts an integer from 1 to 10 (default 10). Invalid values use 10. General search sends `query` and `max_results` to `POST https://api.anysearch.com/v1/search`. Results come from `data.results`; OpenClaude reports HTTP and business-code failures separately from an empty result list. A key, when present, is sent only as `Authorization: Bearer <key>` to that endpoint.\n\n```json\n{\"query\":\"search terms\",\"max_results\":10}\n```\n\n### Ollama\n\nWhen the active route resolves to Ollama, OpenClaude first calls the configured server's signed-in proxy:\n\n```text\nPOST http://localhost:11434/api/experimental/web_search\nContent-Type: application/json\n\n{\"query\":\"search terms\",\"max_results\":10}\n```\n\nIf that request fails and `OLLAMA_API_KEY` is set, it retries the hosted API with `Authorization: Bearer $OLLAMA_API_KEY`. The key is never sent to the local server.\n\n```text\nPOST https://ollama.com/api/web_search\nAuthorization: Bearer $OLLAMA_API_KEY\nContent-Type: application/json\n```\n\nBoth endpoints return structured `results` containing `title`, `url`, and `content`. Use `WEB_SEARCH_PROVIDER=ollama` to select only this local-to-hosted Ollama chain; in `auto` mode, another configured search backend is tried if Ollama search fails.\n\n### Tavily\n\n```bash\nexport TAVILY_API_KEY=tvly-your-key\n```\n\n**Request:**\n```\nPOST https://api.tavily.com/search\nAuthorization: Bearer tvly-your-key\nContent-Type: application/json\n\n{\"query\": \"search terms\", \"max_results\": 10, \"include_answer\": false}\n```\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"content\": \"Full text snippet from the page...\",\n      \"score\": 0.95\n    }\n  ]\n}\n```\n\n### Exa (default)\n\n```bash\nexport EXA_API_KEY=your-exa-key   # optional — the free tier works without it\n```\n\n| Env var | Default | Effect |\n|---|---|---|\n| `EXA_API_KEY` | — | Use the keyed Exa API (higher limits) |\n| `EXA_SEARCH_TYPE` | `auto` | `auto`, `instant`, `fast`, `deep-lite`, `deep`, or `deep-reasoning` (keyed API only; the deep types may need a higher `WEB_SEARCH_TIMEOUT_SEC`) |\n| `EXA_NUM_RESULTS` | `15` | Results per search, 1–50 (keyed API and free tier) |\n| `EXA_FREE_TIER` | on | Set to `0` to never use the keyless free tier |\n\n**Request:**\n```\nPOST https://api.exa.ai/search\nx-api-key: your-exa-key\nContent-Type: application/json\n\n{\"query\": \"search terms\", \"numResults\": 15, \"type\": \"auto\", \"contents\": {\"highlights\": true}}\n```\n\n`allowed_domains` / `blocked_domains` are sent as `includeDomains` / `excludeDomains`.\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"highlights\": [\"The most query-relevant excerpt...\", \"Another excerpt...\"],\n      \"highlightScores\": [0.91, 0.84]\n    }\n  ]\n}\n```\n\n### Exa Free Tier (Zero-Config Default)\n\nNo configuration needed. When no `EXA_API_KEY` is set, OpenClaude calls Exa's\nhosted endpoint, which serves a free tier with a per-IP requests-per-second limit\nand a daily quota. Concurrent searches are spaced automatically. When the limit is\nhit, `auto` mode falls through to DuckDuckGo and skips the free tier for the next\nfew minutes; in `exa` mode (and `/search test`) the error tells you to add a key\nwith `/search key exa`.\n\n**Request** (MCP streamable HTTP, JSON-RPC):\n```\nPOST https://mcp.exa.ai/mcp\nContent-Type: application/json\nAccept: application/json, text/event-stream\nx-exa-source: openclaude\n\n{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\",\n \"params\": {\"name\": \"web_search_exa\",\n            \"arguments\": {\"query\": \"search terms\", \"objective\": \"...\", \"numResults\": 15}}}\n```\n\nThe reply is plain text (`Title:` / `URL:` / `Highlights:` blocks) inside an SSE\n`message` event. `allowed_domains` become `site:` operators in the query, and both\ndomain lists are also enforced on the parsed results.\n\nOpt out (searches never go to Exa without a key):\n\n```bash\nexport EXA_FREE_TIER=0\n```\n\n### You.com\n\n```bash\nexport YOU_API_KEY=your-you-key\n```\n\n**Request:**\n```\nGET https://api.ydc-index.io/v1/search?query=search+terms\nX-API-Key: your-you-key\n```\n\n**Response:**\n```json\n{\n  \"results\": {\n    \"web\": [\n      {\n        \"title\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"snippets\": [\"First snippet from the page...\", \"Second snippet...\"],\n        \"description\": \"Page description\"\n      }\n    ]\n  }\n}\n```\n\n### Jina\n\n```bash\nexport JINA_API_KEY=your-jina-key\n```\n\n**Request:**\n```\nGET https://s.jina.ai/?q=search+terms\nAuthorization: Bearer your-jina-key\nAccept: application/json\n```\n\n**Response:**\n```json\n{\n  \"data\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"description\": \"Snippet from the page...\"\n    }\n  ]\n}\n```\n\n### Bing\n\n```bash\nexport BING_API_KEY=your-bing-key\n```\n\n**Request:**\n```\nGET https://api.bing.microsoft.com/v7.0/search?q=search+terms&count=10\nOcp-Apim-Subscription-Key: your-bing-key\n```\n\n**Response:**\n```json\n{\n  \"webPages\": {\n    \"value\": [\n      {\n        \"name\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"snippet\": \"A short excerpt from the page...\",\n        \"displayUrl\": \"example.com/page\"\n      }\n    ]\n  }\n}\n```\n\n### Mojeek\n\n```bash\nexport MOJEEK_API_KEY=your-mojeek-key\n```\n\n**Request:**\n```\nGET https://www.mojeek.com/search?q=search+terms&fmt=json\nAuthorization: Bearer your-mojeek-key\n```\n\n**Response:**\n```json\n{\n  \"response\": {\n    \"results\": [\n      {\n        \"title\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"snippet\": \"Excerpt from the page...\"\n      }\n    ]\n  }\n}\n```\n\n### Linkup\n\n```bash\nexport LINKUP_API_KEY=your-linkup-key\n```\n\n**Request:**\n```\nPOST https://api.linkup.so/v1/search\nAuthorization: Bearer your-linkup-key\nContent-Type: application/json\n\n{\"q\": \"search terms\", \"search_type\": \"standard\"}\n```\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"name\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"snippet\": \"A short description of the result...\"\n    }\n  ]\n}\n```\n\n### SearXNG (Built-in Preset)\n\n```bash\nexport WEB_PROVIDER=searxng\nexport WEB_SEARCH_API=https://search.example.com/search\n```\n\n**Request:**\n```\nGET https://search.example.com/search?q=search+terms\n```\n\n**Response:**\n```json\n{\n  \"results\": [\n    {\n      \"title\": \"Result Title\",\n      \"url\": \"https://example.com/page\",\n      \"content\": \"Snippet from the page...\",\n      \"engine\": \"google\"\n    }\n  ]\n}\n```\n\n### Google Custom Search (Built-in Preset)\n\n> ⚠️ **Sunset 2027-01-01.** Google has announced the Custom Search JSON API\n> will be discontinued and is closed to new customers. Use Brave/Tavily/Exa\n> for new setups.\n\n```bash\nexport WEB_PROVIDER=google\nexport WEB_KEY=your-google-api-key\nexport GOOGLE_CSE_ID=your-programmable-search-engine-id\n```\n\n`GOOGLE_CSE_ID` is the `cx` value from your Programmable Search Engine — both\nthe API key and the engine ID are required.\n\n**Request:**\n```\nGET https://www.googleapis.com/customsearch/v1?q=search+terms&key=your-google-api-key&cx=your-engine-id\n```\n\n**Response:**\n```json\n{\n  \"items\": [\n    {\n      \"title\": \"Result Title\",\n      \"link\": \"https://example.com/page\",\n      \"snippet\": \"A short excerpt...\",\n      \"displayLink\": \"example.com\"\n    }\n  ]\n}\n```\n\n### Brave (First-Class Adapter)\n\nThe recommended way to use Brave — auto-detected, joins the auto fallback chain.\n\n```bash\nexport BRAVE_API_KEY=your-brave-key\n```\n\n**Request:**\n```\nGET https://api.search.brave.com/res/v1/web/search?q=search+terms&count=15\nX-Subscription-Token: your-brave-key\n```\n\n### Brave (Built-in Preset, alternative)\n\nFor users who prefer the generic preset path. Functionally equivalent to the\nadapter above; either works.\n\n```bash\nexport WEB_PROVIDER=brave\nexport WEB_KEY=your-brave-key\n```\n\n**Response:**\n```json\n{\n  \"web\": {\n    \"results\": [\n      {\n        \"title\": \"Result Title\",\n        \"url\": \"https://example.com/page\",\n        \"description\": \"Page description...\"\n      }\n    ]\n  }\n}\n```\n\n### SerpAPI (Built-in Preset)\n\n```bash\nexport WEB_PROVIDER=serpapi\nexport WEB_KEY=your-serpapi-key\n```\n\n**Request:**\n```\nGET https://serpapi.com/search.json?q=search+terms\nAuthorization: Bearer your-serpapi-key\n```\n\n**Response:**\n```json\n{\n  \"organic_results\": [\n    {\n      \"title\": \"Result Title\",\n      \"link\": \"https://example.com/page\",\n      \"snippet\": \"A short excerpt...\",\n      \"displayed_link\": \"example.com\"\n    }\n  ]\n}\n```\n\n### DuckDuckGo (Last-Resort Fallback)\n\nNo configuration needed. Uses the `duck-duck-scrape` npm package. In `auto` mode it\nruns only after every configured backend (including the Exa free tier) has failed.\n\n```bash\n# Set as explicit-only backend\nexport WEB_SEARCH_PROVIDER=ddg\n```\n\n---\n\n## Custom API Configuration\n\n### Standard GET\n\n```\nGET https://api.example.com/search?q=hello\n```\n\n```bash\nexport WEB_SEARCH_API=https://api.example.com/search\nexport WEB_QUERY_PARAM=q\n```\n\n### Query in URL Path\n\n```\nGET https://api.example.com/v2/search/hello\n```\n\n```bash\nexport WEB_URL_TEMPLATE=https://api.example.com/v2/search/{query}\n```\n\n### POST with Custom Body\n\n```\nPOST https://api.example.com/v1/query\nContent-Type: application/json\n\n{\"input\": {\"text\": \"hello\"}}\n```\n\n```bash\nexport WEB_SEARCH_API=https://api.example.com/v1/query\nexport WEB_METHOD=POST\nexport WEB_BODY_TEMPLATE='{\"input\":{\"text\":\"{query}\"}}'\n```\n\n### Extra Static Params\n\n```bash\nexport WEB_PARAMS='{\"lang\":\"en\",\"count\":\"10\"}'\n```\n\n## Auth\n\nAPI keys are sent in HTTP headers, **never** in query strings.\n\n```bash\n# Default: Authorization: Bearer <key>\nexport WEB_KEY=your-key\n\n# Custom header\nexport WEB_AUTH_HEADER=X-Api-Key\nexport WEB_AUTH_SCHEME=\"\"\n\n# Extra headers\nexport WEB_HEADERS=\"X-Tenant: acme; Accept: application/json\"\n```\n\n## Response Parsing\n\nThe tool auto-detects many response formats:\n\n```jsonc\n{ \"results\": [{ \"title\": \"...\", \"url\": \"...\" }] }     // flat array\n{ \"items\": [{ \"title\": \"...\", \"link\": \"...\" }] }       // Google-style\n{ \"results\": { \"engine\": [{ \"title\": \"...\", \"url\": \"...\" }] } }  // nested map\n[{ \"title\": \"...\", \"url\": \"...\" }]                      // bare array\n```\n\nField name aliases: `title`/`headline`/`name`, `url`/`link`/`href`, `description`/`snippet`/`content`\n\nFor deeply nested responses:\n```bash\nexport WEB_JSON_PATH=response.payload.results\n```\n\n## Retry\n\nFailed custom-provider requests (network errors, 5xx) are retried once after 500ms. Client errors (4xx) are not retried. Custom requests have a default 120s timeout.\n\n## Custom Provider Security Guardrails\n\nThe custom provider enforces the following guardrails by default:\n\n| Guardrail | Default | Override |\n|-----------|---------|----------|\n| HTTPS-only | ✅ | `WEB_CUSTOM_ALLOW_HTTP=true` |\n| Block private IPs / localhost | ✅ | `WEB_CUSTOM_ALLOW_PRIVATE=true` |\n| Header allowlist | ✅ | `WEB_CUSTOM_ALLOW_ARBITRARY_HEADERS=true` |\n| Max POST body | 300 KB | `WEB_CUSTOM_MAX_BODY_KB=<kb>` |\n| Request timeout | 120s | `WEB_CUSTOM_TIMEOUT_SEC=<seconds>` |\n| Audit log (one-time warning) | ✅ | — |\n\n### Self-hosted SearXNG example\n\n```bash\nexport WEB_PROVIDER=searxng\nexport WEB_SEARCH_API=https://search.mydomain.com/search\nexport WEB_CUSTOM_ALLOW_PRIVATE=true   # needed if SearXNG is on a private IP\n```\n\n### Header allowlist\n\nBy default only these headers are permitted:\n`accept`, `accept-encoding`, `accept-language`, `authorization`, `cache-control`, `content-type`, `if-modified-since`, `if-none-match`, `ocp-apim-subscription-key`, `user-agent`, `x-api-key`, `x-subscription-token`, `x-tenant-id`\n\n## Adding a Provider\n\n1. Create `providers/myprovider.ts`:\n\n```typescript\nimport type { SearchInput, SearchProvider } from './types.js'\nimport { applyDomainFilters, type ProviderOutput } from './types.js'\n\nexport const myProvider: SearchProvider = {\n  name: 'myprovider',\n  isConfigured() { return Boolean(process.env.MYPROVIDER_API_KEY) },\n  async search(input: SearchInput): Promise<ProviderOutput> {\n    const start = performance.now()\n    // ... call API, map to SearchHit[] ...\n    return {\n      hits: applyDomainFilters(hits, input),\n      providerName: 'myprovider',\n      durationSeconds: (performance.now() - start) / 1000,\n    }\n  },\n}\n```\n\n2. Register in `providers/index.ts` — add import and push to `ALL_PROVIDERS`.\n</yakaai_skills>"}