# dsh-tinyfish-search

English | [Chinese](https://github.com/maxwell-feng/dsh-tinyfish-search/blob/main/README.zh.md)

> [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that backs the built-in `web_search` tool with the [TinyFish Search API](https://docs.tinyfish.ai/search-api). One GET per query, no model call — fast and free (TinyFish Search is free at any wallet balance).

## What it does

DeepSeek Harness's built-in `web_search` tool normally runs through the DeepSeek Anthropic-compatible endpoint (`web-search-deepseek`). This plugin registers an alternative **web search provider** on the `ctx.web` capability seam:

- Stable provider id: `tinyfish`
- Every `web_search` call becomes `GET https://api.search.tinyfish.ai?query=...` with the `X-API-Key` header
- `results[]` (title / snippet / url / date) are normalized into the seam's portable source shape
- Reports provider readiness through `WebSearchProvider.ready()` (DeepSeek Harness 0.2.2+): the harness asks it before it declares `web_search`, so a profile with no usable TinyFish credential simply shows no `web_search` tool instead of a tool whose every call fails
- No LLM turn consumed per search — unlike the Anthropic server-tool approach
- Bundled native `icon.svg` vector artwork and bilingual manifest resources (`locale/en.json`, `locale/zh.json`) conforming to DSH 0.2.2-alpha.2 display conventions for Plugin Manager
- Pure TypeScript architecture across sources, scripts, and build configurations

Installing the bundle **takes over the built-in `web_search` automatically**: the
bundle patch overrides the `web` seam row (`searchProvider: tinyfish`,
`fetchProvider: http` restated), because `dsh-base` pins the seam to
`deepseek-official` and would otherwise keep the tool on the DeepSeek backend.
It also **re-enables the host-level `tool-web` row** (`disabled: false` plus
`search: true`, `fetch: true` and the base timeouts restated): the
`dsh-web-app` bundle ships that row disabled (the Web app normally composes
web tools per agent preset), so without it the model would see no `web_search`
tool at all on a clean web profile install. **Scope note:** re-enabling the
host row makes the tools visible to *every* agent preset on the profile —
including presets that would not otherwise carry web tools (e.g. `minimal`);
a preset that mounts its own `tool-web` row still shadows this global
registration for its agents. To scope the tools to one preset instead,
override or remove the `tool-web` row in your profile's `cordis.patch.yml`
and add `tool-web` to that preset's agent composition. Later layers (profile
/ home `cordis.patch.yml` / `--patch`) can still override both rows.

Configuration is declared as a **volatile schema** (DeepSeek Harness 0.1.7+): the
Host reads the `Config` schema this plugin exports and renders it as the form for
the `dsh-tinyfish-search` row on the Plugins page. There is no plugin-side
settings registration and no plugin-supplied browser half. A saved edit reaches
the next search without a restart.

## Requirements

- DeepSeek Harness `dsh` CLI (any profile with the web seam, e.g. `web`) — verified on `0.2.2-alpha.2`, the current `alpha` line and the one that carries `WebSearchProvider.ready()`; the plugin declares `engines.dsh` `>=0.1.7-alpha.2 <0.3.0`, so the whole `0.1.7` prerelease line and the `0.2.x` line are admitted, including the `0.2.1-rc.*` maintenance line, which predates `ready()` and therefore runs the plugin without the readiness gate
- Node.js `^22.19.0 || >=24.0.0` (matches the harness engine range)
- A [TinyFish API key](https://agent.tinyfish.ai/api-keys) (free to create; Search is free)
- The harness credential seam and launch environment (`@deepseek-ai/dsh-credentials`, `@deepseek-ai/dsh-launch-environment`) are required peers — every `dsh` profile carries them already

### Harness compatibility gate

DeepSeek Harness verifies a plugin's declared runtime requirement against the
running runtime **before** it admits the row, and refuses an incompatible plugin
instead of loading it. **Since `0.2.1-alpha.4` the rule reads differently:** when
a package declares its own `engines.dsh` range, that range decides alone and the
`@deepseek-ai/dsh*` `peerDependencies` are ignored for the compatibility
judgement; the peers are consulted only as a fallback when `engines.dsh` is
absent. Both are matched with prerelease semantics
(`semver.satisfies(runtime, range, { includePrerelease: true })`), so prerelease
runtimes participate in the comparison, and a refusal record reports
`requirements` rather than `peers`.

This plugin declares `engines.dsh: ">=0.1.7-alpha.2 <0.3.0"`, and that range
holds on `0.2.2-alpha.2`, on `0.2.1-rc.2`, on `0.2.0-rc.2`, on `0.2.0-rc.1`, and
on the whole `0.1.7` prerelease line — so no exemption is needed. Because
`0.14.0` and `0.13.0` already declared that widened range in `engines.dsh` as
well, they remain admitted on `0.2.2-alpha.2` too. The narrow-range `0.12.0`,
however, declared `^0.1.7-alpha.2`, which excludes `0.2.x`, so the harness still
refuses `0.12.0` on `0.2.x`: upgrade to `0.18.1` instead of granting an
exemption. If you run a `dsh` outside the declared range, DSH refuses the row
with a diagnostic naming the exact requirement pair; to accept that risk
explicitly, grant the exempted exact pair it prints:

```sh
dsh plugin allow-version dsh-tinyfish-search@0.18.1 --dsh-version <your-dsh-version> --accept-risk
```

## Documentation

- [Install Guide](INSTALL.md) ([Chinese](INSTALL.zh.md))
- [Usage Guide](USAGE.md) ([Chinese](USAGE.zh.md))
- [Configuration Guide](CONFIG.md) ([Chinese](CONFIG.zh.md))
- [Update Guide](UPDATE.md) ([Chinese](UPDATE.zh.md))
- [Uninstall Guide](UNINSTALL.md) ([Chinese](UNINSTALL.zh.md))
- [Changelog](CHANGELOG.md) ([Chinese](CHANGELOG.zh.md))

## Install

```sh
dsh plugin --profile web add dsh-tinyfish-search
```

or from the repository / a tarball:

```sh
dsh plugin --profile web add ./dsh-tinyfish-search        # source checkout
dsh plugin --profile web add ./dsh-tinyfish-search-0.18.1.tgz
dsh plugin --profile web add github:maxwell-feng/dsh-tinyfish-search
```

> **CLI syntax note**: Per DeepSeek Harness 0.2.1-alpha.1 specifications, `--profile <name>` must always precede forwarded subcommands like `add`, `remove`, and `update`.
>
> Git installs fetch sources, not built artifacts: pnpm runs the package's `prepare` script, which compiles `lib/` from source via `tsc -p tsconfig.build.json`.

See the [Install Guide](INSTALL.md) for requirements, all install methods, and verification.

## Configure

Set your API key (recommended — no secret in config files):

**Linux / macOS:**

```sh
export TINYFISH_API_KEY="your_api_key_here"                      # current shell
echo 'export TINYFISH_API_KEY="your_api_key_here"' >> ~/.bashrc  # permanent (bash)
echo 'export TINYFISH_API_KEY="your_api_key_here"' >> ~/.zshrc   # permanent (zsh)
source ~/.bashrc                                                 # or reopen the terminal
```

**Windows (PowerShell):**

```powershell
setx TINYFISH_API_KEY "your_api_key_here"    # permanent — takes effect in new terminals
$env:TINYFISH_API_KEY = "your_api_key_here"  # current session only
```

Or set fields in your profile's `cordis.yml` / patch layer:

```yaml
- insert:
    - id: dsh-tinyfish-search
      name: dsh-tinyfish-search
      config:
        # apiKey: "literal-key"          # alternative to the env var; avoid committing it
        # apiKeyEnv: TINYFISH_API_KEY     # default
        # baseURL: https://api.search.tinyfish.ai   # default
        # location: US                    # optional geo targeting forwarded to TinyFish
        # language: en                    # optional search language forwarded to TinyFish
```

| Field | Default | Meaning |
|---|---|---|
| `apiKey` | — | Literal TinyFish API key (secret role; wins over the env var) |
| `apiKeyEnv` | `TINYFISH_API_KEY` | Environment variable carrying the API key |
| `baseURL` | `https://api.search.tinyfish.ai` | TinyFish Search API endpoint base |
| `location` | — | Optional geo location forwarded as TinyFish's `location` (e.g. `US`); blank/unset sends nothing |
| `language` | — | Optional search language forwarded as TinyFish's `language` (e.g. `en`); blank/unset sends nothing |

See the [Configuration Guide](CONFIG.md) for the full schema, credential resolution order, and runtime settings UI.

## Verify

```sh
dsh --profile web --dump-config | grep tinyfish   # layer present
```

Inside a session, call `web_search` and check that results carry TinyFish URLs/snippets. The web search settings card in the GUI shows the provider state.

## Usage

After installation, no code changes required. In any session with the `web` profile:

1. The model calls `web_search` as usual (e.g. "search for TinyFish docs").
2. The harness routes it through `ctx.web → tinyfish → https://api.search.tinyfish.ai`.
3. Results appear as `WebSearchSource[]` (`url` / `title` / `snippet` / `publishedAt`) in the tool result.
4. Check GUI: **Settings → Web Search** shows provider `tinyfish` and `available: true` when the API key is configured.

Availability, readiness and failure are three different signals. `available()` is a cheap local check the seam uses to pick a provider while an operation runs. `ready()` answers whether a search for a model request on a given chat route has a credential to send, and `dsh-tool-web` uses it as the activation condition of `web_search` — no usable credential, no tool. A search that still runs without a key (for example a programmatic caller that bypasses the gate) fails with `WEB_PROVIDER_CREDENTIAL_MISSING` rather than reaching TinyFish unauthenticated.

Abort and error semantics follow the `dsh-web` seam: `WEB_PROVIDER_CREDENTIAL_MISSING` when no key, `WEB_ABORTED` on cancellation, `WEB_PROVIDER_ERROR` otherwise.

TinyFish returns no long-form summary and no generated answer, so the plugin fills neither `WebSearchSource.summary` nor `WebSearchResult.content`: both are optional on the seam because not every provider returns them, and inventing a value would make the seam lie.

See the [Usage Guide](USAGE.md) for providers, credential configuration, worked examples, and the error table.

## Uninstall

```sh
dsh plugin --profile web remove dsh-tinyfish-search
```

Removes the bundle layer and the `tinyfish` provider registration, and restores
the composed `web` / `tool-web` rows to exactly what the underlying bundles
ship (an inserted row's override returns to the row's own defaults when the
inserting layer is removed). Restart `dsh --profile web` to confirm
`web_search` falls back to the base `deepseek-official` provider (or none if
no other provider is installed).

## Updating

```sh
dsh plugin --profile web add dsh-tinyfish-search@latest
# or from git, to pick up changes before they reach npm:
dsh plugin --profile web add github:maxwell-feng/dsh-tinyfish-search
```

Upgrading to 0.18.1 from ≤ 0.18.0 needs no manual steps: it corrects the
Requirements entry above and changes nothing else — no dependency, source, test
or behavioural contract differs from 0.18.0.

Upgrading to 0.18.0 from ≤ 0.17.1 needs no manual steps: it aligns the plugin
with DeepSeek Harness `0.2.2-alpha.2` and adds the `ready()` member the new
`ctx.web` contract introduced. It moves the pinned `@deepseek-ai/dsh-*`
devDependencies and the lockfile to `0.2.2-alpha.2`, lifts `@deepseek-ai/cordis`
to `~4.0.6-alpha.1`, widens the cordis peer to `^4.0.4 || ^4.0.6-alpha.1` so
hosts on 4.0.4 and 4.0.5 stay admitted, moves `@deepseek-ai/schemastery` to
`~3.18.6-alpha.1`, refreshes the workspace supply-chain exemption to that
package set, and bumps `USER_AGENT` to `dsh-tinyfish-search/0.18.0`. `engines.dsh`
remains `>=0.1.7-alpha.2 <0.3.0`, and no search, mapping or configuration logic
changed.

Upgrading to 0.17.0 from ≤ 0.16.0 needs no manual steps: it aligns the plugin
with DeepSeek Harness `0.2.1-rc.2`, the latest release of the
plugin-development documentation this plugin follows. It moves the pinned
`@deepseek-ai/dsh-*` devDependencies and the lockfile to `0.2.1-rc.2`, lifts
`@deepseek-ai/cordis` to `^4.0.5` and `@deepseek-ai/schemastery` to `3.18.5`,
refreshes the workspace supply-chain exemption to that package set, and bumps
`USER_AGENT` to `dsh-tinyfish-search/0.17.0`. The `engines.dsh` and peer ranges
remain `>=0.1.7-alpha.2 <0.3.0`, and no search-provider code changed.

Upgrading to 0.16.0 from ≤ 0.15.0 needs no manual steps: it aligns the plugin
with DeepSeek Harness `0.2.1-alpha.4`, the current release of the plugin-development
documentation this plugin follows. It moves the pinned `@deepseek-ai/dsh-*`
devDependencies and the lockfile to `0.2.1-alpha.4`, refreshes the workspace
supply-chain exemption to that package set, and bumps `USER_AGENT` to
`dsh-tinyfish-search/0.16.0`. The `@deepseek-ai/dsh-*` peers remain
`>=0.1.7-alpha.2 <0.3.0`, and no search-provider code changed.

Upgrading to 0.15.0 from ≤ 0.14.0 needs no manual steps: it aligns the plugin
with DeepSeek Harness `0.2.1-alpha.2`, the current release of the plugin-development
documentation this plugin follows. It bundles native `icon.svg` artwork and
bilingual display manifest resources (`locale/en.json`, `locale/zh.json`), updates
`prepare` to self-contained `tsc -p tsconfig.build.json`, updates CLI syntax to put
`--profile` first, and bumps `USER_AGENT` to `dsh-tinyfish-search/0.15.0`.
The `@deepseek-ai/dsh-*` peers remain `>=0.1.7-alpha.2 <0.3.0`.

Upgrading to 0.14.0 from ≤ 0.13.0 needs no manual steps: it aligns the plugin
with DeepSeek Harness `0.2.0-rc.2`, the current release of the plugin-development
documentation this plugin follows, and changes no plugin source apart from the
`USER_AGENT` version constant. The `@deepseek-ai/dsh-*` peers stay
`>=0.1.7-alpha.2 <0.3.0`, so `0.13.0` is already admitted on `0.2.0-rc.2` — this
release is not a forced upgrade: it pins `devDependencies` to `0.2.0-rc.2` and
re-verifies every consumed seam against it. The `0.2.0-rc.2` delta is additive —
Cordis Inspect diagnostics, `TypertGateway.hasLiveClient()`, and timed user
questions — and touches no interface this plugin uses. `USER_AGENT` is bumped to
`dsh-tinyfish-search/0.14.0`.

Upgrading to 0.13.0 from ≤ 0.12.0 needs no manual steps: it aligns the plugin
with DeepSeek Harness `0.2.0-rc.1`, the current release of the plugin-development
documentation this plugin follows, and changes no plugin source apart from the
`USER_AGENT` version constant. The `@deepseek-ai/dsh-*` peers widen to
`>=0.1.7-alpha.2 <0.3.0`, because `0.2.0-rc.1` enforces peer compatibility as a
hard refusal and the previous `^0.1.7-alpha.2` range excludes `0.2.x` — that
widening is what admits the plugin on `0.2.0-rc.1` while keeping every `0.1.7`
prerelease admitted. Every consumed seam is source-identical between
`0.1.7-rc.2` and `0.2.0-rc.1`, so no configuration field moved. `USER_AGENT` is
bumped to `dsh-tinyfish-search/0.13.0`.

Upgrading to 0.12.0 from ≤ 0.11.1 needs no manual steps: it aligns the plugin
with DeepSeek Harness `0.1.7-rc.2`, the current release of the plugin-development
documentation this plugin follows, and changes no plugin source apart from the
`USER_AGENT` version constant. The `@deepseek-ai/dsh-*` peers stay
`^0.1.7-alpha.2`, so the plugin stays installable on every `0.1.7` prerelease from
`0.1.7-alpha.2` through `0.1.7-rc.2`, and every consumed seam is source-identical
between `0.1.7-rc.1` and `0.1.7-rc.2`. `USER_AGENT` is bumped to
`dsh-tinyfish-search/0.12.0`.

Upgrading to 0.11.1 from ≤ 0.11.0 needs no manual steps: it changes no runtime
code, no configuration and no tool surface. The repository holds TypeScript
sources only — the bilingual-documents gate is now
[`scripts/check-docs-language.ts`](./scripts/check-docs-language.ts), which Node
runs directly by stripping the types — and this release corrects several
documentation-ordering defects.

Upgrading to 0.11.0 from ≤ 0.10.0 needs no manual steps, but it is a **harness
floor raise**: the plugin now requires DeepSeek Harness `0.1.7-alpha.2` or newer
and is verified on `0.1.7-rc.1`. On a `0.1.6` host DSH refuses the row (see
[Harness compatibility gate](#harness-compatibility-gate)). Configuration moves
to the 0.1.7 volatile schema — the same fields, the same values, the same
defaults; only the form that edits them changed. `USER_AGENT` is bumped to
`dsh-tinyfish-search/0.11.0`.

Upgrading to 0.10.0 from ≤ 0.9.0 needs no manual steps: aligned with DeepSeek
Harness `0.1.6-alpha.2` (`@deepseek-ai/dsh-*` peers now `^0.1.6-alpha.2`, Node
`^22.19.0 || >=24.0.0`), and the `USER_AGENT` is bumped to
`dsh-tinyfish-search/0.10.0`.

Upgrading to 0.9.0 from ≤ 0.8.3 needs no manual steps: aligned with DeepSeek
Harness `0.1.6-alpha.1` (`@deepseek-ai/dsh-*` peers now `^0.1.6-alpha.1`, Node
`^22.19.0 || >=24.0.0`), and the published package now ships only `lib/`,
`cordis.patch.yml`, and `LICENSE` — the guides stay in this repository and are
no longer installed into your profile. The `USER_AGENT` is bumped to
`dsh-tinyfish-search/0.9.0`.

Upgrading to 0.8.3 from ≤ 0.8.1 needs no manual steps: hardened with enterprise-grade SSRF security defenses, verified against DeepSeek Harness `0.1.5-rc.2`, and `USER_AGENT` attribution updated to `dsh-tinyfish-search/0.8.3`.

Upgrading to 0.8.1 from ≤ 0.8.0 needs no manual steps: resolved all Dependabot
security alerts for `js-yaml` (CVE-2026-84375, GHSA-5p4m-2wfm-xmqj, CVE-2026-59869, CVE-2026-53550),
retaining the pure TypeScript architecture (zero JavaScript tracked), and the
`USER_AGENT` is bumped to `dsh-tinyfish-search/0.8.1`.

Upgrading to 0.8.0 from ≤ 0.7.0 needs no manual steps: the settings section,
patch rows, and credential reference are all carried by the bundle layer, and
pnpm refreshes the package in place. The plugin is now a pure TypeScript
codebase (zero JavaScript tracked), and the `USER_AGENT` is bumped to
`dsh-tinyfish-search/0.8.0`.

Upgrading to 0.7.0 from ≤ 0.6.1 needs no manual steps: the settings section,
patch rows, and credential reference are all carried by the bundle layer, and
pnpm refreshes the package in place. The codebase has been fully refactored
into modular TypeScript components conforming to official guidelines, and the
`USER_AGENT` is bumped to `dsh-tinyfish-search/0.7.0`.

Upgrading to 0.6.1 from ≤ 0.5.0 needs no manual steps: the settings section,
patch rows, and credential reference are all carried by the bundle layer, and
pnpm refreshes the package in place. The user-visible changes are the
manifest declaration (`manifestVersion: 1`, `@deepseek-ai/dsh-*` peers now `^0.1.5-rc.2`, Node `>=22`) and
the `USER_AGENT` attribution (`dsh-tinyfish-search/0.6.1`).

Upgrading to 0.5.0 from ≤ 0.4.0 needs no manual steps: the settings section,
patch rows, and credential reference are all carried by the bundle layer, and
pnpm refreshes the package in place. The only user-visible changes are the
harness floor (`@deepseek-ai/dsh-*` peers now `^0.1.5-rc.1`, Node `>=22`) and
the `USER_AGENT` attribution (`dsh-tinyfish-search/0.5.0`).

Upgrading to 0.3.0 from ≤ 0.2.1 needs no manual steps: the settings section,
patch rows, and credential reference are all carried by the bundle layer, and
pnpm refreshes the package in place. The only user-visible change is the
`web`/`tool-web` row behavior documented above, which stays identical unless
you had already overridden those rows yourself.

## Development

```sh
pnpm install
pnpm build     # tsc -> lib/
pnpm test      # node --test (mocked fetch)
pnpm typecheck # tsc --noEmit, src + test + scripts
```

The repository is TypeScript throughout; there is no JavaScript source to keep in
sync. The bilingual-documents gate is
[`scripts/check-docs-language.ts`](./scripts/check-docs-language.ts), which CI runs
with bare Node (no dependencies installed) before the install step:

```sh
node scripts/check-docs-language.ts
```

Publishing to npm runs through GitHub Actions with npm **Trusted Publishing** (OIDC) — see [`.github/workflows/publish.yml`](./.github/workflows/publish.yml) and the [npm docs](https://docs.npmjs.com/trusted-publishers/). Tag `vX.Y.Z` (or dispatch the workflow) to release; provenance is generated automatically.

## Release notes

See [CHANGELOG.md](./CHANGELOG.md) (English) or [CHANGELOG.zh.md](./CHANGELOG.zh.md) and the [GitHub Releases](https://github.com/maxwell-feng/dsh-tinyfish-search/releases) page.

## License

MIT — see [LICENSE](./LICENSE).
