dsh-packycode-api 1.3.5 → 1.3.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.7
4
+
5
+ - **The CLI card also offers the update when a newer Claude Code is out.** Until now **Update Claude Code CLI** only appeared for a CLI older than 2.1.259. The plugin now reads the newest release on the host, from `https://registry.npmjs.org/@anthropic-ai/claude-code/latest` (npm's `latest` and the native installer's `latest` channel named the same 2.1.303 when checked), with a 10-second timeout and redirects refused, at most once every 30 minutes, again after 2 minutes when it got no answer, and at once when **Check again** is clicked. A CLI that works but is behind gets a note naming the new version, the same button and the command it runs; the installed version keeps working, so it is not marked too old. Without an answer from npm no update is offered. The update itself is unchanged: `claude update` for the native installer, npm with `--allow-scripts` for an npm install. Its outcome is now judged for a CLI that already worked: a run that changed nothing while a newer version exists fails with the installer's own output (for example Homebrew's `brew upgrade claude-code`) instead of reading as "updated", a failed command that changed nothing reports that failure, and a clean run with the newest release unknown says the version is unchanged.
6
+ - Validation, 2026-10-19: 467 offline tests (new: the newest-release reader's URL, timeout, refused redirects, cache, forced re-read and silent failures; `updateAvailable` for a CLI behind, at, or without a newest release; an update from behind to latest; a clean run that left the CLI behind, a failed run that changed nothing, and a no-op run; the card's note, button, progress and outcomes), both rebuilt bundles and the versioned tarball. Live, the built manager read this device's native CLI as 2.1.295 and npm's latest as 2.1.303 and offered the update (887 ms for both reads). In a scratch HOME and npm prefix, an npm install of 2.1.295 was offered the update with the npm command, and the built `update()` took it to 2.1.303 in 36 s, after which no update was offered; the device's own CLI stayed untouched. The native `claude update` path was not run again; it is unchanged from 1.3.5. The card was checked through the client tests, not in DeepSeek Harness.
7
+ - **GPT web search works again.** With the GPT backend, every search failed with "PackyCode search returned no web_search_call". The search did run: PackyCode's Codex channel answers a non-streaming `/v1/responses` request with `status: completed`, `tool_usage.web_search.num_requests: 1` and about 16K input tokens, but an empty `output`. Streamed, the same request carries the preamble, each `web_search_call` and the cited answer as `response.output_item.done` events, then ends on a `response.completed` whose `output` is empty as well. The GPT backend now streams (`stream: true`) and rebuilds the output from those events in `output_index` order; a final event that does carry output is used as it is, and a channel that answers with one JSON body is read as before. Sources and the summary come from the same items as before. A body with no output at all now says the gateway did not pass the result on, instead of claiming the model ran no search.
8
+ - **A full upstream is resent inside the search.** The channel relays OpenAI's "Temporary capacity limit. Please try again in 2s." (`rate_limit_exceeded`) and its `server_error` inside the 200 stream, before any output, so the gateway cannot resend them itself. The search resends on the same key after the pause the message names (1 s when it names none; a pause over 5 s is reported instead), at most twice, as the official Codex client resends them. Quota and key wording are never resent, and no key is moved aside for this. When it persists, the error names the model and suggests the DeepSeek backend. Any other failure relayed in the stream is reported at once as "failed upstream", not as HTTP 200.
9
+ - **Every GPT search sends a `prompt_cache_key`.** The Codex group answers a Responses request without one with a 403 ("请使用标准 Codex 客户端"; 3 of 3 live, while 3 of 3 with one were accepted). DSH's searches already carried the session id or the plugin's fallback id; a caller that names no session, such as `scripts/live-check.mjs --with-search`, now gets one id per provider, as the model route does.
10
+ - Validation, 2026-10-21: 480 offline tests (new: the empty-output message; the stream reader rebuilding the output in index order, keeping a final event's own output, surviving CRLF, bare CR and chunks split mid-line and mid-character, turning `error` / `response.failed` into the body error and refusing a stream without a final event; which upstream errors are resent and after which pause; a GPT search that streams, always sends a cache key, resends a capacity refusal on the same key without blaming it, reports a persisting one after three attempts and any other one at once; cancellation during the pause and mid-stream), both rebuilt bundles and the versioned tarball. Against a local server, a stream that stalls ends at the request timeout as "no answer within", and a cancel ends it as `WEB_ABORTED`. Live with a key in `codex`: non-streaming `gpt-6.1-sol` searches returned `completed` with an empty `output` 3 of 3 times, and the streams checked for it ended on an empty final `output` with every item in `response.output_item.done`. 28 live GPT searches parsed through the rebuild; through the built provider they returned 13–20 sources and a 1.0–1.8K-character summary in 14.6–29.9 s (one more ran past the test script's own 30 s cancel; DSH's desktop presets give `web_search` 60 s). `node scripts/live-check.mjs --with-search` passed every HTTP model, the rotation and the classification; DeepSeek search returned 10 sources in 6.0 s, and GPT search met the upstream capacity limit on all three attempts and reported it with the new message. The capacity limit is upstream: `gpt-6.1-sol`, `gpt-5.6-sol` and `gpt-6-astra` searches each met it about once in five, plain requests never. One existing CLI test that aborts after 300 ms failed in two full runs while the machine was heavily loaded (it also fails on its own at the unchanged HEAD); the final full run passed all 480.
11
+
12
+ ## 1.3.6
13
+
14
+ - **DeepSeek Flash runs over Responses.** `deepseek-flash` moves from `openai-completions` to `openai-responses`; the base URL stays `https://cf.api.fan/v1`, so requests now go to `/v1/responses` instead of `/v1/chat/completions`. PackyCode's `/api/pricing` lists `openai-response` among the model's endpoints, for the same groups (`deepseek-officially`, `deepseek-sale`). The effort levels are unchanged (`off`, `low`, `high`, `max`), and so is `thinkingLevelMap`: `off` now sends `reasoning.effort: "none"`, which the gateway answers with no reasoning item and zero reasoning tokens. The Chat Completions dialect (`thinkingFormat: 'deepseek'`, `max_tokens`, reasoning content on assistant messages) is gone with it; reasoning comes back as `reasoning_text` items, which pi-ai streams as thinking and sends back with the turn's tool calls. Like Grok, DeepSeek omits `prompt_cache_key`: the gateway accepts the field, but its prefix cache already hits without it. Image input is sent as `input_image`. The 384,000-token output ceiling stays under the gateway's Responses limit of 393,216. No catalog model uses Chat Completions any more, so the provider no longer registers that API; the group probe still uses `/v1/chat/completions`, which needs no model. A session that ran on Chat Completions continues over Responses: pi-ai sends its earlier thinking as text and keeps its tool calls.
15
+ - Validation, 2026-10-19: 461 offline tests (new: DeepSeek's Responses efforts and `off` → `none` without a cache key, reasoning streamed as thinking and replayed with its tool call, the vision payload as `input_image`, and the key-choice gateway answering on Responses), both rebuilt bundles and the versioned tarball. Live against the gateway with a `deepseek-officially` key: direct `/v1/responses` requests for `deepseek-flash` returned 200 at `none`, `minimal`, `low`, `medium`, `high`, `xhigh` and `max`, with no reasoning item at `none`; `max_output_tokens` up to 393,216 was accepted and 500,000 refused. Through the plugin's provider, four-turn tool conversations passed at `high`, `max`, `off`, mixed efforts, and with replayed reasoning items (up to three in one request); the prefix cache hit without `prompt_cache_key`; a conversation started over Chat Completions continued over Responses. `scripts/live-check.mjs` passed for all ten models, key rotation (an invalid key on `deepseek-flash` was disabled and the real key answered) and the RPC projections, and `--model deepseek-flash --with-vision` named both color fixtures. Not checked in DeepSeek Harness itself; no DSH restart.
16
+
3
17
  ## 1.3.5
4
18
 
5
19
  - **An outdated Claude Code CLI is caught before the request, and the CLI card updates it.** 1.3.4 always passes `--permission-prompts none`, which Claude Code added in 2.1.259 (2026-09-02). A colleague's CLI from the official installer was 2.1.223 (2026-08-05): it rejected the option at start-up, every `cc` Claude turn failed with "the installed Claude Code CLI is too old", and the card still showed it as installed. The plugin now requires 2.1.259 or later. An older detected CLI is refused before it is started, with the detected and required versions; a cached detection that looks outdated is probed again first, so a CLI updated in a terminal is used at once. The card marks such a CLI **too old** and offers **Update Claude Code CLI**. A CLI from the official installer (or any install that is not npm's) is updated with `claude update`, run with the plugin's own CLI home and without the user's `ANTHROPIC_*` / `CLAUDE_*` / `DISABLE_*` variables. An npm global install is updated by npm itself — the npm beside the CLI, which owns its prefix — with `npm install -g @anthropic-ai/claude-code@latest --allow-scripts=@anthropic-ai/claude-code`. The update succeeds only when the CLI detected afterwards is new enough; otherwise the card says whether it stayed old (for example a Homebrew install), stopped running, or could not download, with the command's output. The CLI's flags and environment for requests are unchanged.
package/README.en.md CHANGED
@@ -47,10 +47,10 @@ You need **DeepSeek Harness** and a **PackyCode (PackyAPI) API key**. For DSH se
47
47
  Open the desktop application's **Plugins** page, click **Add plugin**, and enter the package name **with a version** as the **package name or address**:
48
48
 
49
49
  ```text
50
- dsh-packycode-api@1.3.5
50
+ dsh-packycode-api@1.3.7
51
51
  ```
52
52
 
53
- Choose the **official** install source, then click **Install**. `1.3.5` was the latest release when this was written; the [npm page](https://www.npmjs.com/package/dsh-packycode-api) shows the current one.
53
+ Choose the **official** install source, then click **Install**. `1.3.7` was the latest release when this was written; the [npm page](https://www.npmjs.com/package/dsh-packycode-api) shows the current one.
54
54
 
55
55
  > **Always include the version.** With a bare `dsh-packycode-api`, the install prompt shows the newest version on npm, but DSH's bundled pnpm by default installs only versions published at least 24 hours ago, so you can end up several releases behind with no sign of it in the prompt. With an exact version, pnpm installs that version and adds it to the profile's `minimumReleaseAgeExclude` itself. Afterwards, check **Current version** under **Settings → PackyCode → Plugin version**.
56
56
 
@@ -71,7 +71,7 @@ Choose a model from the `PackyCode` group. All ten models use the plugin’s HTT
71
71
 
72
72
  The **Plugins** page shows the installed state and version, and is also where you enable, disable, or uninstall.
73
73
 
74
- **For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.3.5`. Do not use it for `desktop` — the CLI refuses it (`profile "desktop" is managed exclusively by the Electron application`).
74
+ **For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.3.7`. Do not use it for `desktop` — the CLI refuses it (`profile "desktop" is managed exclusively by the Electron application`).
75
75
 
76
76
  For a local checkout, keep the `link:` prefix: `dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`. **Do not pass a bare path or `file:`** — that install copies the whole checkout, `node_modules` included, into the profile, where it then shadows the packages the DSH installation itself provides and the plugin looks uninstalled; see the FAQ. If you already installed that way, uninstall first and reinstall as above.
77
77
 
@@ -89,7 +89,7 @@ A profile without a browser can still use the model route. It just has no settin
89
89
 
90
90
  **Read the balance.** Each key under **Accounts and keys** shows its own remaining, used, and limit (limit = remaining + used); an unlimited key shows **No quota limit**. Keys are not added up into a total. An API key can read only its own quota, not the PackyCode account's balance, so check the account balance in the PackyCode console. A key that could not be read says why on its own row; a key whose read failed this time but succeeded before keeps showing its last balance with the time it was read (the composer shows it as `≈US$…`) until DSH restarts. The composer shows the active key's remaining balance only while a PackyCode model is selected. **Refresh** bypasses the roughly 60-second cache. The default model host is `https://cf.api.fan` and the balance host is `https://www.packyapi.ai`; both can be changed under **Advanced: endpoints**. The old default, `https://www.packyapi.com`, is DNS-poisoned on mainland networks and answers `fetch failed` without a proxy; even when settings still name it, `.ai` is tried first.
91
91
 
92
- **Use Claude.** Select any Claude model. When the key is in the `cc` group, the plugin sends the request with this device's official Claude Code CLI, so install it first under **Settings → PackyCode → Claude Code CLI** (or run `npm install -g @anthropic-ai/claude-code --allow-scripts=@anthropic-ai/claude-code`), and click **Update Claude Code CLI** there when it is too old; keys in `cc-sale`, `cc-expensive`, `claude-officially` and other groups use the plugin's HTTP route and need no CLI. Under **Accounts and keys**, every key that reaches Claude is marked “Claude via official CLI” or “Claude via HTTP”; the groups alone decide the route, and it cannot be switched by hand. On both routes DSH executes every tool under its own permissions, and plan mode, questions, todos, subagents, attachments, reasoning effort and history behave as for any other model. See “Claude Code CLI route” below.
92
+ **Use Claude.** Select any Claude model. When the key is in the `cc` group, the plugin sends the request with this device's official Claude Code CLI, so install it first under **Settings → PackyCode → Claude Code CLI** (or run `npm install -g @anthropic-ai/claude-code --allow-scripts=@anthropic-ai/claude-code`), and click **Update Claude Code CLI** there when it is too old or a newer version is out; keys in `cc-sale`, `cc-expensive`, `claude-officially` and other groups use the plugin's HTTP route and need no CLI. Under **Accounts and keys**, every key that reaches Claude is marked “Claude via official CLI” or “Claude via HTTP”; the groups alone decide the route, and it cannot be switched by hand. On both routes DSH executes every tool under its own permissions, and plan mode, questions, todos, subagents, attachments, reasoning effort and history behave as for any other model. See “Claude Code CLI route” below.
93
93
 
94
94
  **Web search.** DSH's `web_search` calls DeepSeek's official API by default (billed to `DEEPSEEK_API_KEY`). **Settings → PackyCode → Web search** can route it through PackyCode instead:
95
95
 
@@ -139,7 +139,7 @@ PackyCode's `cc` group (“claude code专用”) only serves the official Claude
139
139
  - **DSH's prompt and history, unchanged.** DSH's system prompt is appended to the CLI's own; the history (text, images, signed thinking, tool calls and results) is written as a CLI transcript and resumed; the reasoning effort is passed as `--effort`.
140
140
  - **One long-lived process per conversation.** A DSH session keeps one CLI process per model: tool results and the next user turn continue it, so requests keep one cacheable prefix. After compaction, a plan-mode switch or a key change, a new process is resumed from DSH's full history; session titles and compaction use one-shot processes. A process idle for 20 minutes stops; at most 8 stay alive.
141
141
  - **Isolated from your own Claude Code.** It runs with its own config home, `~/.dsh/packycode-claude-cli`, and never reads `~/.claude` settings, CLAUDE.md, MCP configuration or logins; only the key stored in the plugin is passed to the child. Auto-update, telemetry, auto-compaction, auto-memory and model fallback are off, and retries are left to DSH's retry policy, which rotates keys.
142
- - **An outdated CLI is one click away from current.** The plugin needs Claude Code 2.1.259 or later (the first release with the `--permission-prompts` option it passes). An older CLI is stopped before the request with an explanation, and the CLI card marks it too old and offers **Update Claude Code CLI**: an install from the official installer (`~/.local/bin/claude`) runs `claude update`; an npm global install runs `npm install -g @anthropic-ai/claude-code@latest --allow-scripts=@anthropic-ai/claude-code`, because npm 12 skips install scripts by default and `claude update` does not allow them on an npm install, which leaves a CLI that cannot run. Your Claude Code is only updated when you click that button.
142
+ - **An outdated CLI is one click away from current.** The plugin needs Claude Code 2.1.259 or later (the first release with the `--permission-prompts` option it passes). An older CLI is stopped before the request with an explanation, and the CLI card marks it too old and offers **Update Claude Code CLI**: an install from the official installer (`~/.local/bin/claude`) runs `claude update`; an npm global install runs `npm install -g @anthropic-ai/claude-code@latest --allow-scripts=@anthropic-ai/claude-code`, because npm 12 skips install scripts by default and `claude update` does not allow them on an npm install, which leaves a CLI that cannot run. When the CLI works but is not the latest release, the card names the new version and offers the same button: the plugin reads `latest` of `@anthropic-ai/claude-code` from npm on this device (the same version as the native installer's `latest` channel), at most once every 30 minutes, and again when you click Check again; without an answer it offers nothing. Your Claude Code is only updated when you click that button.
143
143
  - **Failures say why.** A missing CLI, an outdated CLI, and a request from the official CLI that the gateway still refuses each get their own explanation; nothing switches to another route or provider, and no key is disabled.
144
144
 
145
145
  Known differences: starting a process takes about 0.3–1 s; when a process is resumed in the middle of a tool turn, the CLI adds “Continue from where you left off.” after the tool results, as the official client does. Requires Claude Code 2.1.259 or later (tested with 2.1.295 and 2.1.303). On Windows, Claude Code also needs Git for Windows.
@@ -170,7 +170,7 @@ Live checks on 2026-10-15 passed generation and editing at `low` on all three en
170
170
 
171
171
  | Model | How it runs | Effort | Token group |
172
172
  | --- | --- | --- | --- |
173
- | `deepseek-flash` | HTTP, chat/completions, images allowed | Can be off; `low` / `high` / `max` | `deepseek-officially`, `deepseek-sale` |
173
+ | `deepseek-flash` | HTTP, Responses, images allowed | Can be off; `low` / `high` / `max` | `deepseek-officially`, `deepseek-sale` |
174
174
  | `gpt-6.1-sol` | HTTP, Responses, images allowed | Cannot be off; `low` / `medium` / `high` / `xhigh` / `max` | `codex`, `codex-sale` |
175
175
  | `gpt-6-astra` | Same | Same | Same |
176
176
  | `gpt-5.6-sol` | Same | Same | Same |
@@ -183,7 +183,7 @@ Live checks on 2026-10-15 passed generation and editing at `low` on all three en
183
183
 
184
184
  A model in the list does not mean the current token group allows it. When changing groups, do not drop the group that serves the model you still want.
185
185
 
186
- All ten models in the current catalog declare text and image input. DeepSeek Flash follows the [official vision guide](https://api-docs.deepseek.com/guides/vision/); images are read through DSH's attachment service and sent with the Chat Completions request.
186
+ All ten models in the current catalog declare text and image input. DeepSeek Flash follows the [official vision guide](https://api-docs.deepseek.com/guides/vision/); images are read through DSH's attachment service and sent with the Responses request.
187
187
 
188
188
  Gemini capabilities and limits follow Google's model pages: [Gemini 4 Argon](https://ai.google.dev/gemini-api/docs/models/gemini-4-argon), [Gemini 3.8 Flash](https://ai.google.dev/gemini-api/docs/models/gemini-3.8-flash). Both models use the native Gemini protocol through `/v1beta/models/{model}:streamGenerateContent`, streaming text, thinking and tool calls; images are read through DSH's attachment service. Thinking levels are sent with `thinkingLevel`. Both routes use the single-stream output limit without automatic continuation beyond 65,536 tokens. Available endpoints and groups follow the [PackyAPI live model catalog](https://www.packyapi.com/api/pricing).
189
189
 
@@ -191,7 +191,9 @@ Gemini capabilities and limits follow Google's model pages: [Gemini 4 Argon](htt
191
191
 
192
192
  Grok 4.7's context and reasoning levels follow the [official guide](https://docs.x.ai/developers/grok-4-7). There is no separate text-output limit; the plugin uses the context size as its numeric output ceiling, bounded by the remaining context per request.
193
193
 
194
- Only GPT Responses requests keep `prompt_cache_key`; other Responses models omit it by default. This gateway's Grok route reports a misleading `Incorrect API key provided` error when the field is present.
194
+ Only GPT Responses requests keep `prompt_cache_key`; other Responses models omit it by default. This gateway's Grok route reports a misleading `Incorrect API key provided` error when the field is present, and DeepSeek Flash hits its prefix cache without it.
195
+
196
+ DeepSeek Flash uses the gateway's Responses endpoint (`/api/pricing` lists `openai-response`). With effort off it sends `reasoning.effort: "none"`, and the gateway answers without reasoning; otherwise reasoning comes back as `reasoning_text` and is sent back with the same turn's tool calls.
195
197
 
196
198
  <details>
197
199
  <summary>Protocol, group and client compatibility</summary>
@@ -214,9 +216,9 @@ Only GPT Responses requests keep `prompt_cache_key`; other Responses models omit
214
216
 
215
217
  ## Update and uninstall
216
218
 
217
- > **Upgrading from 1.3.2 or earlier:** install `dsh-packycode-api@1.3.5` on the Plugins page instead of clicking **Update plugin** in the old settings card. The old button bypasses pnpm's release-age policy: if the version it installs is less than 24 hours old, the profile refuses every other plugin install until that version is 24 hours old. After the install and a restart, the button is the fixed one, and later updates can use it.
219
+ > **Upgrading from 1.3.2 or earlier:** install `dsh-packycode-api@1.3.7` on the Plugins page instead of clicking **Update plugin** in the old settings card. The old button bypasses pnpm's release-age policy: if the version it installs is less than 24 hours old, the profile refuses every other plugin install until that version is 24 hours old. After the install and a restart, the button is the fixed one, and later updates can use it.
218
220
 
219
- **Update:** open **Settings → PackyCode** and, when a newer version is listed, click **Update plugin**. It installs that exact version into the owning profile with DSH's bundled pnpm and reads `package.json` back before reporting success. The install keeps pnpm's release-age policy instead of overriding it: by default pnpm installs the version at once and records it in the profile's `minimumReleaseAgeExclude`; under a stricter pnpm policy the button says when the update can run. A local `link:` checkout does not get the button. Then quit and reopen the desktop application. A page refresh does not reload the Host. Reinstalling from the **Plugins** page works too, again with the version in the package name (for example `dsh-packycode-api@1.3.5`); without it you may get a version that is merely the newest one published at least 24 hours ago.
221
+ **Update:** open **Settings → PackyCode** and, when a newer version is listed, click **Update plugin**. It installs that exact version into the owning profile with DSH's bundled pnpm and reads `package.json` back before reporting success. The install keeps pnpm's release-age policy instead of overriding it: by default pnpm installs the version at once and records it in the profile's `minimumReleaseAgeExclude`; under a stricter pnpm policy the button says when the update can run. A local `link:` checkout does not get the button. Then quit and reopen the desktop application. A page refresh does not reload the Host. Reinstalling from the **Plugins** page works too, again with the version in the package name (for example `dsh-packycode-api@1.3.7`); without it you may get a version that is merely the newest one published at least 24 hours ago.
220
222
 
221
223
  **Coming back from a local-path install, uninstall first.** An update is a `pnpm add`: it rewrites the files inside the package directory and leaves a `node_modules` that an earlier local-path install left there. That older tree keeps shadowing the packages the DSH installation provides, so the plugin row fails to load with no visible reason. **Uninstall** runs `pnpm remove`, which deletes the package directory outright — only a reinstall after that is clean. `node scripts/install-check.mjs` reports whether a profile is in that state.
222
224
 
@@ -232,7 +234,7 @@ ls ~/.dsh/profiles/desktop/node_modules/dsh-packycode-api/node_modules
232
234
 
233
235
  Output means a leftover tree: a registry install ships no `node_modules`, so this one usually comes from an earlier local-path install (`file:`, or a bare path instead of `link:`). It sits inside the profile directory, and DSH resolves a plugin's imports from inside the profile first, so the plugin gets those old copies — for example `@deepseek-ai/schemastery` 3.18.2, which has no `Schema.volatile()`, so the module throws while it is imported. When an import fails the Loader creates no fiber for that row and no client bundle is composed either, so the model route, the RPC channel, and the settings panel all stay absent.
234
236
 
235
- Recovery: **uninstall** the plugin on the **Plugins** page, **install** it again with the version in the package name (for example `dsh-packycode-api@1.3.5`), then quit and reopen the desktop application. Uninstall runs `pnpm remove` and deletes the whole package directory; reinstalling or clicking **Update** only rewrites files and keeps the leftover tree. Deleting that `node_modules` by hand and restarting works too.
237
+ Recovery: **uninstall** the plugin on the **Plugins** page, **install** it again with the version in the package name (for example `dsh-packycode-api@1.3.7`), then quit and reopen the desktop application. Uninstall runs `pnpm remove` and deletes the whole package directory; reinstalling or clicking **Update** only rewrites files and keeps the leftover tree. Deleting that `node_modules` by hand and restarting works too.
236
238
 
237
239
  **Right after a first install, every card in Settings shows `transport failure for /packycode-api/…: HTTP 405`?** The installed version is 1.1.6 or earlier, usually because only the bare package name was entered: the install prompt shows the newest version, but pnpm installs the newest one published at least 24 hours ago (see **Install the plugin**). Those older versions can register their settings channel only after DSH restarts and rebuilds its `connection` service. So right after a first install `/packycode-api` does not exist yet, requests fall through to the static page, and it answers 405; the model route and the settings panel itself load normally. From 1.1.7 on, a first install needs no restart. Check which version was installed:
238
240
 
package/README.md CHANGED
@@ -47,10 +47,10 @@
47
47
  打开桌面应用的**插件**页,点击**添加插件**,在**包名或地址**中填入**带版本号**的包名:
48
48
 
49
49
  ```text
50
- dsh-packycode-api@1.3.5
50
+ dsh-packycode-api@1.3.7
51
51
  ```
52
52
 
53
- 在**安装源**中选择**官方源**,然后点击**安装**。`1.3.5` 是写这份说明时的最新版,当前最新版本号以 [npm 页面](https://www.npmjs.com/package/dsh-packycode-api) 为准。
53
+ 在**安装源**中选择**官方源**,然后点击**安装**。`1.3.7` 是写这份说明时的最新版,当前最新版本号以 [npm 页面](https://www.npmjs.com/package/dsh-packycode-api) 为准。
54
54
 
55
55
  > **一定要带版本号。** 只填 `dsh-packycode-api` 时,安装前的提示显示的是 npm 上的最新版,但 DSH 自带的 pnpm 默认只安装发布满 24 小时的版本,实际装上的可能是好几个版本之前的旧版,而安装提示里看不出这一点。填写精确版本时,pnpm 会直接安装这个版本,并自动把它加入 profile 的 `minimumReleaseAgeExclude`。装好后可以在 **设置 → PackyCode → 插件版本** 核对「当前版本」。
56
56
 
@@ -71,7 +71,7 @@ dsh-packycode-api@1.3.5
71
71
 
72
72
  桌面应用的**插件**页可以看到已安装状态与版本,也可以在那里启用、停用或卸载。
73
73
 
74
- **仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.3.5`。不要对 `desktop` 使用它 —— CLI 会直接拒绝(`profile "desktop" is managed exclusively by the Electron application`)。
74
+ **仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.3.7`。不要对 `desktop` 使用它 —— CLI 会直接拒绝(`profile "desktop" is managed exclusively by the Electron application`)。
75
75
 
76
76
  本地开发要加 `link:` 前缀:`dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`。**不要直接写路径(或 `file:`)** —— 那样的安装会把整个仓库目录(连同 `node_modules`)复制进 profile,之后它会在 profile 内部挡住 DSH 自带的包,插件看起来像没装,见「常见问题」。已经那样装过的话,先卸载再按上面的方式重装。
77
77
 
@@ -89,7 +89,7 @@ dsh-packycode-api@1.3.5
89
89
 
90
90
  **看余额。** 「账户与 Key」里每个 Key 显示它自己的剩余、已用与额度(额度 = 剩余 + 已用),不限额的 Key 显示「不限额度」;多个 Key 不相加成合计。API Key 只能读到自己的额度,读不到 PackyCode 账号的总余额,账号余额请到 PackyCode 控制台查看。读失败的 Key 会在它那一行注明原因;某个 Key 这次读失败但之前读到过,会继续显示上次的余额并注明读取时间(输入框里显示为 `≈US$…`),重启 DSH 后才清空。输入框右侧只在选中 PackyCode 模型时显示当前 Key 的剩余额度。「刷新」会跳过约 60 秒的缓存。默认模型接口是 `https://cf.api.fan`,余额接口是 `https://www.packyapi.ai`,可在「高级:接口地址」里改。旧默认值 `https://www.packyapi.com` 在国内网络下 DNS 被污染、直连会 `fetch failed`,即使设置里保存的是它,插件也会先试 `.ai`。
91
91
 
92
- **用 Claude。** 直接选择三个 Claude 模型之一。Key 在 `cc` 分组时,插件用本机的官方 Claude Code CLI 发出请求,需要先在 **设置 → PackyCode → Claude Code CLI** 一键安装(或自行 `npm install -g @anthropic-ai/claude-code --allow-scripts=@anthropic-ai/claude-code`),版本过低时在同一处点「更新 Claude Code CLI」;`cc-sale`、`cc-expensive`、`claude-officially` 等分组直接走插件内 HTTP,不需要 CLI。「账户与 Key」里每个能用 Claude 的 Key 会标出「Claude 经官方 CLI」或「Claude 经 HTTP」,通道只由分组决定,不能手动切换。无论哪条通道,工具都由 DSH 执行并按 DSH 的权限审批,计划模式、提问、待办、子代理、附件、思考档位和历史都和其它模型一样。详见下方「Claude Code CLI 通道」。
92
+ **用 Claude。** 直接选择三个 Claude 模型之一。Key 在 `cc` 分组时,插件用本机的官方 Claude Code CLI 发出请求,需要先在 **设置 → PackyCode → Claude Code CLI** 一键安装(或自行 `npm install -g @anthropic-ai/claude-code --allow-scripts=@anthropic-ai/claude-code`),版本过低或有新版本时在同一处点「更新 Claude Code CLI」;`cc-sale`、`cc-expensive`、`claude-officially` 等分组直接走插件内 HTTP,不需要 CLI。「账户与 Key」里每个能用 Claude 的 Key 会标出「Claude 经官方 CLI」或「Claude 经 HTTP」,通道只由分组决定,不能手动切换。无论哪条通道,工具都由 DSH 执行并按 DSH 的权限审批,计划模式、提问、待办、子代理、附件、思考档位和历史都和其它模型一样。详见下方「Claude Code CLI 通道」。
93
93
 
94
94
  **网页搜索。** DSH 的 `web_search` 默认调用 DeepSeek 官方接口(用 `DEEPSEEK_API_KEY` 计费)。在 **设置 → PackyCode → 网页搜索** 里可以改走 PackyCode:
95
95
 
@@ -139,7 +139,7 @@ PackyCode 的 `cc` 分组(「claude code专用」)只接受官方 Claude Cod
139
139
  - **提示与历史保持 DSH 原样。** DSH 的系统提示追加在 CLI 官方提示之后;历史(文字、图片、带签名的思考、工具调用与结果)由插件写成 CLI 的会话记录后续接,思考档位以 `--effort` 传入。
140
140
  - **一个会话一个常驻进程。** 同一个 DSH 会话、同一模型复用一个 CLI 进程:工具结果和下一轮消息直接续接,请求前缀不变,网关缓存照常命中。历史被压缩、切换计划模式或换 Key 时,用 DSH 的完整历史恢复一个新进程;会话标题和压缩用一次性进程。空闲 20 分钟的进程自动结束,最多同时保留 8 个。
141
141
  - **与你自己的 Claude Code 隔离。** 使用独立配置目录 `~/.dsh/packycode-claude-cli`,不读取 `~/.claude` 的设置、CLAUDE.md、MCP 配置和登录状态;只把插件保存的 Key 交给子进程。自动更新、遥测、自动压缩、自动记忆和模型回退都关闭,失败重试交还给 DSH 的重试策略(会按 Key 轮换)。
142
- - **版本过低一键更新。** 插件需要 Claude Code 2.1.259 或更新版本(插件传入的 `--permission-prompts` 从这一版开始提供)。更旧的 CLI 在请求发出前就被拦下并说明原因,CLI 卡片标出「版本过低」并提供「更新 Claude Code CLI」:官方安装器(`~/.local/bin/claude`)装的执行 `claude update`;npm 全局安装的执行 `npm install -g @anthropic-ai/claude-code@latest --allow-scripts=@anthropic-ai/claude-code`,因为 npm 12 默认不运行安装脚本,而 `claude update` 在 npm 安装上不会放行脚本,会留下一个无法运行的 CLI。只有点这个按钮时才会更新你的 Claude Code。
142
+ - **版本过低一键更新。** 插件需要 Claude Code 2.1.259 或更新版本(插件传入的 `--permission-prompts` 从这一版开始提供)。更旧的 CLI 在请求发出前就被拦下并说明原因,CLI 卡片标出「版本过低」并提供「更新 Claude Code CLI」:官方安装器(`~/.local/bin/claude`)装的执行 `claude update`;npm 全局安装的执行 `npm install -g @anthropic-ai/claude-code@latest --allow-scripts=@anthropic-ai/claude-code`,因为 npm 12 默认不运行安装脚本,而 `claude update` 在 npm 安装上不会放行脚本,会留下一个无法运行的 CLI。CLI 能用但不是最新版时,卡片也会提示新版本并提供同一个按钮:插件在本机读取 npm 上 `@anthropic-ai/claude-code` 的 `latest`(与官方安装器的 `latest` 通道是同一版本),30 分钟内最多读一次,点「重新检测」立即重读;读不到时不提示。只有点这个按钮时才会更新你的 Claude Code。
143
143
  - **出错会说清原因。** 未安装 CLI、CLI 版本过旧、官方 CLI 发出的请求仍被网关拒绝,分别给出说明和下一步,不会改走其它通道或提供商,也不会停用 Key。
144
144
 
145
145
  已知差异:新进程启动约 0.3–1 秒;在一轮工具调用中途恢复进程时,CLI 会按官方行为在工具结果后附上一句 “Continue from where you left off.”。需要 Claude Code 2.1.259 或更新版本(实测 2.1.295 与 2.1.303)。Windows 上 Claude Code 还需要 Git for Windows。
@@ -170,7 +170,7 @@ Claude 与其它模型一样,通过 DSH 的 `packycode_image_generate` 工具
170
170
 
171
171
  | 模型 | 怎么调用 | 推理档位 | 适合的令牌分组 |
172
172
  | --- | --- | --- | --- |
173
- | `deepseek-flash` | HTTP,chat/completions,可带图 | 可关;`low` / `high` / `max` | `deepseek-officially`、`deepseek-sale` |
173
+ | `deepseek-flash` | HTTP,Responses,可带图 | 可关;`low` / `high` / `max` | `deepseek-officially`、`deepseek-sale` |
174
174
  | `gpt-6.1-sol` | HTTP,Responses,可带图 | 不能关;`low` / `medium` / `high` / `xhigh` / `max` | `codex`、`codex-sale` |
175
175
  | `gpt-6-astra` | 同上 | 同上 | 同上 |
176
176
  | `gpt-5.6-sol` | 同上 | 同上 | 同上 |
@@ -183,7 +183,7 @@ Claude 与其它模型一样,通过 DSH 的 `packycode_image_generate` 工具
183
183
 
184
184
  列表里看得到模型,不代表当前令牌分组一定放行。换分组时不要把对应分组一起去掉。
185
185
 
186
- 当前目录中的 10 个模型都声明了文字和图像输入。DeepSeek Flash 的识图能力按 [官方图像理解文档](https://api-docs.deepseek.com/zh-cn/guides/vision/) 配置;图片通过 DSH 附件服务读取,再随 chat/completions 请求发送。
186
+ 当前目录中的 10 个模型都声明了文字和图像输入。DeepSeek Flash 的识图能力按 [官方图像理解文档](https://api-docs.deepseek.com/zh-cn/guides/vision/) 配置;图片通过 DSH 附件服务读取,再随 Responses 请求发送。
187
187
 
188
188
  Gemini 的能力与限制来自 Google 官方模型页:[Gemini 4 Argon](https://ai.google.dev/gemini-api/docs/models/gemini-4-argon)、[Gemini 3.8 Flash](https://ai.google.dev/gemini-api/docs/models/gemini-3.8-flash)。两款模型统一使用 Gemini 原生协议,通过 `/v1beta/models/{model}:streamGenerateContent` 流式返回文字、思考和工具调用,图片仍由 DSH 附件服务读取。思考档位通过 `thinkingLevel` 传入。两者均使用单次流式输出上限,不执行超过 65,536 token 的自动续写。可用端点与分组以 [PackyAPI 实时模型目录](https://www.packyapi.com/api/pricing) 为准。
189
189
 
@@ -191,7 +191,9 @@ Gemini 的能力与限制来自 Google 官方模型页:[Gemini 4 Argon](https:
191
191
 
192
192
  Grok 4.7 的上下文与推理档位来自 [官方文档](https://docs.x.ai/developers/grok-4-7)。官方没有单独限制文字输出长度;插件将输出上限设为上下文大小,并按每次请求的剩余上下文收紧。
193
193
 
194
- Responses 请求只有 GPT 模型保留 `prompt_cache_key`,其他 Responses 模型默认省略。当前网关的 Grok 路由会将这个字段误报成 `Incorrect API key provided`。
194
+ Responses 请求只有 GPT 模型保留 `prompt_cache_key`,其他 Responses 模型默认省略。当前网关的 Grok 路由会将这个字段误报成 `Incorrect API key provided`;DeepSeek Flash 不带它也能命中前缀缓存。
195
+
196
+ DeepSeek Flash 走网关的 Responses 接口(`/api/pricing` 列出 `openai-response`)。关闭推理时发送 `reasoning.effort: "none"`,网关返回时不带思考;开启时思考内容以 `reasoning_text` 返回,并随同一轮的工具调用一起回传。
195
197
 
196
198
  <details>
197
199
  <summary>协议、分组与客户端兼容性</summary>
@@ -214,9 +216,9 @@ Responses 请求只有 GPT 模型保留 `prompt_cache_key`,其他 Responses
214
216
 
215
217
  ## 更新与卸载
216
218
 
217
- > **从 1.3.2 或更早版本升级:** 请在插件页填写 `dsh-packycode-api@1.3.5` 安装,不要点旧版设置页里的「更新插件」。旧按钮安装时会绕过 pnpm 的发布冷却期:装到的版本如果发布还不满 24 小时,这个 profile 在它满 24 小时之前就装不了其他插件。装好并重启后,按钮就换成了修好的版本,以后直接点即可。
219
+ > **从 1.3.2 或更早版本升级:** 请在插件页填写 `dsh-packycode-api@1.3.7` 安装,不要点旧版设置页里的「更新插件」。旧按钮安装时会绕过 pnpm 的发布冷却期:装到的版本如果发布还不满 24 小时,这个 profile 在它满 24 小时之前就装不了其他插件。装好并重启后,按钮就换成了修好的版本,以后直接点即可。
218
220
 
219
- **更新:** 打开 **设置 → PackyCode**,有新版本时点「更新插件」。它会在当前 profile 目录里用 DSH 自带的 pnpm 安装精确版本,并读回 `package.json` 确认版本落地。安装遵守 pnpm 的发布冷却策略,不会绕过它:默认情况下 pnpm 会直接装上这个版本,并把它记进 profile 的 `minimumReleaseAgeExclude`;如果你给 pnpm 配了更严格的策略,按钮会提示什么时候可以更新。本地 `link:` 开发安装不会出现这个按钮。完成后完全退出并重新打开桌面应用。只刷新页面不会重载 Host。也可以在**插件**页重新安装,同样要填写带版本号的包名(例如 `dsh-packycode-api@1.3.5`),否则装上的可能是发布满 24 小时的旧版。
221
+ **更新:** 打开 **设置 → PackyCode**,有新版本时点「更新插件」。它会在当前 profile 目录里用 DSH 自带的 pnpm 安装精确版本,并读回 `package.json` 确认版本落地。安装遵守 pnpm 的发布冷却策略,不会绕过它:默认情况下 pnpm 会直接装上这个版本,并把它记进 profile 的 `minimumReleaseAgeExclude`;如果你给 pnpm 配了更严格的策略,按钮会提示什么时候可以更新。本地 `link:` 开发安装不会出现这个按钮。完成后完全退出并重新打开桌面应用。只刷新页面不会重载 Host。也可以在**插件**页重新安装,同样要填写带版本号的包名(例如 `dsh-packycode-api@1.3.7`),否则装上的可能是发布满 24 小时的旧版。
220
222
 
221
223
  **从本地路径安装切回官方源时,必须先卸载。** 「更新」是 `pnpm add`,它只重写包目录里的文件,不会删除更早一次本地路径安装留在包目录内的 `node_modules`;那份旧依赖会继续挡住 DSH 自带的包,插件行加载失败且看不出原因。「卸载」执行的是 `pnpm remove`,会删掉整个包目录,重装才是干净的。用 `node scripts/install-check.mjs` 可以确认当前 profile 是否处于这个状态。
222
224
 
@@ -232,7 +234,7 @@ ls ~/.dsh/profiles/desktop/node_modules/dsh-packycode-api/node_modules
232
234
 
233
235
  有输出就是残留:从 npm 安装的包不会自带 `node_modules`,这一份通常来自更早一次「本地路径」安装(`file:` 或直接写路径,而不是 `link:`)。它位于 profile 目录内部,而 DSH 解析插件导入时优先使用 profile 内的副本,所以插件拿到的仍是那份旧依赖 —— 例如 `@deepseek-ai/schemastery` 3.18.2 没有 `Schema.volatile()`,模块在导入阶段就抛错。导入失败时 Loader 不会为这一行创建 fiber,客户端 bundle 也不会被组合,于是模型路由、RPC 通道和设置页全都不会出现。
234
236
 
235
- 修复:到**插件**页**卸载**本插件,再用带版本号的包名重新**安装**(例如 `dsh-packycode-api@1.3.5`),然后完全退出并重新打开桌面应用。卸载执行的是 `pnpm remove`,会删掉整个包目录;直接重装或点「更新」只重写文件,不会清掉这份残留。也可以手动删掉上面那个 `node_modules` 再重启。
237
+ 修复:到**插件**页**卸载**本插件,再用带版本号的包名重新**安装**(例如 `dsh-packycode-api@1.3.7`),然后完全退出并重新打开桌面应用。卸载执行的是 `pnpm remove`,会删掉整个包目录;直接重装或点「更新」只重写文件,不会清掉这份残留。也可以手动删掉上面那个 `node_modules` 再重启。
236
238
 
237
239
  **首次安装后,设置页每张卡片都显示 `transport failure for /packycode-api/…: HTTP 405`?** 实际装上的是 1.1.6 或更早的版本,通常是因为安装时只填了包名:安装提示显示的是最新版,pnpm 实际装的却是发布满 24 小时的旧版(见「安装插件」)。这些旧版要等 DSH 重启、重建它的 `connection` 服务之后,才能注册设置通道。所以首次安装后 `/packycode-api` 通道还不存在,请求落到静态页面,返回 405;模型路由和设置页本身照常加载。1.1.7 起首次安装不需要重启。先确认装上的版本:
238
240
 
package/lib/client.js CHANGED
@@ -157,6 +157,9 @@ window.__ModuleLoader__.load({
157
157
  cliUpdating: "正在更新…(通常需要一两分钟,请勿关闭 DSH)",
158
158
  cliUpdateCommand: "将在本机执行:{command}",
159
159
  cliUpdateSucceeded: "已更新到 {version}。",
160
+ cliUpdateUnchanged: "更新命令已执行,版本仍是 {version}。",
161
+ cliUpdateAvailable: "有新版本 {latest},可以更新。当前版本仍可正常使用。",
162
+ cliUpdateNotUpdated: "更新命令已执行,但版本仍是 {version},最新版是 {latest}。请在终端执行 {command},看它给出的提示(例如用 Homebrew 安装的要执行 brew upgrade claude-code),然后点「重新检测」。当前版本仍可正常使用。",
160
163
  cliUpdateFailed: "更新失败",
161
164
  cliUpdateNotInstalled: "没有找到要更新的 CLI,请点「重新检测」。",
162
165
  cliUpdateStillOutdated: "更新命令已执行,但版本仍低于 {minVersion}。请在终端执行 {command},看它给出的提示(例如用 Homebrew 安装的要执行 brew upgrade claude-code),然后点「重新检测」。",
@@ -369,6 +372,9 @@ window.__ModuleLoader__.load({
369
372
  cliUpdating: "Updating… (usually a minute or two; keep DSH open)",
370
373
  cliUpdateCommand: "Runs on this device: {command}",
371
374
  cliUpdateSucceeded: "Updated to {version}.",
375
+ cliUpdateUnchanged: "The update ran; the version is still {version}.",
376
+ cliUpdateAvailable: "Version {latest} is available. The installed version keeps working until you update.",
377
+ cliUpdateNotUpdated: "The update ran, but the version is still {version} and the latest is {latest}. Run {command} in a terminal and follow what it says (for a Homebrew install, brew upgrade claude-code), then click Check again. The installed version keeps working.",
372
378
  cliUpdateFailed: "Update failed",
373
379
  cliUpdateNotInstalled: "No CLI was found to update. Click Check again.",
374
380
  cliUpdateStillOutdated: "The update ran, but the version is still below {minVersion}. Run {command} in a terminal and follow what it says (for a Homebrew install, brew upgrade claude-code), then click Check again.",
@@ -2591,6 +2597,7 @@ window.__ModuleLoader__.load({
2591
2597
  "not-found-after-install": () => "cliNotFoundAfterInstall",
2592
2598
  "not-installed": () => "cliUpdateNotInstalled",
2593
2599
  "still-outdated": () => "cliUpdateStillOutdated",
2600
+ "not-updated": () => "cliUpdateNotUpdated",
2594
2601
  "broken-after-update": () => "cliUpdateBroken"
2595
2602
  };
2596
2603
  /** The sentence explaining a failed install or update, by the code the host classified. */
@@ -2599,12 +2606,15 @@ window.__ModuleLoader__.load({
2599
2606
  const command = cli.install?.action === "update" ? cli.updateCommand : cli.command;
2600
2607
  return key === void 0 ? void 0 : fill(t(key), {
2601
2608
  command: command ?? "",
2602
- minVersion: cli.minVersion ?? ""
2609
+ minVersion: cli.minVersion ?? "",
2610
+ version: cli.version ?? "",
2611
+ latest: cli.install?.latest ?? cli.latest ?? ""
2603
2612
  });
2604
2613
  }
2605
2614
  /**
2606
2615
  * Whether this device has the Claude Code CLI that Claude requests on cc
2607
- * keys go through: an installer without one, an updater for one too old.
2616
+ * keys go through: an installer without one, an updater for one too old or
2617
+ * behind npm's latest.
2608
2618
  */
2609
2619
  function ClaudeCliCard({ call, t }) {
2610
2620
  const [cli, setCli] = react.useState();
@@ -2642,6 +2652,7 @@ window.__ModuleLoader__.load({
2642
2652
  };
2643
2653
  const usable = cli?.installed === true && cli.version !== void 0;
2644
2654
  const outdated = usable && cli.outdated === true;
2655
+ const updateAvailable = usable && !outdated && cli.updateAvailable === true;
2645
2656
  const action = cli?.install?.action ?? "install";
2646
2657
  const failed = cli?.install?.state === "failed";
2647
2658
  const failure = cli === void 0 ? void 0 : installFailure(cli, t);
@@ -2691,8 +2702,27 @@ window.__ModuleLoader__.load({
2691
2702
  }),
2692
2703
  cli.install?.state === "succeeded" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2693
2704
  className: "pkc-note",
2694
- children: action === "update" ? fill(t("cliUpdateSucceeded"), { version: cli.install.to ?? cli.version }) : t("cliInstallSucceeded")
2695
- })
2705
+ children: action === "update" ? fill(t(cli.install.from !== void 0 && cli.install.from === cli.install.to ? "cliUpdateUnchanged" : "cliUpdateSucceeded"), { version: cli.install.to ?? cli.version }) : t("cliInstallSucceeded")
2706
+ }),
2707
+ updateAvailable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2708
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2709
+ className: "pkc-note",
2710
+ children: fill(t("cliUpdateAvailable"), { latest: cli.latest ?? "" })
2711
+ }),
2712
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2713
+ className: "pkc-split",
2714
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2715
+ size: "md",
2716
+ disabled: running || checking,
2717
+ onClick: update,
2718
+ children: running && action === "update" ? t("cliUpdating") : t("cliUpdate")
2719
+ })
2720
+ }),
2721
+ cli.updateCommand !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2722
+ className: "pkc-note",
2723
+ children: fill(t("cliUpdateCommand"), { command: cli.updateCommand })
2724
+ })
2725
+ ] })
2696
2726
  ] }),
2697
2727
  outdated && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2698
2728
  className: "pkc-callout",
package/lib/index.js CHANGED
@@ -14,7 +14,6 @@ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
14
14
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
15
15
  import { lazyStream } from "@earendil-works/pi-ai/api/lazy";
16
16
  import { clampThinkingLevel, createProvider } from "@earendil-works/pi-ai";
17
- import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
18
17
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
19
18
  import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
20
19
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -1053,8 +1052,9 @@ function resolveSpawnCommand(binary, { platform = process.platform, exists = exi
1053
1052
  * sent by this device's Claude Code CLI, so the settings panel shows whether
1054
1053
  * one is installed (and which version), and offers to install it with
1055
1054
  * `npm install -g @anthropic-ai/claude-code` — the same command on macOS and
1056
- * Windows — or to update one older than the transport needs. Both run on the
1057
- * host, never in a shell built from user text: their arguments are constants.
1055
+ * Windows — or to update one older than the transport needs or than npm's
1056
+ * `latest`. Both run on the host, never in a shell built from user text:
1057
+ * their arguments are constants.
1058
1058
  *
1059
1059
  * npm 12 skips install scripts unless they are allowed, and the CLI's
1060
1060
  * postinstall is what puts its native binary in place, so every npm install
@@ -1068,6 +1068,11 @@ const CLAUDE_INSTALL_COMMAND = `npm install -g ${CLAUDE_NPM_PACKAGE} ${ALLOW_SCR
1068
1068
  const CLAUDE_NPM_UPDATE_COMMAND = `npm install -g ${CLAUDE_NPM_PACKAGE}@latest ${ALLOW_SCRIPTS}`;
1069
1069
  /** How any other install (the native installer's, Homebrew's) is updated: by the CLI itself. */
1070
1070
  const CLAUDE_SELF_UPDATE_COMMAND = "claude update";
1071
+ /**
1072
+ * The newest CLI release. npm's `latest` tag and the native installer's
1073
+ * `latest` channel name the same version, and both updates above install it.
1074
+ */
1075
+ const CLAUDE_NPM_LATEST_URL = `https://registry.npmjs.org/${CLAUDE_NPM_PACKAGE}/latest`;
1071
1076
  const INSTALL_ARGS = Object.freeze([
1072
1077
  "install",
1073
1078
  "-g",
@@ -1087,6 +1092,11 @@ const PREFIX_TIMEOUT_MS = 2e4;
1087
1092
  const INSTALL_TIMEOUT_MS = 900 * 1e3;
1088
1093
  /** A detection is reused this long; the panel's refresh and an install bypass it. */
1089
1094
  const DETECTION_TTL_MS = 300 * 1e3;
1095
+ const LATEST_TIMEOUT_MS = 1e4;
1096
+ /** The newest release is read again after this long; the panel's refresh bypasses it. */
1097
+ const LATEST_TTL_MS = 1800 * 1e3;
1098
+ /** An unanswered read is retried sooner, so one network blip does not hide an update for long. */
1099
+ const LATEST_RETRY_MS = 120 * 1e3;
1090
1100
  const DETAIL_CHARS = 800;
1091
1101
  /**
1092
1102
  * The version `claude --version` prints, e.g. `2.1.3 (Claude Code)`.
@@ -1195,6 +1205,48 @@ function explainNpmFailure(result) {
1195
1205
  };
1196
1206
  }
1197
1207
  /**
1208
+ * The newest Claude Code CLI version on npm, read host-side with a timeout
1209
+ * and redirects refused. A read never throws: no answer is `undefined`, which
1210
+ * offers no update.
1211
+ * @param options - fetch, registry URL, timeout, cache lifetimes and clock.
1212
+ * @returns `{ read({ force }), peek() }`.
1213
+ */
1214
+ function createClaudeLatestReader({ fetchImpl = ambientFetch, url = CLAUDE_NPM_LATEST_URL, timeoutMs = LATEST_TIMEOUT_MS, ttlMs = LATEST_TTL_MS, retryMs = LATEST_RETRY_MS, now = () => Date.now() } = {}) {
1215
+ let cached;
1216
+ let inFlight;
1217
+ const load = async () => {
1218
+ let version;
1219
+ try {
1220
+ const response = await boundedFetch(url, { headers: { Accept: "application/json" } }, {
1221
+ fetchImpl,
1222
+ timeoutMs
1223
+ });
1224
+ if (response.ok) version = (await response.json())?.version;
1225
+ } catch {
1226
+ version = void 0;
1227
+ }
1228
+ return {
1229
+ version: parseSemver(version) === void 0 ? void 0 : version,
1230
+ fetchedAt: now()
1231
+ };
1232
+ };
1233
+ return Object.freeze({
1234
+ async read({ force = false } = {}) {
1235
+ const lifetime = cached?.version === void 0 ? retryMs : ttlMs;
1236
+ if (!force && cached !== void 0 && now() - cached.fetchedAt < lifetime) return cached.version;
1237
+ if (inFlight === void 0) inFlight = load().then((value) => {
1238
+ cached = value;
1239
+ return value;
1240
+ }).finally(() => {
1241
+ inFlight = void 0;
1242
+ });
1243
+ return (await inFlight).version;
1244
+ },
1245
+ /** The last version read, without a request. */
1246
+ peek: () => cached?.version
1247
+ });
1248
+ }
1249
+ /**
1198
1250
  * Run a command to completion and capture its output. Never rejects: a
1199
1251
  * missing program or a timeout comes back as `error`.
1200
1252
  */
@@ -1264,10 +1316,12 @@ function runCaptured(spawnImpl, command, args, options, timeoutMs) {
1264
1316
  /**
1265
1317
  * The CLI manager behind the settings panel and the runner.
1266
1318
  * @param options - the configured CLI path (a function, so a settings change
1267
- * applies without a restart), plus platform, environment, and process seams.
1319
+ * applies without a restart), the newest-release reader (none: no update
1320
+ * is offered for a CLI the transport can use), plus platform, environment,
1321
+ * and process seams.
1268
1322
  * @returns `{ binary, detect, install, status }`.
1269
1323
  */
1270
- function createClaudeCliManager({ explicitPath = () => "", platform = process.platform, home = homedir(), env = process.env, execPath = process.execPath, configDir = DEFAULT_CLAUDE_CONFIG_DIR, spawnImpl = spawn, exists = existsSync, readFile = (file) => readFileSync(file, "utf8"), realpath = realpathSync, readdir, now = () => Date.now() } = {}) {
1324
+ function createClaudeCliManager({ explicitPath = () => "", latestReader, platform = process.platform, home = homedir(), env = process.env, execPath = process.execPath, configDir = DEFAULT_CLAUDE_CONFIG_DIR, spawnImpl = spawn, exists = existsSync, readFile = (file) => readFileSync(file, "utf8"), realpath = realpathSync, readdir, now = () => Date.now() } = {}) {
1271
1325
  const path = pathFor(platform);
1272
1326
  const configured = () => {
1273
1327
  const value = explicitPath();
@@ -1434,10 +1488,16 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
1434
1488
  const beside = path.join(path.dirname(binary), platform === "win32" ? "npm.cmd" : "npm");
1435
1489
  return exists(beside) ? beside : npm();
1436
1490
  };
1491
+ /** The newest release, or undefined when unknown. */
1492
+ const latestVersion = ({ force = false } = {}) => latestReader?.read({ force }) ?? Promise.resolve(void 0);
1493
+ /** Whether a version is older than the newest release; false when either is unknown. */
1494
+ const behind = (version, latest) => latest !== void 0 && compareSemver(version, latest) === -1;
1437
1495
  /**
1438
1496
  * Update the located CLI the way it was installed: npm's global package
1439
1497
  * through npm, anything else through `claude update` with the plugin's
1440
- * own CLI home. Success means the CLI now detected is new enough.
1498
+ * own CLI home. Success means the CLI now detected is new enough and either
1499
+ * changed or was not behind the newest release; a command that exited
1500
+ * cleanly but left it behind says so.
1441
1501
  */
1442
1502
  const runUpdate = async () => {
1443
1503
  const before = await detect({ force: true });
@@ -1485,8 +1545,10 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
1485
1545
  } }, INSTALL_TIMEOUT_MS);
1486
1546
  }
1487
1547
  const after = await detect({ force: true });
1548
+ const latest = await latestVersion();
1488
1549
  const from = before.version === void 0 ? {} : { from: before.version };
1489
- if (after.version !== void 0 && !isClaudeCliOutdated(after.version)) installation = {
1550
+ const changed = after.version !== void 0 && after.version !== before.version;
1551
+ if (after.version !== void 0 && !isClaudeCliOutdated(after.version) && (changed || result.code === 0 && !behind(after.version, latest))) installation = {
1490
1552
  state: "succeeded",
1491
1553
  action: "update",
1492
1554
  ...from,
@@ -1506,13 +1568,21 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
1506
1568
  detail: detailOf(after.error, result.stdout, result.stderr),
1507
1569
  finishedAt: now()
1508
1570
  };
1509
- else installation = {
1571
+ else if (isClaudeCliOutdated(after.version)) installation = {
1510
1572
  state: "failed",
1511
1573
  action: "update",
1512
1574
  code: "still-outdated",
1513
1575
  detail: detailOf(result.stdout, result.stderr),
1514
1576
  finishedAt: now()
1515
1577
  };
1578
+ else installation = {
1579
+ state: "failed",
1580
+ action: "update",
1581
+ code: "not-updated",
1582
+ latest,
1583
+ detail: detailOf(result.stdout, result.stderr),
1584
+ finishedAt: now()
1585
+ };
1516
1586
  };
1517
1587
  const update = () => {
1518
1588
  if (installing !== void 0) return installation;
@@ -1553,15 +1623,18 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
1553
1623
  settled: () => installing ?? Promise.resolve(),
1554
1624
  /**
1555
1625
  * What the settings panel shows. While an install or update runs, the
1556
- * last detection stands instead of probing a half-written package.
1626
+ * last detection and the last newest-release reading stand instead of
1627
+ * probing a half-written package. The two reads run side by side.
1557
1628
  */
1558
1629
  async status({ force = false } = {}) {
1559
- const value = installing === void 0 ? await detect({ force }) : detection;
1630
+ const [value, latest] = installing === void 0 ? await Promise.all([detect({ force }), latestVersion({ force })]) : [detection, latestReader?.peek()];
1560
1631
  return {
1561
1632
  installed: value?.installed === true,
1562
1633
  ...value?.version === void 0 ? {} : { version: value.version },
1563
1634
  minVersion: CLAUDE_MIN_VERSION,
1564
1635
  outdated: isClaudeCliOutdated(value?.version),
1636
+ ...latest === void 0 ? {} : { latest },
1637
+ updateAvailable: value?.version !== void 0 && behind(value.version, latest),
1565
1638
  ...value?.path === void 0 ? {} : { updateCommand: isNpmClaudeInstall(value.path, {
1566
1639
  platform,
1567
1640
  realpath
@@ -3251,8 +3324,8 @@ function createDiagnostics({ sessionQuery, readEvents, ring, groups, secrets = a
3251
3324
  *
3252
3325
  * The gateway's own guidance for these routes, which the base URLs and pinned
3253
3326
  * protocols below exist to satisfy:
3254
- * - OpenAI/Codex traffic takes `/v1` in the base URL and never an endpoint
3255
- * path — the SDK appends `/responses` or `/chat/completions` itself.
3327
+ * - OpenAI-protocol traffic takes `/v1` in the base URL and never an endpoint
3328
+ * path — the SDK appends `/responses` itself.
3256
3329
  * - Anthropic traffic takes `https://cf.api.fan` with **no** `/v1`: the
3257
3330
  * Anthropic client appends `/v1/messages` itself.
3258
3331
  * - A model asked over the wrong protocol does not fail cleanly; the stream
@@ -3262,7 +3335,7 @@ function createDiagnostics({ sessionQuery, readEvents, ring, groups, secrets = a
3262
3335
  * model does not support is refused before the request is sent: Claude Opus
3263
3336
  * 5.5 (platform.claude.com effort docs), GPT-6.1 Sol, GPT-6 Astra and GPT-5.6
3264
3337
  * Sol `reasoning.effort` (developers.openai.com model pages), DeepSeek
3265
- * thinking mode (`thinking` toggle plus `low|high|max` effort), Grok 4.7
3338
+ * thinking mode (`low|high|max` effort, or off), Grok 4.7
3266
3339
  * (docs.x.ai/developers/grok-4-7), and Gemini 4 Argon
3267
3340
  * (ai.google.dev/gemini-api/docs/models/gemini-4-argon and docs/openai).
3268
3341
  *
@@ -3275,15 +3348,14 @@ const NO_COST = Object.freeze({
3275
3348
  cacheRead: 0,
3276
3349
  cacheWrite: 0
3277
3350
  });
3278
- const COMPLETIONS_BASE = `${DEFAULT_API_HOST}/v1`;
3279
3351
  const RESPONSES_BASE = `${DEFAULT_API_HOST}/v1`;
3280
3352
  const MESSAGES_BASE = DEFAULT_API_HOST;
3281
3353
  const GEMINI_BASE = `${DEFAULT_API_HOST}/v1beta`;
3282
3354
  /**
3283
3355
  * pi-ai thinking-level maps. A level pinned to `null` is unsupported; an
3284
3356
  * absent key keeps pi-ai's own default for that level, which is how `off`
3285
- * stays available for DeepSeek (`off` sends nothing and the deepseek dialect
3286
- * then emits `thinking: disabled`).
3357
+ * stays available for DeepSeek: pi-ai's Responses API then sends
3358
+ * `reasoning.effort: "none"`, which the gateway answers without reasoning.
3287
3359
  */
3288
3360
  const DEEPSEEK_LEVELS = Object.freeze({
3289
3361
  minimal: null,
@@ -3322,21 +3394,15 @@ const PACKYCODE_MODELS = Object.freeze([
3322
3394
  Object.freeze({
3323
3395
  id: "deepseek-flash",
3324
3396
  name: "DeepSeek Flash",
3325
- api: "openai-completions",
3397
+ api: "openai-responses",
3326
3398
  provider: PROVIDER,
3327
- baseUrl: COMPLETIONS_BASE,
3399
+ baseUrl: RESPONSES_BASE,
3328
3400
  reasoning: true,
3329
3401
  input: Object.freeze(["text", "image"]),
3330
3402
  cost: NO_COST,
3331
3403
  contextWindow: 1e6,
3332
3404
  maxTokens: 384e3,
3333
- thinkingLevelMap: DEEPSEEK_LEVELS,
3334
- compat: Object.freeze({
3335
- thinkingFormat: "deepseek",
3336
- maxTokensField: "max_tokens",
3337
- requiresReasoningContentOnAssistantMessages: true,
3338
- supportsDeveloperRole: false
3339
- })
3405
+ thinkingLevelMap: DEEPSEEK_LEVELS
3340
3406
  }),
3341
3407
  Object.freeze({
3342
3408
  id: "gpt-6.1-sol",
@@ -5829,8 +5895,8 @@ function createGeminiApi() {
5829
5895
  * The PackyCode pi-ai provider, built in code so the user configures no route.
5830
5896
  *
5831
5897
  * `createProvider` dispatches on `model.api`, which is what lets one route
5832
- * carry Chat Completions, Responses, Anthropic Messages and native Gemini
5833
- * models — the gateway refuses some of them over other protocols.
5898
+ * carry Responses, Anthropic Messages and native Gemini models — the gateway
5899
+ * refuses some of them over other protocols.
5834
5900
  *
5835
5901
  * @module dsh-packycode-api/provider
5836
5902
  */
@@ -5874,7 +5940,6 @@ function createPackyCodeProvider({ claudeCli } = {}) {
5874
5940
  auth: keyRingAuth(),
5875
5941
  models: PACKYCODE_MODELS,
5876
5942
  api: {
5877
- "openai-completions": openAICompletionsApi(),
5878
5943
  "openai-responses": openAIResponsesApi(),
5879
5944
  "anthropic-messages": createClaudeApi({ cli: claudeCli }),
5880
5945
  "google-generative-ai": createGeminiApi()
@@ -6471,8 +6536,8 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claude,
6471
6536
  * the `web_search_20250305` server tool. Results come from its structured
6472
6537
  * `web_search_tool_result` blocks, never from prose.
6473
6538
  * - `gpt`: OpenAI Responses at `/v1/responses`, `gpt-6.1-sol`, with the
6474
- * hosted `web_search` tool. Results come from `url_citation` annotations and
6475
- * the `web_search_call` actions.
6539
+ * hosted `web_search` tool, streamed. Results come from `url_citation`
6540
+ * annotations and the `web_search_call` actions.
6476
6541
  *
6477
6542
  * Only keys stored in this plugin's key ring authenticate a search; there is no
6478
6543
  * fallback to another credential. A key the gateway reports as exhausted or
@@ -6519,6 +6584,14 @@ const MAX_OUTPUT_TOKENS = 4096;
6519
6584
  const ANTHROPIC_VERSION = "2023-06-01";
6520
6585
  /** Distinct keys one search may try before reporting the last failure. */
6521
6586
  const MAX_KEY_ATTEMPTS = 4;
6587
+ /** Resends of one search after an upstream hiccup the gateway relayed with 200. */
6588
+ const MAX_TRANSIENT_RETRIES = 2;
6589
+ /** Pause before such a resend when the upstream names none. */
6590
+ const DEFAULT_RETRY_DELAY_MS = 1e3;
6591
+ /** A longer advised pause would not fit DSH's search budget; report it instead. */
6592
+ const MAX_RETRY_DELAY_MS = 5e3;
6593
+ /** Bound on one unterminated stream event. */
6594
+ const MAX_EVENT_CHARS = 4 * 1024 * 1024;
6522
6595
  const MAX_CONTENT_CHARS = 4e3;
6523
6596
  const MAX_SNIPPET_CHARS = 500;
6524
6597
  const MAX_TITLE_CHARS = 200;
@@ -6532,6 +6605,7 @@ const MAX_ERROR_DETAIL_CHARS = 400;
6532
6605
  var WebError = class extends HarnessError {};
6533
6606
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
6534
6607
  const text = (value) => typeof value === "string" ? value : "";
6608
+ const named = (value) => text(value) === "" ? void 0 : value;
6535
6609
  function bounded(value, maximum) {
6536
6610
  const clean = text(value).replace(/\s+/gu, " ").trim();
6537
6611
  if (clean === "") return void 0;
@@ -6631,10 +6705,12 @@ function parseMessagesSearchResponse(response) {
6631
6705
  * search actions surfaced; the final message becomes `content`.
6632
6706
  * @param response - parsed Responses body.
6633
6707
  * @returns the normalized search result.
6634
- * @throws {WebError} when the model ran no web search.
6708
+ * @throws {WebError} when the model ran no web search, or the body carries no
6709
+ * output at all.
6635
6710
  */
6636
6711
  function parseResponsesSearchResponse(response) {
6637
6712
  const output = Array.isArray(response?.output) ? response.output : [];
6713
+ if (output.length === 0) throw new WebError(`PackyCode search returned no output items${text(response?.status) === "" ? "" : ` (status ${text(response.status)})`}; the gateway did not pass the search result on`, "WEB_PROVIDER_ERROR");
6638
6714
  const calls = output.filter((item) => item?.type === "web_search_call");
6639
6715
  if (calls.length === 0) throw new WebError("PackyCode search returned no web_search_call; the model did not run a web search", "WEB_PROVIDER_ERROR");
6640
6716
  const sources = sourceList();
@@ -6663,6 +6739,129 @@ function parseResponsesSearchResponse(response) {
6663
6739
  }
6664
6740
  return result(sources.values(), boundedBlock(answer, MAX_CONTENT_CHARS));
6665
6741
  }
6742
+ /** Events after which a Responses stream carries nothing a search uses. */
6743
+ const RESPONSES_LAST_EVENTS = /* @__PURE__ */ new Set([
6744
+ "response.completed",
6745
+ "response.incomplete",
6746
+ "response.failed",
6747
+ "error"
6748
+ ]);
6749
+ /**
6750
+ * Read a streamed Responses answer into the body it describes.
6751
+ *
6752
+ * PackyCode's Codex channel ends a stream with a `response.completed` whose
6753
+ * `output` is empty; the items only arrive as `response.output_item.done`
6754
+ * events. A non-streaming request gets that same empty `output`, which is why
6755
+ * the GPT backend streams. The items are rebuilt here in `output_index`
6756
+ * order; the final event's own `output` wins when it has any. An `error`
6757
+ * event or `response.failed` becomes the body's `error`.
6758
+ * @param response - a 200 answer whose body is `text/event-stream`.
6759
+ * @param signal - the caller's cancellation; it cancels the read.
6760
+ * @returns the body, with `output` rebuilt or `error` set.
6761
+ * @throws {WebError} when the stream ends before its final event.
6762
+ */
6763
+ async function readResponsesStream(response, signal) {
6764
+ if (response.body === null || response.body === void 0) throw new WebError("PackyCode search returned an empty stream", "WEB_PROVIDER_ERROR");
6765
+ const items = [];
6766
+ let final;
6767
+ let failure;
6768
+ /** Apply one event; true once nothing after it matters. */
6769
+ const dispatch = (block) => {
6770
+ let name = "";
6771
+ let data;
6772
+ for (const line of block.split("\n")) {
6773
+ if (line.startsWith(":")) continue;
6774
+ const colon = line.indexOf(":");
6775
+ const field = colon < 0 ? line : line.slice(0, colon);
6776
+ const value = colon < 0 ? "" : line.slice(colon + 1).replace(/^ /u, "");
6777
+ if (field === "event") name = value;
6778
+ else if (field === "data") data = data === void 0 ? value : `${data}\n${value}`;
6779
+ }
6780
+ if (data === void 0 || data === "[DONE]") return false;
6781
+ let event;
6782
+ try {
6783
+ event = JSON.parse(data);
6784
+ } catch {
6785
+ return false;
6786
+ }
6787
+ const type = text(event?.type) || name;
6788
+ if (type === "response.output_item.done" && record(event.item)) items.push({
6789
+ index: Number.isInteger(event.output_index) ? event.output_index : Number.MAX_SAFE_INTEGER,
6790
+ item: event.item
6791
+ });
6792
+ else if (type === "response.completed" || type === "response.incomplete") final = record(event.response) ? event.response : {};
6793
+ else if (type === "error") failure = record(event.error) ? event.error : {
6794
+ code: event.code,
6795
+ message: event.message
6796
+ };
6797
+ else if (type === "response.failed") failure ??= record(event.response?.error) ? event.response.error : { message: "the response failed" };
6798
+ return RESPONSES_LAST_EVENTS.has(type);
6799
+ };
6800
+ const reader = response.body.getReader();
6801
+ const cancel = () => {
6802
+ reader.cancel(signal.reason).catch(() => void 0);
6803
+ };
6804
+ signal?.addEventListener("abort", cancel, { once: true });
6805
+ const decoder = new TextDecoder();
6806
+ let pending = "";
6807
+ let carry = "";
6808
+ let ended = false;
6809
+ try {
6810
+ signal?.throwIfAborted();
6811
+ while (!ended) {
6812
+ const { done, value } = await reader.read();
6813
+ signal?.throwIfAborted();
6814
+ const decoded = carry + (done ? decoder.decode() : decoder.decode(value, { stream: true }));
6815
+ carry = !done && decoded.endsWith("\r") ? "\r" : "";
6816
+ pending += decoded.slice(0, decoded.length - carry.length).replace(/\r\n?/gu, "\n");
6817
+ for (let boundary = pending.indexOf("\n\n"); !ended && boundary >= 0; boundary = pending.indexOf("\n\n")) {
6818
+ ended = dispatch(pending.slice(0, boundary));
6819
+ pending = pending.slice(boundary + 2);
6820
+ }
6821
+ if (done) {
6822
+ if (!ended && pending.trim() !== "") ended = dispatch(pending);
6823
+ break;
6824
+ }
6825
+ if (pending.length > MAX_EVENT_CHARS) throw new WebError("PackyCode search stream sent an oversized event", "WEB_PROVIDER_ERROR");
6826
+ }
6827
+ } finally {
6828
+ signal?.removeEventListener("abort", cancel);
6829
+ await reader.cancel().catch(() => void 0);
6830
+ reader.releaseLock();
6831
+ }
6832
+ if (failure !== void 0) return { error: failure };
6833
+ if (final === void 0) throw new WebError("PackyCode search stream ended before the response completed", "WEB_PROVIDER_ERROR");
6834
+ const output = Array.isArray(final.output) && final.output.length > 0 ? final.output : items.sort((left, right) => left.index - right.index).map((entry) => entry.item);
6835
+ return {
6836
+ ...final,
6837
+ output
6838
+ };
6839
+ }
6840
+ /** "Please try again in 2s.", "try again in 1.898s", "try again in 28ms". */
6841
+ const RETRY_HINT = /try again in\s*(\d+(?:\.\d+)?)\s*(ms|s|secs?|seconds?)\b/iu;
6842
+ /**
6843
+ * Whether an error the gateway relayed with a 200 answer is an upstream
6844
+ * hiccup a resent request clears, and how long to wait before resending.
6845
+ *
6846
+ * The Codex channel reports a model at capacity as `rate_limit_exceeded`
6847
+ * ("Temporary capacity limit. Please try again in 2s.") and a fault as
6848
+ * `server_error`, inside a stream it has already answered 200 for, so the
6849
+ * gateway cannot resend it on another channel. The official Codex client
6850
+ * resends both, after the pause the message names.
6851
+ * @param error - the body's or the stream's `error` object.
6852
+ * @param fallbackMs - the pause when the upstream names none.
6853
+ * @returns the pause in milliseconds, or undefined when a resend would not
6854
+ * help or its pause would not fit DSH's search budget.
6855
+ */
6856
+ function transientRetryDelay(error, fallbackMs = DEFAULT_RETRY_DELAY_MS) {
6857
+ if (!record(error)) return void 0;
6858
+ const message = text(error.message);
6859
+ if (classifyGatewayFailure(message) !== void 0) return void 0;
6860
+ if (!(["rate_limit_exceeded", "server_error"].includes(text(error.code)) || ["server_error", "service_unavailable_error"].includes(text(error.type)) || classifyUpstreamServerError(message))) return void 0;
6861
+ const hint = RETRY_HINT.exec(message);
6862
+ const delay = hint === null ? fallbackMs : Math.round(Number(hint[1]) * (hint[2].toLowerCase() === "ms" ? 1 : 1e3));
6863
+ return delay <= MAX_RETRY_DELAY_MS ? delay : void 0;
6864
+ }
6666
6865
  /** Build one backend's request; the key is attached by the caller. */
6667
6866
  function buildSearchRequest(backend, { apiHost, query, sessionId }) {
6668
6867
  const model = SEARCH_BACKEND_MODELS[backend];
@@ -6685,6 +6884,7 @@ function buildSearchRequest(backend, { apiHost, query, sessionId }) {
6685
6884
  reasoning: { effort: "low" },
6686
6885
  max_output_tokens: MAX_OUTPUT_TOKENS,
6687
6886
  store: false,
6887
+ stream: true,
6688
6888
  ...typeof sessionId === "string" && sessionId !== "" ? { prompt_cache_key: sessionId } : {}
6689
6889
  },
6690
6890
  parse: parseResponsesSearchResponse
@@ -6728,29 +6928,51 @@ async function failureText(response) {
6728
6928
  } catch {}
6729
6929
  return raw.replace(/\s+/gu, " ").trim().slice(0, MAX_ERROR_DETAIL_CHARS);
6730
6930
  }
6931
+ /** An error object as bounded text, its code first, the way failureText words one. */
6932
+ function errorText(error) {
6933
+ const message = text(error.message);
6934
+ if (message === "") return JSON.stringify(error).slice(0, MAX_ERROR_DETAIL_CHARS);
6935
+ return `${text(error.code) === "" ? "" : `${error.code}: `}${message}`.slice(0, MAX_ERROR_DETAIL_CHARS);
6936
+ }
6731
6937
  const mask = (message, key) => typeof key === "string" && key.length >= 8 ? message.split(key).join(`…${key.slice(-4)}`) : message;
6732
6938
  function isAbort(error, signal) {
6733
6939
  return signal?.aborted === true || error?.name === "AbortError";
6734
6940
  }
6941
+ const isEventStream = (response) => /^text\/event-stream\b/iu.test(response.headers?.get?.("content-type") ?? "");
6942
+ /** Wait before a resend; a cancelled search stops waiting. */
6943
+ async function pause(ms, signal) {
6944
+ try {
6945
+ await setTimeout$1(ms, void 0, { signal });
6946
+ } catch (error) {
6947
+ throw new WebError("PackyCode search aborted", "WEB_ABORTED", { cause: error });
6948
+ }
6949
+ }
6735
6950
  /**
6736
6951
  * Create the provider that searches through PackyCode on every request.
6737
6952
  * @param options - the key ring, the host resolution, the backend preference,
6738
6953
  * the key-failure sink shared with the model route, and test seams.
6739
6954
  * @returns a DSH web search provider.
6740
6955
  */
6741
- function createPackyCodeSearchProvider({ ring, hosts, backend = () => DEFAULT_SEARCH_BACKEND, onKeyFailure, covers, resolveSessionId, fetchImpl = ambientFetch, timeoutMs = DEFAULT_SEARCH_TIMEOUT_MS } = {}) {
6956
+ function createPackyCodeSearchProvider({ ring, hosts, backend = () => DEFAULT_SEARCH_BACKEND, onKeyFailure, covers, resolveSessionId, fetchImpl = ambientFetch, timeoutMs = DEFAULT_SEARCH_TIMEOUT_MS, retryDelayMs = DEFAULT_RETRY_DELAY_MS } = {}) {
6742
6957
  if (ring === void 0 || typeof ring.activeKey !== "function") throw new Error("PackyCode search requires the key ring");
6743
6958
  if (typeof hosts !== "function") throw new Error("PackyCode search requires the host resolution");
6959
+ /**
6960
+ * The Codex group refuses a Responses request without `prompt_cache_key`
6961
+ * with a 403 ("请使用标准 Codex 客户端"); a search no session is named for
6962
+ * still sends one, as the model route does.
6963
+ */
6964
+ const fallbackSessionId = `${PACKYCODE_SEARCH_PROVIDER_ID}-search-${randomUUID()}`;
6744
6965
  /** One request with one key; classified failures come back as values. */
6745
6966
  const attempt = async (request, key, signal) => {
6746
6967
  if (signal?.aborted === true) throw new WebError("PackyCode search aborted", "WEB_ABORTED", { cause: signal.reason });
6968
+ const streamed = request.body.stream === true;
6747
6969
  let response;
6748
6970
  try {
6749
6971
  response = await boundedFetch(request.url, {
6750
6972
  method: "POST",
6751
6973
  headers: {
6752
6974
  ...request.headers(key),
6753
- accept: "application/json",
6975
+ accept: streamed ? "text/event-stream" : "application/json",
6754
6976
  "content-type": "application/json"
6755
6977
  },
6756
6978
  body: JSON.stringify(request.body)
@@ -6770,14 +6992,17 @@ function createPackyCodeSearchProvider({ ring, hosts, backend = () => DEFAULT_SE
6770
6992
  };
6771
6993
  let body;
6772
6994
  try {
6773
- body = await response.json();
6995
+ body = streamed && isEventStream(response) ? await readResponsesStream(response, signal) : await response.json();
6774
6996
  } catch (error) {
6775
6997
  if (isAbort(error, signal)) throw new WebError("PackyCode search aborted", "WEB_ABORTED", { cause: error });
6998
+ if (error?.name === "TimeoutError") throw new WebError(`PackyCode search got no answer within ${Math.round(timeoutMs / 1e3)}s`, "WEB_PROVIDER_ERROR", { cause: error });
6999
+ if (error instanceof WebError) throw error;
6776
7000
  throw new WebError("PackyCode search returned an unreadable response", "WEB_PROVIDER_ERROR", { cause: error });
6777
7001
  }
6778
7002
  if (record(body?.error)) return {
6779
- failure: mask(text(body.error.message) || JSON.stringify(body.error).slice(0, MAX_ERROR_DETAIL_CHARS), key),
6780
- status: response.status
7003
+ failure: mask(errorText(body.error), key),
7004
+ status: response.status,
7005
+ retryAfterMs: transientRetryDelay(body.error, retryDelayMs)
6781
7006
  };
6782
7007
  return { value: request.parse(body) };
6783
7008
  };
@@ -6792,16 +7017,22 @@ function createPackyCodeSearchProvider({ ring, hosts, backend = () => DEFAULT_SE
6792
7017
  const request = buildSearchRequest(chosen, {
6793
7018
  apiHost: hosts().apiHost,
6794
7019
  query,
6795
- sessionId: searchRequest?.sessionId ?? resolveSessionId?.()
7020
+ sessionId: named(searchRequest?.sessionId) ?? named(resolveSessionId?.()) ?? fallbackSessionId
6796
7021
  });
6797
7022
  const candidates = await orderedKeys(ring, request.model, covers);
7023
+ const other = chosen === "gpt" ? SEARCH_BACKEND_DEEPSEEK : "gpt";
6798
7024
  let lastFailure;
6799
7025
  let uncovered = false;
6800
7026
  for (const key of candidates.slice(0, MAX_KEY_ATTEMPTS)) {
6801
- const outcome = await attempt(request, key.key, signal);
7027
+ let outcome = await attempt(request, key.key, signal);
7028
+ for (let retry = 0; outcome.retryAfterMs !== void 0 && retry < MAX_TRANSIENT_RETRIES; retry += 1) {
7029
+ await pause(outcome.retryAfterMs, signal);
7030
+ outcome = await attempt(request, key.key, signal);
7031
+ }
6802
7032
  if (outcome.value !== void 0) return outcome.value;
6803
7033
  const failure = outcome.failure === "" ? `HTTP ${outcome.status}` : outcome.failure;
6804
7034
  lastFailure = failure;
7035
+ if (outcome.retryAfterMs !== void 0) throw new WebError(`PackyCode search: ${request.model} stayed unavailable upstream after 3 attempts (${failure}). Try again shortly, or switch the search backend to "${other}" under Settings → PackyCode → Web search.`, "WEB_PROVIDER_ERROR");
6805
7036
  if (classifyClientPolicy(failure)) throw new WebError(explainClientPolicyFailure(), "WEB_PROVIDER_ERROR");
6806
7037
  const kind = classifyGatewayFailure(failure) ?? (outcome.status === 401 ? "invalid-key" : void 0);
6807
7038
  if (kind !== void 0) {
@@ -6812,13 +7043,10 @@ function createPackyCodeSearchProvider({ ring, hosts, backend = () => DEFAULT_SE
6812
7043
  uncovered = true;
6813
7044
  continue;
6814
7045
  }
6815
- throw new WebError(`PackyCode search failed (HTTP ${outcome.status}): ${failure}`, "WEB_PROVIDER_ERROR");
7046
+ throw new WebError(`PackyCode search failed ${outcome.status === 200 ? "upstream" : `(HTTP ${outcome.status})`}: ${failure}`, "WEB_PROVIDER_ERROR");
6816
7047
  }
6817
7048
  if (candidates.length === 0) throw new WebError("PackyCode search has no usable API key; add one under Settings → PackyCode", "WEB_PROVIDER_CREDENTIAL_MISSING");
6818
- if (uncovered) {
6819
- const other = chosen === "gpt" ? SEARCH_BACKEND_DEEPSEEK : "gpt";
6820
- throw new WebError(`PackyCode search: no stored API key's group covers ${request.model} (${chosen} backend). Switch the search backend to "${other}" under Settings → PackyCode → Web search, or add a key whose group includes it. Gateway said: ${lastFailure}`, "WEB_PROVIDER_ERROR");
6821
- }
7049
+ if (uncovered) throw new WebError(`PackyCode search: no stored API key's group covers ${request.model} (${chosen} backend). Switch the search backend to "${other}" under Settings → PackyCode → Web search, or add a key whose group includes it. Gateway said: ${lastFailure}`, "WEB_PROVIDER_ERROR");
6822
7050
  throw new WebError(`PackyCode search: every usable key was refused (${lastFailure ?? "no detail"})`, "WEB_PROVIDER_CREDENTIAL_MISSING");
6823
7051
  }
6824
7052
  });
@@ -7438,7 +7666,10 @@ function apply(ctx, config) {
7438
7666
  * requests through this device's Claude Code CLI. DSH's agent loop, tools,
7439
7667
  * permissions and plan mode are unchanged; only the transport differs.
7440
7668
  */
7441
- const claudeCli = createClaudeCliManager({ explicitPath: () => fieldValue(source()?.claudeCliPath) });
7669
+ const claudeCli = createClaudeCliManager({
7670
+ explicitPath: () => fieldValue(source()?.claudeCliPath),
7671
+ latestReader: createClaudeLatestReader()
7672
+ });
7442
7673
  const cliTransport = createClaudeCliTransport({
7443
7674
  binary: async () => {
7444
7675
  let found = await claudeCli.detect();
@@ -7643,4 +7874,4 @@ function apply(ctx, config) {
7643
7874
  });
7644
7875
  }
7645
7876
  //#endregion
7646
- export { CHANNEL, CLAUDE_INSTALL_COMMAND, CLAUDE_MIN_VERSION, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLAUDE_NPM_UPDATE_COMMAND, CLAUDE_SELF_UPDATE_COMMAND, CLI_ONLY_GROUPS, CLI_SESSION_IDLE_MS, CLI_TOOL_NOTE, Config, DEFAULT_API_HOST, DEFAULT_CLAUDE_CONFIG_DIR, DEFAULT_IMAGE_MODEL, DEFAULT_SEARCH_BACKEND, DEFAULT_STATUS_HOST, DIAGNOSTICS_FORMAT, DIAGNOSTICS_LIMITS, DISPLAY_NAME, HTTP_CLAUDE_GROUPS, IMAGE_MODELS, IMAGE_MODEL_IDS, IMAGE_QUALITIES, IMAGE_TIMEOUT_MS, IMAGE_TOOL_NAME, LEGACY_STATUS_HOSTS, MAX_IMAGE_BYTES, MAX_LIVE_CLI_SESSIONS, MCP_SERVER_NAME, MCP_TOOL_PREFIX, PACKYCODE_AUTO_SEARCH_PROVIDER_ID, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PACKYCODE_SEARCH_PROVIDER_ID, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SEARCH_BACKENDS, SEARCH_BACKEND_MODELS, SEARCH_MODES, SETTINGS_NAMESPACE, TEST_MESSAGE_POLICY_CODE, apply, assertApiKey, baseUrlOf, buildCliArgs, buildCliEnvironment, buildSearchRequest, buildTranscript, canonicalMessage, catalogCoverage, classifyCliOutdated, classifyCliUnavailable, classifyClientPolicy, classifyGatewayFailure, classifyTestMessagePolicy, classifyUpstreamServerError, claudeBinaryCandidates, claudeTransportFor, cliErrorMessage, cliToolName, createBalanceReader, createClaudeCliManager, createClaudeCliTransport, createClaudeToolNames, createDiagnostics, createGroupReader, createImageController, createKeyChooser, createModelSelectionController, createPackyCodeAutoSearchProvider, createPackyCodeImageClient, createPackyCodeImageTool, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, createPackyCodeSearchProvider, createProfileSearchComposition, createRouteAdapter, createSearchComposition, createSearchController, decodeImagePng, describeRequest, dshSystemText, explainCliClientPolicyFailure, explainCliOutdated, explainCliUnavailable, explainClientPolicyFailure, explainNpmFailure, explainTestMessagePolicyFailure, guardKeyRotation, inject, isClaudeCliOutdated, isNpmClaudeInstall, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, parseClaudeVersion, parseMessagesSearchResponse, parsePricing, parseResponsesSearchResponse, parseSelectedGroups, parseTokenUsage, pickKeyForModel, planContinuation, pngDimensions, readHiddenModels, readImagePreference, readSearchChoice, resolveSpawnCommand, splitConversation, startClaudeCliSession, startClaudeMcpBridge, statusHostOrder, summarizeSessionEvents, toolResultToMcp, validateImageOptions };
7877
+ export { CHANNEL, CLAUDE_INSTALL_COMMAND, CLAUDE_MIN_VERSION, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_LATEST_URL, CLAUDE_NPM_PACKAGE, CLAUDE_NPM_UPDATE_COMMAND, CLAUDE_SELF_UPDATE_COMMAND, CLI_ONLY_GROUPS, CLI_SESSION_IDLE_MS, CLI_TOOL_NOTE, Config, DEFAULT_API_HOST, DEFAULT_CLAUDE_CONFIG_DIR, DEFAULT_IMAGE_MODEL, DEFAULT_SEARCH_BACKEND, DEFAULT_STATUS_HOST, DIAGNOSTICS_FORMAT, DIAGNOSTICS_LIMITS, DISPLAY_NAME, HTTP_CLAUDE_GROUPS, IMAGE_MODELS, IMAGE_MODEL_IDS, IMAGE_QUALITIES, IMAGE_TIMEOUT_MS, IMAGE_TOOL_NAME, LEGACY_STATUS_HOSTS, MAX_IMAGE_BYTES, MAX_LIVE_CLI_SESSIONS, MCP_SERVER_NAME, MCP_TOOL_PREFIX, PACKYCODE_AUTO_SEARCH_PROVIDER_ID, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PACKYCODE_SEARCH_PROVIDER_ID, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SEARCH_BACKENDS, SEARCH_BACKEND_MODELS, SEARCH_MODES, SETTINGS_NAMESPACE, TEST_MESSAGE_POLICY_CODE, apply, assertApiKey, baseUrlOf, buildCliArgs, buildCliEnvironment, buildSearchRequest, buildTranscript, canonicalMessage, catalogCoverage, classifyCliOutdated, classifyCliUnavailable, classifyClientPolicy, classifyGatewayFailure, classifyTestMessagePolicy, classifyUpstreamServerError, claudeBinaryCandidates, claudeTransportFor, cliErrorMessage, cliToolName, createBalanceReader, createClaudeCliManager, createClaudeCliTransport, createClaudeLatestReader, createClaudeToolNames, createDiagnostics, createGroupReader, createImageController, createKeyChooser, createModelSelectionController, createPackyCodeAutoSearchProvider, createPackyCodeImageClient, createPackyCodeImageTool, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, createPackyCodeSearchProvider, createProfileSearchComposition, createRouteAdapter, createSearchComposition, createSearchController, decodeImagePng, describeRequest, dshSystemText, explainCliClientPolicyFailure, explainCliOutdated, explainCliUnavailable, explainClientPolicyFailure, explainNpmFailure, explainTestMessagePolicyFailure, guardKeyRotation, inject, isClaudeCliOutdated, isNpmClaudeInstall, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, parseClaudeVersion, parseMessagesSearchResponse, parsePricing, parseResponsesSearchResponse, parseSelectedGroups, parseTokenUsage, pickKeyForModel, planContinuation, pngDimensions, readHiddenModels, readImagePreference, readSearchChoice, resolveSpawnCommand, splitConversation, startClaudeCliSession, startClaudeMcpBridge, statusHostOrder, summarizeSessionEvents, toolResultToMcp, validateImageOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-packycode-api",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "description": "Use PackyCode API keys in DeepSeek Harness with multi-protocol models, image generation and editing, separate key rings, and balance display",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",