{"_id":"@balababa/dsh-web-search","_rev":"2-8e4f3acd2e009fa685c71ded4e7aa950","name":"@balababa/dsh-web-search","dist-tags":{"latest":"0.2.1"},"versions":{"0.2.0":{"name":"@balababa/dsh-web-search","version":"0.2.0","keywords":["dsh","dsh-plugin","deepseek-harness","web-search","web-fetch","tavily","firecrawl"],"author":{"name":"CBalaa"},"license":"MIT","_id":"@balababa/dsh-web-search@0.2.0","maintainers":[{"name":"balababa","email":"cao18272922035@163.com"}],"homepage":"https://github.com/CBalaa/dsh-web-search#readme","bugs":{"url":"https://github.com/CBalaa/dsh-web-search/issues"},"dsh":{"bundle":{"patch":"./cordis.patch.yml"},"client":{"inject":["@deepseek-ai/dsh-client-locale","@deepseek-ai/dsh-client-ui-slots","@deepseek-ai/dsh-client-ui-settings","@deepseek-ai/dsh-api-remotes"],"platform":"web"}},"dist":{"shasum":"8aba72b9d1ee035aa101b4218f29c9b05c3ad897","tarball":"https://registry.npmjs.org/@balababa/dsh-web-search/-/dsh-web-search-0.2.0.tgz","fileCount":15,"integrity":"sha512-GRj3IyzonDH5+D/4F7pu8OMeuPCLQuhVw/4Kb5hofKXOCrQS96Oz91qJ4u44hdgfWfMLsxdTw4gtVyv2NIj6Lw==","signatures":[{"sig":"MEQCICOcdBmPg3JT6NzqeJutCGhhhCixRcjtH6icD8xzTNK+AiBbnl/aBwuDcjwz7MZDPW8vuujgGrGM3mULUsyBUFpI5g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":135369},"main":"lib/index.js","type":"module","engines":{"node":">=20"},"exports":{".":"./lib/index.js","./client":"./lib/client.js","./package.json":"./package.json"},"gitHead":"1e1381aa486c51c25742da29968f49cb8c4219bb","scripts":{"test":"node --test","build":"tsdown","prepublishOnly":"npm run build"},"_npmUser":{"name":"balababa","email":"cao18272922035@163.com"},"repository":{"url":"git+https://github.com/CBalaa/dsh-web-search.git","type":"git"},"_npmVersion":"11.9.0","description":"DSH web plugin: multi-vendor web_search / web_fetch providers for the ctx.web seam (Tavily, Firecrawl, extensible) — works with any LLM provider.","directories":{},"_nodeVersion":"24.14.0","dependencies":{"@deepseek-ai/schemastery":"^3.18.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^18.2.0","tsdown":"^0.22.2","react-dom":"^18.2.0","playwright-core":"^1.62.1","@deepseek-ai/dsh-credentials":"^0.1.2-rc.1","@deepseek-ai/dsh-settings-file":"^0.1.2-rc.1"},"peerDependencies":{"@deepseek-ai/dsh-web":"^0.1.2-rc.1","@deepseek-ai/dsh-settings":"^0.1.2-rc.1","@deepseek-ai/dsh-credentials":"^0.1.2-rc.1"},"peerDependenciesMeta":{"@deepseek-ai/dsh-settings":{"optional":true},"@deepseek-ai/dsh-credentials":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/dsh-web-search_0.2.0_1788773919167_0.42968582159097446","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@balababa/dsh-web-search","version":"0.2.1","description":"DSH web plugin: multi-vendor web_search / web_fetch providers for the ctx.web seam (Tavily, Firecrawl, extensible) — works with any LLM provider.","type":"module","main":"lib/index.js","exports":{".":"./lib/index.js","./client":"./lib/client.js","./package.json":"./package.json"},"dsh":{"bundle":{"patch":"./cordis.patch.yml"},"client":{"inject":["@deepseek-ai/dsh-client-locale","@deepseek-ai/dsh-client-ui-slots","@deepseek-ai/dsh-client-ui-settings","@deepseek-ai/dsh-api-remotes"],"platform":"web"}},"engines":{"node":">=20"},"scripts":{"build":"tsdown","test":"node --test","prepublishOnly":"npm run build"},"peerDependencies":{"@deepseek-ai/dsh-credentials":"^0.1.2-rc.1","@deepseek-ai/dsh-settings":"^0.1.2-rc.1","@deepseek-ai/dsh-web":"^0.1.2-rc.1"},"peerDependenciesMeta":{"@deepseek-ai/dsh-credentials":{"optional":true},"@deepseek-ai/dsh-settings":{"optional":true}},"dependencies":{"@deepseek-ai/schemastery":"^3.18.2"},"devDependencies":{"@deepseek-ai/dsh-credentials":"^0.1.2-rc.1","@deepseek-ai/dsh-settings-file":"^0.1.2-rc.1","playwright-core":"^1.62.1","react":"^18.2.0","react-dom":"^18.2.0","tsdown":"^0.22.2"},"publishConfig":{"access":"public"},"keywords":["dsh","dsh-plugin","deepseek-harness","web-search","web-fetch","tavily","firecrawl"],"license":"MIT","repository":{"type":"git","url":"git+https://github.com/CBalaa/dsh-web-search.git"},"homepage":"https://github.com/CBalaa/dsh-web-search#readme","bugs":{"url":"https://github.com/CBalaa/dsh-web-search/issues"},"author":{"name":"CBalaa"},"gitHead":"e413ebb075a5f1957404fcb90c7354c06226a5ba","_id":"@balababa/dsh-web-search@0.2.1","_nodeVersion":"24.14.0","_npmVersion":"11.9.0","dist":{"integrity":"sha512-DodwIulpmoTh82Q+rY+ise+S5IE95wq7rxE49VnPq0vBXM3VEs9jL1zt80WOSMf0eARupUannjHfThuv6VYuUg==","shasum":"e841013a24b5fbf9da626878a978bbae47b9b6f7","tarball":"https://registry.npmjs.org/@balababa/dsh-web-search/-/dsh-web-search-0.2.1.tgz","fileCount":15,"unpackedSize":135414,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIGtwvf9SlDgvM08h4p74Q+2+W1q6hPb7lsFFP/nFjjK0AiBgfDV+/ot46apkUyMqhWYoOY7jEk0cTsG640ljDtaoAQ=="}]},"_npmUser":{"name":"balababa","email":"cao18272922035@163.com"},"directories":{},"maintainers":[{"name":"balababa","email":"cao18272922035@163.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dsh-web-search_0.2.1_1788788612874_0.9299116111588626"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-07T09:38:38.966Z","modified":"2026-09-07T13:43:33.259Z","0.2.0":"2026-09-07T09:38:39.302Z","0.2.1":"2026-09-07T13:43:33.019Z"},"bugs":{"url":"https://github.com/CBalaa/dsh-web-search/issues"},"author":{"name":"CBalaa"},"license":"MIT","homepage":"https://github.com/CBalaa/dsh-web-search#readme","keywords":["dsh","dsh-plugin","deepseek-harness","web-search","web-fetch","tavily","firecrawl"],"repository":{"type":"git","url":"git+https://github.com/CBalaa/dsh-web-search.git"},"description":"DSH web plugin: multi-vendor web_search / web_fetch providers for the ctx.web seam (Tavily, Firecrawl, extensible) — works with any LLM provider.","maintainers":[{"name":"balababa","email":"cao18272922035@163.com"}],"readme":"# dsh-web-search\n\nDSH web plugin: **multi-vendor `web_search` / `web_fetch` backends** for the\n`ctx.web` seam. Works with any LLM provider — you no longer need DeepSeek's\nnative search for `web_search` to work.\n\n| Vendor    | Config name | Search provider      | Fetch provider                 | Key lookup (in order)                                                        |\n| --------- | ----------- | -------------------- | ------------------------------ | ---------------------------------------------------------------------------- |\n| Tavily    | `tavily`    | `POST /search`       | `POST /extract` (clean text)   | `apiKey` → credentials service(`apiKeyEnv`) → `$TAVILY_API_KEY` → Tavily CLI config file |\n| Firecrawl | `firecrawl` | `POST /v2/search`    | `POST /v2/scrape` (markdown)   | `apiKey` → credentials service(`apiKeyEnv`) → `$FIRECRAWL_API_KEY`            |\n| *yours*   | —           | see [Adding a vendor](#adding-a-vendor) | —                 | —                                                            |\n\nThe plugin registers under the **fixed** ids `dsh-web-search` (search) and\n`dsh-web-fetch` (fetch), so switching vendors never touches the web-seam row —\none config line does it.\n\n## Install\n\nPublished to npm — install straight from the DSH CLI:\n\n```sh\n# latest (or pin a version: @balababa/dsh-web-search@0.2.0)\ndsh plugin --profile web add @balababa/dsh-web-search\n```\n\n`dsh plugin add` forwards its argument to `pnpm add` inside the profile\ndirectory, then reconciles `dsh.profile.bundles`: because this package declares\n`dsh.bundle.patch`, `dsh-web-search` is appended to the bundle stack\nautomatically — no manual `cordis.patch.yml` edit to mount it.\n\nRestart the web profile to take effect (`dsh web`, or `dsh --profile web`).\n\n> **Prerequisites** — Node.js >= 20. The plugin's only runtime npm dependency is\n> `@deepseek-ai/schemastery`. The `@deepseek-ai/dsh-web`, `@deepseek-ai/dsh-settings`,\n> and `@deepseek-ai/dsh-credentials` peers — plus `react` / `react-dom` /\n> `@deepseek-ai/cordis` — are provided by the DSH web runtime, no separate\n> install. The settings and credentials peers are optional: without them the\n> plugin still works composition-only / env-only.\n\n### From a local checkout (development)\n\nRun these from the checkout root:\n\n```sh\nnpm install        # tsdown / react / playwright-core (dev) + schemastery (runtime)\nnpm run build      # bundles src/client/settings-card.tsx → lib/client.js\ndsh plugin --profile web add file:.\n```\n\nThe repo ships a pre-built `lib/client.js`, so installation works without a\nbuild; but **after editing `src/client/`, re-run `npm run build`** or the\nsettings card will still serve the old bundle. The `file:.` spec is anchored\nto your invoking directory, so run it from inside the checkout.\n\n### Verify\n\n```sh\ndsh web --port 9000\n```\n\nAfter restarting, the plugin is live: open **Settings → Plugins → Plugin\nconfiguration** to see the \"Web search\" card, or run a `web_search` /\n`web_fetch` to confirm. The bundle patch mounts the plugin and points\n`searchProvider` at it (`fetchProvider` stays on the built-in anonymous `http`\nprovider until you opt in — free, no vendor credits).\n\n## Configure (two layers)\n\nConfiguration resolves through two layers, **both hot-applied**:\n\n1. **Composition** — the plugin row's cordis config in your profile's\n   `cordis.patch.yml`, applied at boot.\n2. **Settings** (optional) — the `web-search:` section of the settings\n   document (`settings.yaml`), layered **over** the composition config\n   (`schema defaults < composition < user document`; user edits win). Editing\n   the document — or saving the **Settings → Plugins → Plugin configuration →\n   Web search** card — takes effect **without a restart**: vendor switches\n   dispose/re-register the fixed-id providers, and key/endpoint/parameter\n   edits reach the very next request.\n\n```yaml\n# your profile's cordis.patch.yml\n- id: web-search\n  config:\n    search: tavily          # vendor for web_search\n    fetch: tavily           # vendor for web_fetch  (\"off\" = none)\n    providers:\n      tavily: {}            # key from credentials / $TAVILY_API_KEY / Tavily CLI config file\n      # firecrawl:\n      #   apiKey: 'fc-...'  # or store/export FIRECRAWL_API_KEY\n\n- id: web\n  config:\n    searchProvider: dsh-web-search\n    fetchProvider: dsh-web-fetch   # or keep the built-in \"http\"\n```\n\nThe equivalent in the settings document (`settings.yaml`), saved hot:\n\n```yaml\nweb-search:\n  search: firecrawl          # switches web_search to Firecrawl immediately\n  providers:\n    firecrawl:\n      maxResults: 10\n```\n\nSwitching search vendor = change `search:` and save. Done.\n\n> **`fetch` migration**: the composition config used to mean \"omit `fetch:` =\n> no plugin fetch provider\". The schema now spells that case explicitly as\n> `\"off\"`. A profile patch that sets `fetch: tavily` keeps working unchanged;\n> a user document that wrote an empty `fetch:` should now read `off`.\n\n### The settings card\n\nWhen a settings provider is mounted, the plugin registers the `web-search`\nnamespace and contributes a card to **Settings → Plugins → Plugin\nconfiguration**. The card edits vendor selection and per-vendor options, plus\n**capability-scoped API keys**: a **Search API key** (always shown, for the\nsearch vendor) and a **Fetch API key** (shown only when the fetch vendor\ndiffers from the search vendor — a shared vendor needs one key). Keys are\nwritten through the **credentials** domain (never into `settings.yaml` or\nhardcoded in the plugin), addressed by each vendor's `apiKeyEnv` reference,\nand show a configured/unconfigured badge. A blank key box leaves the stored\nkey untouched.\n\n> The card appears only while a settings service is running. Without one, the\n> plugin still works exactly as composed (`cordis.patch.yml`), and edits to\n> the settings document (`settings.yaml`) simply have no effect on it.\n\n### Per-vendor options\n\nCommon to every vendor (all optional):\n\n| Field         | Meaning                                                            |\n| ------------- | ------------------------------------------------------------------ |\n| `apiKey`      | inline key (prefer credentials/env/files for secrets)              |\n| `apiKeyEnv`   | credential reference / env var name to read, per-vendor default    |\n| `baseURL`     | endpoint root override                                             |\n| `transport`   | `\"auto\"` (default), `\"curl\"`, or `\"fetch\"` — `auto` picks curl when a `*_proxy` env var is set (Node fetch ignores proxy env unless `NODE_USE_ENV_PROXY=1`) |\n| `timeoutSec`  | per-request budget, default 30                                     |\n\nTavily adds `maxResults` (default 8), `searchDepth` (`basic`|`advanced`),\n`includeAnswer` (default `true`). Firecrawl adds `limit` (default result count).\n\n> Keys are resolved **per request**, so rotating a key in the credentials\n> service, the env var, or the Tavily CLI config file needs no restart. A\n> request with no key anywhere fails with a readable\n> `WEB_PROVIDER_CREDENTIAL_MISSING`. The curl transport passes requests via\n> `-K -` stdin config: keys never appear in the process argv.\n\n## Adding a vendor\n\n1. Create `lib/providers/<vendor>.js`:\n\n   ```js\n   export const name = \"myvendor\";\n\n   export function create(configOrThunk, h) {\n     const readConfig = () => (typeof configOrThunk === \"function\" ? configOrThunk() : configOrThunk) ?? {};\n     const options = () => ({\n       apiKey: h.firstNonBlank(readConfig().apiKey) ?? h.envValue(\"MYVENDOR_API_KEY\") ?? \"\",\n       baseURL: readConfig().baseURL ?? \"https://api.myvendor.example\",\n     });\n     const search = {\n       id: \"myvendor\",\n       available: () => options().apiKey.length > 0 && URL.canParse(options().baseURL),\n       async search(request, signal) {\n         const o = options();\n         const { status, bodyText } = await h.postJson(\n           `${o.baseURL}/search`,\n           { authorization: `Bearer ${o.apiKey}` },\n           { q: request.query, limit: request.maxResults },\n           { transport: readConfig().transport, timeoutSec: readConfig().timeoutSec, signal },\n         );\n         if (status < 200 || status >= 300) throw h.errors.providerError(`myvendor HTTP ${status}`);\n         const data = h.parseJson(bodyText) ?? {};\n         // → { content?, sources: [{url, title?, snippet?, publishedAt?}], truncated: false }\n         return { sources: data.results ?? [], truncated: false };\n       },\n     };\n     // Optional fetch provider: { id, available(), fetch(request, signal) }\n     // → { url, statusCode, body: { kind: \"text\", content }, truncated }\n     return { search };\n   }\n   ```\n\n2. Register one line in `lib/registry.js` (`import` + array entry).\n3. Add a `VENDOR_SCHEMAS` entry in `lib/config-schema.js` (built with\n   `vendorSchema()` + per-vendor extras) so the settings namespace validates\n   and the card can render it.\n4. Add a row to the README vendor table.\n\nThat's the whole contract — helpers (`postJson`, `envValue`, `readHomeJson`,\n`firstNonBlank`, `cleanSnippet`, `parseJson`, `positiveInteger`, `errors`,\n`resolveCredential`) are injected, so vendor modules need no harness imports\nand share the proxy-aware transport and the credentials-service key chain.\n\n## Development & tests\n\n```sh\nnpm test                        # node:test unit suite (mock ctx.web + settings service)\nnode tests/integration.mjs      # real cordis + dsh-web + file settings hot-switch\nnode tests/verify-client.mjs    # loads lib/client.js through a stub module table\n```\n\n## Notes\n\n- Fully independent of your conversation LLM provider — custom\n  OpenAI-compatible gateways work out of the box.\n- `web_fetch` stays on the built-in anonymous `http` provider unless you set\n  `fetch:` + `fetchProvider: dsh-web-fetch`; switch when you want JS rendering\n  / PDF parsing (Firecrawl scrape) or clean article text (Tavily extract).\n- Errors surface as typed `WebError`s (`WEB_PROVIDER_ERROR` /\n  `WEB_PROVIDER_CREDENTIAL_MISSING` / `WEB_ABORTED`) when\n  `@deepseek-ai/dsh-web` is importable, plain coded Errors otherwise.\n","readmeFilename":"README.md"}