dsh-tinyfish 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,30 +1,52 @@
1
- # dsh-tinyfish
1
+ <div align="center">
2
+ <img src="assets/tinyfish.svg" alt="TinyFish logo" width="120" />
3
+ <h1>dsh-tinyfish</h1>
4
+ <p><strong>Free web search and fetch for the DeepSeek Harness.</strong><br />Give your agent the live web — at $0 per call.</p>
2
5
 
3
- [![npm](https://img.shields.io/npm/v/dsh-tinyfish.svg)](https://www.npmjs.com/package/dsh-tinyfish) [![downloads](https://img.shields.io/npm/dm/dsh-tinyfish.svg)](https://www.npmjs.com/package/dsh-tinyfish) [![ci](https://github.com/viztor/dsh-tinyfish/actions/workflows/ci.yml/badge.svg)](https://github.com/viztor/dsh-tinyfish/actions/workflows/ci.yml) [![license](https://img.shields.io/npm/l/dsh-tinyfish.svg)](https://github.com/viztor/dsh-tinyfish/blob/main/LICENSE) [![node](https://img.shields.io/badge/node-%3E%3D22.14-5FA04E.svg)](https://nodejs.org)
6
+ [![npm](https://img.shields.io/npm/v/dsh-tinyfish.svg)](https://www.npmjs.com/package/dsh-tinyfish) [![downloads](https://img.shields.io/npm/dm/dsh-tinyfish.svg)](https://www.npmjs.com/package/dsh-tinyfish) [![ci](https://github.com/viztor/dsh-tinyfish/actions/workflows/ci.yml/badge.svg)](https://github.com/viztor/dsh-tinyfish/actions/workflows/ci.yml) [![license](https://img.shields.io/npm/l/dsh-tinyfish.svg)](https://github.com/viztor/dsh-tinyfish/blob/main/LICENSE)
4
7
 
5
- **Search and fetch for the DeepSeek Harness, at $0.**
8
+ </div>
6
9
 
7
- A DSH bundle that makes [TinyFish](https://tinyfish.ai) the implementation of the harness's own `web_search` and `web_fetch` tools. Both endpoints are free, so the web path on your host stops costing money per call.
10
+ ---
8
11
 
9
- | | `dsh-web`'s default | with `dsh-tinyfish` |
10
- | --- | --- | --- |
11
- | search | `deepseek-official` | TinyFish `/search` via Monid, **$0** |
12
- | fetch | `http` | TinyFish `/fetch`, **$0**, returns clean Markdown |
13
- | turndown conversion | yes, on every fetch | **no** — the content is already Markdown |
14
- | reversibility | — | two words, no reinstall |
12
+ Your agent can already reason. This gives it something to reason _about_: live search results and clean page content, wired straight into the harness's own `web_search` and `web_fetch` tools. Powered by [TinyFish](https://tinyfish.ai) — both endpoints are free, so the web path on your host stops costing money per call.
13
+
14
+ | Before | After |
15
+ | --- | --- |
16
+ | Search bills per call | **$0**, forever |
17
+ | Fetched pages arrive as HTML, converted clumsily | **Clean Markdown**, straight from a browser-grade extractor |
18
+ | Switching providers means reinstalling | **Two words** in a config file, no reinstall |
15
19
 
16
- ## Install
20
+ ## 🚀 Quick start
21
+
22
+ **1. Install** — in your web profile:
17
23
 
18
24
  ```sh
19
25
  cd ~/.dsh/profiles/web
20
- npm install dsh-tinyfish
26
+ npm install dsh-tinyfish # or: npm install @viztor/dsh-tinyfish — same thing
21
27
  ```
22
28
 
23
- Add the bundle to that profile's `package.json`, then restart DSH:
29
+ <details>
30
+ <summary><strong>📦 Installing from GitHub Packages instead</strong></summary>
31
+
32
+ <br />
33
+
34
+ Every release mirrors both names to GitHub Packages — a second source if npmjs.org is unreachable, and what populates the repository sidebar. Unlike npmjs, GitHub Packages requires authentication even for public packages: an unauthenticated request 404s without saying whether the package exists. With a token carrying `read:packages`:
35
+
36
+ ```ini
37
+ # project-local .npmrc is better than global for a token
38
+ @viztor:registry=https://npm.pkg.github.com
39
+ //npm.pkg.github.com/:_authToken=ghp_xxx
40
+ ```
41
+
42
+ then `npm install @viztor/dsh-tinyfish` resolves from the mirror. Unless npmjs is down, prefer it: no token, no extra configuration.
43
+ </details>
44
+
45
+ **2. Mount it** — add to that profile's `package.json`, then restart DSH:
24
46
 
25
47
  ```jsonc
26
48
  {
27
- "dependencies": { "dsh-tinyfish": "^0.2.0" },
49
+ "dependencies": { "dsh-tinyfish": "^0.3.0" },
28
50
  "dsh": {
29
51
  "profile": {
30
52
  "bundles": [
@@ -37,52 +59,20 @@ Add the bundle to that profile's `package.json`, then restart DSH:
37
59
  }
38
60
  ```
39
61
 
40
- That is the whole install. The bundle's own `cordis.patch.yml` selects itself:
41
-
42
- ```yaml
43
- - id: web
44
- name: "@deepseek-ai/dsh-web"
45
- config:
46
- searchProvider: tinyfish
47
- fetchProvider: tinyfish
48
- ```
49
-
50
- Restarting matters: bundles are resolved when the harness boots, so `patchReload` will not pick up a newly mounted one.
51
-
52
- **To go back**, set those two back to `deepseek-official` and `http`. The bundle stays mounted and idle — registration and selection are separate, and only your profile decides which provider wins.
53
-
54
- **Requirements:** DSH **0.2.0+**, Node **22.14+**, and a credential (below). The harness supplies the `@deepseek-ai/*` peer packages; you do not install them.
55
-
56
- ## Verify
62
+ > Bundles resolve at boot, so a restart picks it up — reloading the patch alone won't.
57
63
 
58
- Ask the agent to search for something. Or check the wiring without an agent:
64
+ **3. Add a key** — pick a channel below, save the key, and ask your agent to search for something. That's the whole install.
59
65
 
60
- ```sh
61
- cd ~/.dsh/profiles/web
62
- node -e '
63
- const m = require("dsh-tinyfish");
64
- const ctx = { web: { registerSearchProvider(){}, registerFetchProvider(){} }, get: () => undefined };
65
- m.apply(ctx, m.Config({}));
66
- console.log("registered:", m.name, "| no throw above = wired");
67
- '
68
- ```
66
+ ## 🔑 Two channels, one plugin
69
67
 
70
- If search reports `WEB_PROVIDER_CREDENTIAL_MISSING` or `WEB_PROVIDER_UNAVAILABLE`, that is the next section.
71
-
72
- ## Credentials
73
-
74
- A fresh install has none, and never will — a key inside an npm tarball would be published forever. Both endpoints are free, but both need an account.
75
-
76
- **In the profile patch (no restart needed).** The `web-tinyfish` row in your profile's `cordis.patch.yml` sets `apiKeyEnv`, which the harness resolves through its credentials service and the launch environment, and re-reads on every call — so a change takes effect on the next search.
77
-
78
- **By hand:**
79
-
80
- | channel | get a key | then |
68
+ | | Direct _(default)_ | Via Monid |
81
69
  | --- | --- | --- |
82
- | `direct` default | [tinyfish.ai](https://tinyfish.ai) → API keys | `tinyfish auth login`, or `echo $KEY \| tinyfish auth set` |
83
- | `monid` | [app.monid.ai](https://app.monid.ai) | `monid keys add`, or `export MONID_API_KEY` |
70
+ | What's behind it | TinyFish's own API | The same TinyFish endpoints, through your Monid wallet |
71
+ | You need | A free key from [tinyfish.ai](https://tinyfish.ai) | A platform key from [app.monid.ai](https://app.monid.ai) |
72
+ | Fastest setup | `tinyfish auth login` | `monid keys add` |
73
+ | Costs | $0 | $0 |
84
74
 
85
- The default is `direct` because the package is named for TinyFish: a fresh install asks for the credential its own name implies rather than for an account at a different service. If you would rather go through Monid — it reuses a platform key a Monid MCP mount already holds, and costs the same — pin it in your own profile patch, which is a host decision and does not need a new release of this plugin:
75
+ The default is `direct`, because the package is named for TinyFish — a fresh install asks for the credential its own name implies. Prefer Monid (it reuses a platform key your Monid MCP mount may already hold)? Pin it in your profile patch:
86
76
 
87
77
  ```yaml
88
78
  - id: web-tinyfish
@@ -90,31 +80,38 @@ The default is `direct` because the package is named for TinyFish: a fresh insta
90
80
  channel: monid
91
81
  ```
92
82
 
93
- Resolution order, first match wins: a literal `apiKey` in the settings row → the harness credentials service → the launch environment → the live environment → the CLI stores. A failing credential service falls through to the next source rather than failing the search.
83
+ Both keys can live side by side — saving one never overwrites the other, and switching channels loses nothing.
84
+
85
+ ## ⚙️ Settings page
94
86
 
95
- ## Configuration
87
+ **Settings → Plugins → TinyFish.** Everything editable lives here: the channel picker, your keys, what the search ranks on, retries, and the search/fetch toggles. Changes stage and save together; a key you type is stored by the harness, never in your profile.
96
88
 
97
- Everything lives in one row, `web-tinyfish`, edited in your profile's `cordis.patch.yml`. The row is validated, so an out-of-range value is rejected with a message rather than silently clamped.
89
+ The page shows the key for the **selected** channel, plus the saved-or-not status of both — so you always know where you stand without switching back and forth.
98
90
 
99
- **Settings → Plugins → TinyFish** edits this row, if you prefer a form to a patch file. The plugin ships both halves: the provider the harness loads, and a client bundle that contributes the page. Changes are staged and written on save, and a key you type is stored by the harness rather than in your profile.
91
+ > Going back is two words: set `searchProvider`/`fetchProvider` to `deepseek-official` and `http`. The bundle stays mounted and idle.
100
92
 
101
- The patch file is still the honest place for the first edit — it is where a selection that overrides someone else's layer belongs, and it needs no build. Use whichever suits the change.
93
+ <details>
94
+ <summary><strong>📖 Full configuration reference</strong></summary>
95
+
96
+ <br />
97
+
98
+ Everything lives in one row, `web-tinyfish`. The row is validated, so an out-of-range value is rejected with a message rather than silently clamped.
102
99
 
103
100
  | key | default | meaning |
104
101
  | --- | --- | --- |
105
- | `channel` | `direct` | `monid` or `direct`; see [Credentials](#credentials) |
102
+ | `channel` | `direct` | `monid` or `direct` |
106
103
  | `apiKey` | _(unset)_ | literal credential for either channel; prefer a ref |
107
104
  | `apiKeyEnv` | `TINYFISH_API_KEY` | credential reference, or env var, for `direct` |
108
105
  | `monidKeyEnv` | `MONID_API_KEY` | credential reference, or env var, for `monid` |
109
106
  | `purpose` | _(unset)_ | goal statement; TinyFish ranks on it |
110
107
  | `attempts` | `3` | retries for a transient failure or an empty search (1–5) |
111
- | `filters.domainType` | _(unset)_ | `web` \| `news` \| `research_paper` |
112
- | `filters.language` / `.location` | _(unset)_ | geo targeting |
113
- | `filters.includeDomains` / `.excludeDomains` | _(unset)_ | comma-separated |
108
+ | `filters.domainType` | _(unset)_ | `web` \| `news` \| `research_paper` — patch file only |
109
+ | `filters.language` / `.location` | _(unset)_ | geo targeting — patch file only |
110
+ | `filters.includeDomains` / `.excludeDomains` | _(unset)_ | comma-separated — patch file only |
114
111
  | `monidBase` / `searchBase` / `fetchBase` | upstream | endpoint override, for staging |
115
112
  | `search` / `fetch` | `true` | offer this kind at all; `false` declines without unregistering |
116
113
 
117
- Search and fetch are switched independently. Both always register, so turning one off makes it report _unavailable_ rather than _missing_ — the harness tells those apart, and only the second means "I turned this off" rather than "the install is broken".
114
+ Turning one off reports _unavailable_ rather than _missing_ — the harness tells those apart, and only the second means "the install is broken".
118
115
 
119
116
  ```yaml
120
117
  - id: web-tinyfish
@@ -123,83 +120,76 @@ Search and fetch are switched independently. Both always register, so turning on
123
120
  fetch: false # keep TinyFish for search, let dsh-web use another fetch
124
121
  ```
125
122
 
126
- ### Where a value comes from
123
+ ### Filters live in the patch file
127
124
 
128
- Every setting resolves in the same three rungs — **row, then environment, then built-in default** — so a deployment can be retargeted without writing a patch file. This is the shape the shipped providers use for `$DEEPSEEK_SEARCH_BASE_URL`.
125
+ `filters` is a nested object, and the settings form addresses one flat key per field — so search tuning stays operator-level:
129
126
 
130
- | setting | environment variable |
131
- | ------------ | -------------------------- |
132
- | `monidBase` | `TINYFISH_MONID_BASE_URL` |
133
- | `searchBase` | `TINYFISH_SEARCH_BASE_URL` |
134
- | `fetchBase` | `TINYFISH_FETCH_BASE_URL` |
135
-
136
- An endpoint that does not parse makes the provider report itself unavailable rather than being trusted.
137
-
138
- ### The credential, in order
139
-
140
- Resolved **per call**, so a key rotated anywhere below takes effect on the next search with no restart. First match wins:
141
-
142
- | # | source | set it by |
143
- | --- | --- | --- |
144
- | 1 | the `apiKey` literal | the row — a secret in config; prefer 2–3 |
145
- | 2 | the credentials service | `apiKeyEnv` (direct) or `monidKeyEnv` (monid) in the settings UI |
146
- | 3 | the launch environment | exported before DSH started |
147
- | 4 | the live environment | `MONID_API_KEY` / `TINYFISH_API_KEY` |
148
- | 5 | the channel's CLI store | `monid keys add` / `tinyfish auth login` |
127
+ ```yaml
128
+ - id: web-tinyfish
129
+ config:
130
+ channel: monid
131
+ filters:
132
+ domainType: research_paper
133
+ language: zh
134
+ includeDomains: arxiv.org,openreview.net
135
+ ```
149
136
 
150
- The harness services sit above the environment on purpose: a value someone typed into Settings is a more deliberate choice than one that merely happens to be exported. A failing service falls through to the next source rather than failing the search, and a host that mounts neither still works.
137
+ ### Where a credential comes from
151
138
 
152
- ### Two keys, one settings page
139
+ Resolved **per call** — a rotated key takes effect on the next search, no restart. First match wins:
153
140
 
154
- The two channels authenticate against different services, so each has **its own** credential reference: `apiKeyEnv` (default `TINYFISH_API_KEY`) for `direct`, and `monidKeyEnv` (default `MONID_API_KEY`) for `monid`. Saving one never overwrites the other, so both can be live at once and switching channels back and forth loses nothing.
141
+ 1. the `apiKey` literal in the row (a secret in config; prefer 2–3)
142
+ 2. the credentials service — `apiKeyEnv` (direct) or `monidKeyEnv` (monid), saved from the settings UI
143
+ 3. the launch environment (exported before DSH started)
144
+ 4. the live environment (`MONID_API_KEY` / `TINYFISH_API_KEY`)
145
+ 5. the channel's CLI store (`monid keys add` / `tinyfish auth login`)
155
146
 
156
- The page shows the key for the **selected** channel only. Showing both at once would invite pasting the Monid platform key into the field TinyFish authenticates with — and a key sent to the wrong service fails as a 401, which reads as "that key is wrong" rather than as "that was the wrong field". The `apiKey` literal still overrides either channel; it exists for a patch file, and the settings page does not write it.
147
+ A failing service falls through to the next source rather than failing the search.
157
148
 
158
- ## Why the fetch path is a real improvement
149
+ ### Where an endpoint comes from
159
150
 
160
- `dsh-tool-web` renders a `kind: "html"` body by running **turndown** to convert HTML to Markdown, behind a depth cap with a `"[HTML content omitted]"` fallback. TinyFish already extracts clean Markdown in a browser-grade extractor, so this provider returns `kind: "text"` and the content reaches the model with no conversion step at all.
151
+ Row, then environment, then built-in default — so staging can retarget without a patch:
161
152
 
162
- ## Two channels, one payload
153
+ | setting | environment variable |
154
+ | ------------ | -------------------------- |
155
+ | `monidBase` | `TINYFISH_MONID_BASE_URL` |
156
+ | `searchBase` | `TINYFISH_SEARCH_BASE_URL` |
157
+ | `fetchBase` | `TINYFISH_FETCH_BASE_URL` |
163
158
 
164
- Monid is a thin envelope whose `output` is the direct response verbatim, and it forwards parameter names unchanged. One transport serves both, and nothing above it branches on which is active — a test asserts the two agree on the top hit for the same query.
159
+ </details>
165
160
 
166
- | | `direct` (default) | `monid` |
167
- | ------ | ---------------------------- | -------------------------- |
168
- | search | `GET api.search.tinyfish.ai` | `POST api.monid.ai/v1/run` |
169
- | fetch | `POST api.fetch.tinyfish.ai` | `POST api.monid.ai/v1/run` |
170
- | auth | `X-API-Key` | `Authorization: Bearer` |
171
- | cost | $0, direct | $0, on the Monid wallet |
161
+ <details>
162
+ <summary><strong>🔍 Behaviour worth knowing</strong></summary>
172
163
 
173
- ## Behaviour worth knowing
164
+ <br />
174
165
 
175
166
  - **A 404 is a result, not an error.** A per-URL fetch failure comes back carrying its status, because that is resource state the model needs.
176
- - **`publishedAt` is honest.** TinyFish reports dates as human strings (`"Apr 30, 2026"`, `"1 year ago"`). What parses is coerced to ISO-8601; what does not is dropped rather than invented. Unzoned dates are read as UTC, so the same page reports the same day regardless of where the Worker ran.
177
- - **Search retries an empty result.** The upstream answers a valid query with nothing about one run in three, so a blank result is retried up to `attempts` before it is believed.
178
- - **A blocked run is terminal.** If a Monid workspace control stops a run, the error says why and links to top up. It is never retried.
167
+ - **`publishedAt` is honest.** TinyFish reports human dates (`"Apr 30, 2026"`, `"1 year ago"`). What parses becomes ISO-8601; what doesn't is dropped, never invented. Unzoned dates read as UTC, so the same page reports the same day everywhere.
168
+ - **Empty searches retry.** The upstream answers a valid query with nothing about one run in three — a blank result is retried up to `attempts` before it is believed.
169
+ - **A blocked run is terminal.** If a Monid workspace control stops a run, the error says why and links to top up. Never retried.
179
170
 
180
- ## Not included
171
+ TinyFish's `agent` and `browser` surfaces are **not** exposed: metered, wallet-billed, and not a search or a fetch. Use the `tinyfish` CLI directly when a page genuinely needs a real browser.
181
172
 
182
- TinyFish's `agent` and `browser` surfaces are **not** exposed. They cost $0.016/step and $0.002/min, are metered against a wallet, and do not fit `ctx.web` — that seam has exactly two provider kinds, and an agent run is an action, not a search or a fetch. Use the `tinyfish` CLI directly when a page genuinely needs a real browser.
173
+ </details>
183
174
 
184
- ## Development
175
+ <details>
176
+ <summary><strong>🛠 Development</strong></summary>
185
177
 
186
- The toolchain is [Vite+](https://viteplus.dev): `vp pack` builds the library with tsdown, `vp test` runs Vitest, and `vp lint` / `vp fmt` are Oxlint and Oxfmt, type-aware. Lint and format settings live in the `lint` and `fmt` blocks of `vite.config.ts` — Vite+ disables nested Oxlint/Oxfmt configs, so a standalone `oxlint.config.ts` would be read by nobody.
178
+ <br />
179
+
180
+ The toolchain is [Vite+](https://viteplus.dev): `vp pack` builds with tsdown, `vp test` runs Vitest, `vp lint` / `vp fmt` are Oxlint and Oxfmt, type-aware. Lint and format live in `vite.config.ts` — Vite+ ignores standalone configs.
187
181
 
188
182
  ```sh
189
183
  pnpm install
190
- pnpm test # 78 hermetic tests — no network, no credential
184
+ pnpm test # 143 hermetic tests — no network, no credential
191
185
  pnpm run check # format + lint + types
192
- pnpm run release:gate # build, then the full gate
186
+ pnpm run release:gate # build, then the full gate incl. 11 package checks
193
187
  pnpm run test:live # the real APIs, still $0, needs credentials
194
188
  ```
195
189
 
196
- `pnpm run ci` ends in one script, `scripts/check.mjs`, that runs six package checks in a single pass: `lib/` freshness, peer ranges npm can parse, the bundle contract, the harness surfaces still being present, no credentials in the tree, and — the one that earns its keep — packing the tarball, installing it with plain npm, and loading it. Each is proved by planting the regression it guards, and CI runs all of it.
197
-
198
- Full process and invariants: [`AGENTS.md`](./AGENTS.md).
199
-
200
- ## Compatibility
190
+ Requires **DSH 0.2.0+** and **Node 22.14+**. Full process and invariants: [`AGENTS.md`](./AGENTS.md). Contributing: [`CONTRIBUTING.md`](./CONTRIBUTING.md).
201
191
 
202
- Requires **DSH 0.2.0+**; tested against 0.2.0-rc.1. The `@deepseek-ai/dsh-*` peers are `^0.2.0-rc.1`, so a DSH patch release will not orphan the plugin, and a 0.3 contract change still fails loudly rather than silently.
192
+ </details>
203
193
 
204
194
  ## License
205
195
 
@@ -0,0 +1,18 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="TinyFish logo">
2
+ <title>TinyFish</title>
3
+ <defs>
4
+ <linearGradient id="tf-fish" x1="12" y1="20" x2="52" y2="44" gradientUnits="userSpaceOnUse">
5
+ <stop offset="0" stop-color="#4D6BFE"/>
6
+ <stop offset="1" stop-color="#14B8A6"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <!--
10
+ The DeepSeek Harness icon family: a softly tinted rounded square carrying
11
+ one vibrant glyph. Pale blue container, DeepSeek-blue-to-teal fish, white
12
+ knockout eye. Fill only, so it stays crisp at any size.
13
+ -->
14
+ <rect x="2" y="2" width="60" height="60" rx="15" fill="#E8F1FE"/>
15
+ <ellipse cx="27" cy="33" rx="15" ry="11" fill="url(#tf-fish)"/>
16
+ <polygon points="39,33 53,23 53,43" fill="url(#tf-fish)"/>
17
+ <circle cx="21" cy="30.5" r="2.4" fill="#ffffff"/>
18
+ </svg>
package/lib/client.js CHANGED
@@ -233,7 +233,11 @@ function TinyfishCard(props) {
233
233
  }
234
234
  });
235
235
  const channel = (state.fields[FIELD.channel]?.text ?? "") === "monid" ? "monid" : "direct";
236
- const key = state.keys[channel];
236
+ const key = state.keys[channel] ?? {
237
+ text: "",
238
+ named: false,
239
+ ref: channel === "monid" ? DEFAULT_MONID_KEY_REF : DEFAULT_API_KEY_REF
240
+ };
237
241
  const keyChannel = channel === "monid" ? FIELD.monidApiKey : FIELD.apiKey;
238
242
  const searchOn = switchValue(state.fields[FIELD.search]?.text ?? "");
239
243
  const fetchOn = switchValue(state.fields[FIELD.fetch]?.text ?? "");
@@ -270,6 +274,24 @@ function TinyfishCard(props) {
270
274
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("channelHint") }),
271
275
  resetButton(FIELD.channel, state.fields[FIELD.channel]?.overridden ?? false)
272
276
  ] }),
277
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
278
+ t("apiKey"),
279
+ ":",
280
+ " ",
281
+ state.keys.direct?.named ?? false ? t("apiKeySet") : t("apiKeyUnset"),
282
+ " ",
283
+ "(",
284
+ state.keys.direct?.ref ?? DEFAULT_API_KEY_REF,
285
+ ") · ",
286
+ t("monidApiKey"),
287
+ ":",
288
+ " ",
289
+ state.keys.monid?.named ?? false ? t("apiKeySet") : t("apiKeyUnset"),
290
+ " ",
291
+ "(",
292
+ state.keys.monid?.ref ?? DEFAULT_MONID_KEY_REF,
293
+ ")"
294
+ ] }),
273
295
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SettingsSecretField, {
274
296
  id: `plugin-config-tinyfish-${keyChannel}`,
275
297
  label: channel === "monid" ? t("monidApiKey") : t("apiKey"),
@@ -363,11 +385,13 @@ function apply(ctx) {
363
385
  keys: {
364
386
  direct: {
365
387
  text: model.field(FIELD.apiKey).text,
366
- named: refOf(scope.getSnapshot(), "direct") !== DEFAULT_API_KEY_REF
388
+ named: refOf(scope.getSnapshot(), "direct") !== DEFAULT_API_KEY_REF,
389
+ ref: refOf(scope.getSnapshot(), "direct")
367
390
  },
368
391
  monid: {
369
392
  text: model.field(FIELD.monidApiKey).text,
370
- named: refOf(scope.getSnapshot(), "monid") !== DEFAULT_MONID_KEY_REF
393
+ named: refOf(scope.getSnapshot(), "monid") !== DEFAULT_MONID_KEY_REF,
394
+ ref: refOf(scope.getSnapshot(), "monid")
371
395
  }
372
396
  }
373
397
  }));
package/lib/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
2
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
3
3
  import z from "@deepseek-ai/schemastery";
4
+ import { createHash } from "node:crypto";
4
5
  import { readFileSync } from "node:fs";
5
6
  import { homedir } from "node:os";
6
7
  import { join } from "node:path";
@@ -235,7 +236,10 @@ async function call(url, options) {
235
236
  if (isAbortError(error)) throw aborted(signal, error);
236
237
  throw new WebError(`TinyFish request to ${url} failed: ${String(error)}`, WEB_PROVIDER_ERROR, { cause: error });
237
238
  }
238
- if (response.status === 401 || response.status === 403) throw new WebError(`TinyFish rejected the ${channel} API key (HTTP ${response.status}). Refresh it, or switch the provider's channel.`, WEB_PROVIDER_ERROR);
239
+ if (response.status === 401 || response.status === 403) {
240
+ const fingerprint = createHash("sha256").update(key, "utf8").digest("hex").slice(0, 12);
241
+ throw new WebError(`TinyFish rejected the ${channel} API key (HTTP ${response.status}, key sha256:${fingerprint}). Refresh it, or switch the provider's channel.`, WEB_PROVIDER_ERROR);
242
+ }
239
243
  if (response.status === 429) throw new TransientWebError("TinyFish rate limit reached (HTTP 429).", WEB_PROVIDER_ERROR);
240
244
  if (!response.ok) throw new WebError(`TinyFish returned HTTP ${response.status} for ${url}`, WEB_PROVIDER_ERROR);
241
245
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tinyfish",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "TinyFish-backed search and fetch providers for the DeepSeek Harness web capability seam (ctx.web) — $0 SERP and page extraction, direct or via Monid.",
5
5
  "keywords": [
6
6
  "cordis",
@@ -26,7 +26,8 @@
26
26
  "lib/client.js",
27
27
  "cordis.patch.yml",
28
28
  "README.md",
29
- "LICENSE"
29
+ "LICENSE",
30
+ "assets/tinyfish.svg"
30
31
  ],
31
32
  "type": "module",
32
33
  "main": "lib/index.mjs",
@@ -44,9 +45,9 @@
44
45
  }
45
46
  },
46
47
  "scripts": {
47
- "build": "vp pack && node scripts/name-client-bundle.mjs",
48
+ "build": "vp pack && node scripts/name-client-bundle.ts",
48
49
  "check": "vp check",
49
- "ci": "vp check && vp test && node scripts/check.mjs",
50
+ "ci": "vp check && vp test && node scripts/check.ts",
50
51
  "clean": "rm -rf lib",
51
52
  "format": "vp fmt --check",
52
53
  "format:fix": "vp fmt --write",
@@ -74,7 +75,8 @@
74
75
  "react": "^18.3.1",
75
76
  "typescript": "^7.0.2",
76
77
  "ultracite": "^7.12.1",
77
- "vite-plus": "catalog:"
78
+ "vite-plus": "catalog:",
79
+ "vitest": "5.0.1"
78
80
  },
79
81
  "peerDependencies": {
80
82
  "@deepseek-ai/cordis": "~4.0.4",