@mars-sea/dsh-commandcode-provider 0.11.8 → 0.11.10

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
@@ -4,7 +4,61 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased]
7
+ ## [0.11.10] - 2026-09-23
8
+
9
+ ### Added
10
+
11
+ - **Opt-in zero data retention on every chat request.** With `zdr` on (default off), both chat transports send `x-cmd-zdr: 1` on every request. Command Code refuses a model without an available ZDR upstream with `422 cmd_zdr_no_providers`; the plugin explains that error and never retries without the header. `KNOWN_NON_ZDR_MODELS` remains an informational snapshot of the official CLI’s exception list, not a reason to drop protection. The decision endpoint behind the AI command guard has no ZDR upstream and remains a separately documented opt-in. ZDR capacity uses pass-through pricing and can cost more than the ordinary catalog estimate.
12
+
13
+ - **An opt-in AI command guard: the decision model can approve a safe shell command without interrupting you.** dsh decides *that* a tool call needs approval (permission presets, `PreToolUse` hooks, the sandbox's escalation path) and then asks — there is no second opinion about whether the command is actually safe. With `commandGuard` on, this plugin inserts one answerer in front of the human on the harness's `approval/request` waterfall and asks Command Code's decision model `typesafe/jev` (`POST {apiBase}/provider/v1/systemone`, the same API key and base URL as chat) whether the command is safe to run without asking; only a probability of "safe" at or above `commandGuardThreshold` (default 0.9) becomes a one-shot `allowed-once` grant. **Off by default**, and fail-closed at every step: an unknown call, a non-shell tool, a command longer than 6000 characters (a verdict on a truncated command is a verdict on a different command), a built-in denylist hit (`sudo`, `rm -rf /`, `git push`, publish/upload commands, `curl | sh`, …), a timeout (`commandGuardTimeoutMs`, default 1500 ms), a rate limit, a missing key or an unreadable answer all delegate to the normal prompt. The guard registers only where the approval seam exists (`ctx.inject(['approval'], …)`), reads `tools/pre-execute` purely as a transparent observer (`next()` always — it can never change a decision), and must be prepended on the waterfall because the browser's approval panel otherwise claims the request first. Privacy: the command text, the agent's own description, the working directory and the asker's reason are sent to Command Code, and `typesafe/jev` has **no ZDR upstream** — which is why the feature is opt-in and documented as incompatible with a ZDR-only policy. Billing: free through 2026-09-24, then $0.042/1M input tokens; an identical decision context in the same agent reuses one verdict for 10 minutes, and the timeout also bounds credential lookup. Every decision is logged and lands in the session's own approval audit (`approval/asked`/`approval/decided`).
14
+
15
+ ### New settings
16
+
17
+ - `commandGuard` (default `false`), `commandGuardThreshold` (0.5–1, default 0.9) and `commandGuardTimeoutMs` (200–10000, default 1500) — editable in the web page's Advanced card, in the dsh-TUI section, or in your profile config.
18
+ - `zdr` (default `false`) — zero data retention; editable in the web page's Advanced card, in the dsh-TUI section, or in your profile config.
19
+
20
+ ### Changed
21
+
22
+ - **Synced with the official command-code@1.64.0 CLI** (2026-09-23 check of the npm `latest`; two releases since the last sync — 1.63.0 and 1.64.0, neither of which has a changelog entry yet, so the evidence is the bundle, the public catalog and the docs). `COMMAND_CODE_CLI_VERSION` is now `1.64.0`. The train is purely additive: the CLI registry grows 83 → 86 with exactly three additions and no removal or edit, and the public catalog serves 80 models — exactly those three more than the 77 this plugin snapshot covered. `claude-opus-5-5` (Claude Opus 5.5, 1M context, text+image, the five-level `low`/`medium`/`high`/`xhigh`/`max` effort set, `['/messages']` in the catalog so the existing `claude-*` rule keeps it on the CLI transport) is a Provider/Max model — the first since `claude-opus-5` — at $4 / $20 / $0.2 / $5 per 1M, actually cheaper than its predecessor. `gpt-6-sol` (GPT-6 Sol, 1.05M context, same vision and efforts, Pro and above) is $2 / $10 / $0.2 / $2.5 inside a 272K band and $4 / $15 / $0.4 / $5 past it; `gpt-6-luna` (GPT-6 Luna, the opensource-category sibling, on every plan including Go) is $0.1 / $0.5 / $0.01 / $0.125 and $0.2 / $0.75 / $0.02 / $0.25 past the same band. In snapshot terms: the effort map gains three keys (48 → 51), the Vision set three (57 → 60), the plan map one Provider, one Pro and one Go entry without moving a tier (49/57/70/77 → 50/58/71/80), and the vendored price table exactly the three matching rows (76 → 79) with no rate change to any existing row (`node scripts/sync-model-prices.mjs` regenerated the array and cross-checked every rate against the page's rendered table). The same check re-verified unchanged: the subscription plan maps, the ZDR exception set (both CLI anchors extracted from both bundles — the union is 20 members in each, so `meituan/LongCat-2.0` was already excepted at 1.62.0 and the note in `src/capabilities.ts` now says so), the deals, the peak membership and schedule, the automatic-reasoning set, and the whole wire protocol — every `/alpha/*` path, the `x-command-code-version` / `x-cmd-zdr` header table, the stream event vocabulary and the browser-login call site are identical between the two bundles. Two things ARE new upstream and neither changes this adapter's two chat transports. The Provider API documents a fifth endpoint, `POST /provider/v1/systemone` (System One decisions, model `typesafe/jev`, absent from `/provider/v1/models` and never to be listed as a chat model): it is the endpoint the command guard above already speaks, so this sync confirms that contract rather than adding one — its three question types, its 20-question ceiling and its free-through-2026-09-24 window all match the CLI's own `systemOneRequestSchema`. And the docs' deals list carries two temporary "boosted usage" plan-budget promotions (`deepseek-v4.1-flash` and `mimo-v2.6-flash`) that raise a plan's credit allowance rather than any per-token rate — no `deal` object, no price row, nothing in the snapshot.
23
+
24
+ ## [0.11.9] - 2026-09-22
25
+
26
+ ### Changed
27
+
28
+ - **dsh 0.1.7-alpha.1 is a declared-compatible engine, and the release that rewrote settings needs no user action to keep working.** The engine replaced its settings.yaml document with schema-derived Config forms (`SettingsProvider` → `SettingsForms`, and `installSection` plus the browser's `settingsScope` service are both gone), so this release carries the two-generation adaptation: every Config field except the top-level `apiKey` secret is marked volatile through a feature-detected bridge (`src/config-volatile.ts`) — an unmarked field would be invisible to and unwritable from 0.1.7's settings page — while the ≤0.1.6 registration keeps an unmarked schema and an unwrapped config, exactly as that generation validates and clones it; volatile writes commit in place and re-run the two facts not re-derived per use (the web-search selection and the dsh-TUI section re-registration); and the browser side speaks the `remote.settings` describe/mutate wire directly through the plugin's own scope, so one implementation serves 0.1.2-rc.1 through 0.1.7-alpha.1. The one-time settings.yaml import needs no cooperation from the plugin: the engine looks the section up by profile entry id, which is the same string as the section id (`llm-commandcode`) and must stay that way. `npm run test:engine` passes against 0.1.7-alpha.1 (the bundle links and evaluates there — the request-image policy is the durable one — and the adapter is driven with each engine's own message constructors), so a release installed beside the newest engine is proven to load before it ships.
29
+
30
+ - **The plugin manager now shows a real identity for this plugin on dsh 0.1.7+.** The package declares `icon` (`icon.svg`) and ships `locale/en.json` + `locale/zh.json` under the `locale/*.json` export, so the plugin-management page renders "Command Code" / "Command Code 提供方" with a description instead of a bare package name and a placeholder icon. Both are pure metadata: older engines ignore them, and every other install path is unaffected.
31
+
32
+ ### Fixed
33
+
34
+ - **The account card's "无法连接 Command Code 服务 / 请检查网络连接或 API 地址设置" is no longer a guess, and three of its false causes are gone.** `classifyTotalFailure()` answers `network` whenever all four account endpoints throw without a status, so one verdict covered a real outage, a per-request timeout, a key no HTTP header can carry and an unparseable `apiBase` — while the card hid the only evidence, the four per-endpoint messages. Now: (a) the account path validates the key through the harness's own `assertUsableApiKey`, as chat already did, and answers `blocked: 'invalid-key'` with the credential message instead of four phantom transport failures (a key pasted with an embedded newline or a full-width character makes `fetch` throw a `TypeError` **before any I/O**, once per endpoint — the connection was fine); (b) the four endpoints use the connection's own `requestTimeoutMs` (the SAME 60 s budget as a chat call) instead of the catalog's hidden 10 s cap, which on a slow link made every account query time out while chat kept working; (c) both surfaces now render `report.failures`, so the next occurrence names its actual cause. Refresh keeps working in every case — it was the diagnosis, not the fetch lifecycle, that was wrong.
35
+
36
+ - **Turning the web-search toggle off no longer destroys the user's own `commandcode` pin.** `applyCommandCodeSearchSelection()` remembered "the field already read `commandcode` before we touched it" as `displaced: undefined`, and its disable branch wrote that value back — which is not the no-op its contract promises. The field was cleared, dsh-web reads a cleared `searchProviderId` as auto-select, and with a second usable provider registered (the shipped `deepseek-official` is usable whenever a DeepSeek key resolves) EVERY later search threw `WEB_PROVIDER_AMBIGUOUS`; the same write ran on plugin unload. The tracked selection now carries `preexisting`, so a field we did not take over is left alone, while a field that was genuinely unset at takeover is still cleared (or the toggle would leave Command Code serving). `tests/web-search.test.ts` pins both directions, including an end-to-end boot with `searchProvider: commandcode`.
37
+
38
+ - **A 5xx that proxies a credits/plan rejection rotates the account instead of being retried as a transient outage.** `classifyAccountRejection()` hoisted only `RATE_LIMITED` above its `status >= 500` guard, so `503 {"code":"INSUFFICIENT_CREDITS"}`, `502 MODEL_NOT_IN_PLAN`, `USAGE_EXCEEDED` and `PREMIUM_CREDITS_EXHAUSTED` were classified as "the provider is unavailable": no rotation, and `SERVER` put the byte-identical request on dsh-llm-retry's 1000-attempt cadence while the configured sibling accounts were never consulted. All four structured codes (`ACCOUNT_UNAVAILABLE_CODES`) are now status-independent, exactly like `RATE_LIMITED`; only the prose scan stays confined to 4xx.
39
+
40
+ - **A provider-published rate-limit reset of an implausible magnitude no longer breaks the failure it belongs to.** `error.rateLimit.reset` is seconds multiplied by 1000 and was trusted verbatim: `1e300` reached `new Date(1e303).toISOString()` and threw `RangeError: Invalid time value` out of `stream()` in place of the intended retryable `RATE_LIMIT`, and a mark pinned to it became a `cooldown` that no probe ever revisits (the account left rotation for the life of the process). Resets are now accepted only within `MAX_TRUSTED_RESET_MS` (30 days — the longest window this provider meters, so the bound rejects nonsense rather than second-guessing a schedule) at BOTH seams: the rejection body and the `/alpha/billing/credits` probe. A dropped reset degrades the mark to `unknown`, which the probe pass re-checks.
41
+
42
+ - **An in-band stream error's `statusCode` no longer outvotes a terminal marker or `isRetryable: false`.** `{statusCode: 500, message: 'insufficient credits'}` was reported as retryable `SERVER`, so a permanent quota/plan refusal was replayed on the 1000-attempt cadence. The terminal markers now also match the spaced spelling (the separator is normalized, as the pre-stream classifier already did), and an explicit refusal or a terminal marker outranks a retryable status; an explicit `isRetryable: true` still wins.
43
+
44
+ - **The first usage report no longer sticks as a false "remote is not mounted" error.** The plugin's `remote.commandcode` namespace lands one wire round-trip after the plugin asks for it, and a surface that asked first (the sidebar quota card does, on every boot) stored the synthetic not-mounted failure as `status: 'error'` — and the card's `shouldRefresh` only fires from `idle`, so nothing retried it until the panel's two-minute tick. The mount callback now re-reads the usage report through a seam declared before the effect (a `const` read in its temporal dead zone would throw). `tests/client-boot.test.ts` boots with the namespace deferred by a macrotask and asserts that re-read.
45
+
46
+ - **A settings directory whose FIRST read fails retries instead of leaving the page permanently disabled.** With nothing ever held, the mirror keeps its initial `status: 'loading'`/`writable: false` snapshot, which the settings page renders as a fully disabled form behind a "read-only" banner — and the forwarded invalidations need a live Host to fire, so a Host that was still starting up left the page dead until a reload. `SettingsDescribeMirror` now retries a first-read failure on a bounded 1/2/4 s ladder (`SETTINGS_DESCRIBE_RETRY_MS`, injectable timer, cancelled on disposal); a failure with a HELD view schedules nothing, because the page is already showing the last good document.
47
+
48
+ - **A concurrent `loginBegin` no longer leaks a loopback login server.** `begin()` rejoin read a status that only becomes `waiting` after two awaits, so two GUI tabs (or a Sign-in following a cancel that arrived too early to retire anything) each bound their own server — and only the last one was reachable by `teardown()`, so the orphan kept listening and answering `/callback` for the process's lifetime; ten of those exhausted the login port window. `begin()` is now single-flight across the bind, a `cancel()` in that window sets `cancelPending` (observed after the bind, which closes the fresh server and publishes `failed/cancelled`), and `bindServer` closes a server it does not track.
49
+
50
+ - **`DSH_COMMANDCODE_TRACE=false` (or `0`/`no`/`off`) now disables the trace instead of enabling it.** Only `1`/`true`/`yes`/`on` were recognized as flags and everything else was taken for a path, so an explicit disable switched the trace ON and wrote the conversation to a file named `false` in the process's working directory.
51
+
52
+ - **DSH 0.1.7 tool results survive into the next model request.** That engine uses `role: 'tool'` with message-level `toolCallId` and raw content blocks; the adapter only recognized the older user-role `tool-result` wrapper and silently discarded both results and their paired calls on both transports. The model consequently lost completed work, could repeat tools, and could end with only a statement of intent. Both envelopes now share pairing and serialization, including errors, images and long call ids. Engine smoke tests capture both request bodies using each engine's actual message constructors.
53
+
54
+ - **Reasoning-only replies no longer silently complete a turn.** Both transports now validate that a terminal response contains answer text or a tool call before emitting a success finish. Empty completions use the existing `EMPTY_RESPONSE` retry policy; output-limit completions without an answer report non-retryable `OUTPUT_TOKEN_LIMIT` with the actual budget and available usage, without assuming all tokens went to reasoning. Received usage is retained on failure, and trace terminal diagnostics survive the payload cap.
55
+
56
+ - **A reply cut off mid-generation is a reported failure again, not a turn that quietly finished** (report: 《调用会莫名其妙的自动停止,并没有任何报错信息》). The mechanism was in the stream tail: when the response body ended without its transport's terminal event — the gateway or an intermediary closing the socket mid-generation — the adapter closed the open blocks and yielded a clean `finish: { kind: 'stop' }`. Both transports DO terminate explicitly (the CLI transport with a `finish` event, the Provider API with a final `finish_reason` chunk), so an EOF there is not a normal ending, and reporting it as one is exactly why a truncated answer arrived with no error in the UI and nothing in the session log: the harness saw a completed turn. The tail now classifies. `!sawContent` (no visible text AND no tool call) keeps the pre-existing retryable `EMPTY_RESPONSE`, because such a request is free to repeat and the retry policy's whitelist absorbs it — a cut during a long thinking phase lands there, unchanged. A cut that HAD produced content throws `STREAM_CLOSED`, the code `dsh-llm-deepseek`'s own `translate()` raises for the same condition ("… stream ended before message_stop"); a tool call still in flight when the socket closed is dropped rather than executed with truncated JSON arguments. The code is deliberately OUTSIDE `providerRetryPolicy()`'s whitelist for now: every retry of a content-bearing cut re-generates the whole answer, so auto-retrying is a cost decision rather than an obvious win (adding it to `retryableCodes`, ideally bounded by `src/transport-retry.ts`'s per-agent budget, is the one-line opt-in). `tests/adapter.test.ts` pins both endings on both transports, the in-flight-tool-call refusal, and the untouched `EMPTY_RESPONSE` path.
57
+ - **The Command Code settings page is interactive again on dsh 0.1.7** (report: 《Command Code 设置页面所有的按钮和开关都无法点击,下拉选择模型也无法使用》). The page rendered — with its read-only banner, empty text fields and every switch, button and select disabled — because the plugin's own settings scope read `ctx.remote.settings` directly, and `remote.settings` is a Cordis service NESTED under the `remote` service (api-gateway registers one `Service` per contribution namespace as `remote.<ns>`): a read from a fiber that does not declare that name in its inject list is answered with `Error: cannot get property "remote.settings" without inject`. The throw landed inside the scope's own `try`/`catch`, so nothing was logged and no `settings/describe` ever left the browser; the mirror stayed `idle`, the scope kept its INITIAL snapshot (`status: 'loading'`, `writable: false`, `value: undefined`), and since the page disables its controls on `!state.writable`, everything was inert. The boolean toggles were the confusing part of the report: they fall back to their hardcoded `defaultChecked` while the scope has no value, so they looked "set" over empty text inputs. `createSettingsScope()` now takes the namespace as a resolver, and the client entry fills it from the object `ctx.inject(['remote.settings'], …)` resolves — the same shape the `remote.credentials` and `remote.commandcode` reads have always used. `$host` / `$on` / `$mount` are plain members of the declared `remote` service, so those stay direct reads, and no namespace was added to the exported client `inject` list: gating the whole client half on a service a profile may not serve would silence every surface, which is the failure this scope is built to avoid. Verified end to end against a real dsh 0.1.7-alpha.1 web app (the page converges on the stored row, a toggle marks it dirty, the model multi-select opens its 76-model list, and a save lands in the profile document and survives a reload); `tests/settings-scope.test.ts` now builds its fake context with a `settings` getter that throws the engine's exact error and covers the unmounted → mount + `refresh()` handshake, and `tests/client-boot.test.ts` mounts its namespaces as real `remote.<ns>` Cordis services — with the old direct read restored, the boot suite fails.
58
+
59
+ ### Added
60
+
61
+ - **`DSH_COMMANDCODE_TRACE=<path>`: an opt-in raw-stream trace, so the next "it stopped in the middle" report arrives with evidence** (`src/stream-trace.ts`). A cut stream is diagnosed from the wire or not at all — which events actually arrived, how long the socket then sat silent, and how it ended are the facts that separate "the provider closed it" from "our parser missed the terminal event", and until now the adapter kept none of them. With the variable set (a bare `1`/`true`/`yes`/`on` writes to `$TMPDIR/dsh-commandcode-stream.jsonl`), every request appends JSONL: one record per raw chunk carrying the provider's own payload verbatim, plus `response` / `eof` / `idle-timeout` / `read-error` / `end` / `stream-close`, each stamped with milliseconds since that request's first byte. Records never include the request body, an API key, or a request header; one request is capped at 4 MiB (`trace-capped` names the cap); a path that cannot be written turns the trace off rather than failing the request it is observing; and unset, the whole facility is one environment read and some inert calls. `scripts/probe-stream.mjs` is the companion isolation test — it drives the adapter alone, with no harness and no session, so a cut seen there is the provider's while a clean `finished` puts the cut above the adapter (`PROBE_MODEL` / `PROBE_MAX_TOKENS` / `PROBE_PROMPT` / `PROBE_TRACE` override its defaults). `tests/stream-trace.test.ts` pins switch resolution, the JSONL shape, the cap, the degradation paths, and the adapter wiring on both a finished and a cut stream. Both live probes so far (`deepseek/deepseek-v4.1-flash`, 13 and 244 chunks) reached `finished` on `/provider/v1/chat/completions`, so the gateway does not cut unconditionally.
8
62
 
9
63
  ## [0.11.8] - 2026-09-22
10
64
 
package/README.md CHANGED
@@ -54,7 +54,7 @@ Pick the release line that matches your DeepSeek Harness version:
54
54
  **pnpm 11 holds back new releases.** Its `minimumReleaseAge` defaults to 1440 minutes, so a version published less than a day ago is skipped and `@latest` resolves to the *previous* release — silently, with a success exit code. To install a release from the last 24 hours, name it exactly:
55
55
 
56
56
  ```sh
57
- dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.11.1
57
+ dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.11.9
58
58
  ```
59
59
 
60
60
  The same applies to every profile you install into, including the terminal UI below.
@@ -73,7 +73,7 @@ dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@0.9.1 #
73
73
  Each profile updates separately — the terminal UI owns its own plugin list (see below):
74
74
 
75
75
  ```sh
76
- dsh plugin --profile dsh-tui update @mars-sea/dsh-commandcode-provider@0.11.1
76
+ dsh plugin --profile dsh-tui update @mars-sea/dsh-commandcode-provider@0.11.9
77
77
  ```
78
78
 
79
79
  To move to a version published less than 24 hours ago, name it exactly as in Install above; pnpm 11's age gate resolves `@latest` to the previous release instead.
@@ -102,7 +102,7 @@ After restart, enter your API key in **Settings → Command Code** and save; **S
102
102
  The plugin also works under a terminal front door. **Each dsh profile owns its own plugin list**, so the web install above does not reach the terminal — add the plugin to the `dsh-tui` profile as well:
103
103
 
104
104
  ```sh
105
- dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider@0.11.1
105
+ dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider@0.11.9
106
106
  ```
107
107
 
108
108
  Pin the exact version here. For the first 24 hours after a release, a bare package name (or `@latest`) is silently resolved to the previous one: the install succeeds, but the profile gets the older build — which is how a fresh terminal install ends up with no **`/settings` → Command Code** page and no `commandcode` models at all.
@@ -188,7 +188,7 @@ llm-commandcode:
188
188
 
189
189
  ## Configure
190
190
 
191
- **Settings → Command Code** covers the API key, API base URL, working directory, and request/stream timeouts; once a key is saved, a live **Account usage** card appears at the top of the page. The **Advanced** card holds the toggles — hide out-of-plan models, serve web search with Command Code, and show the quota card in the sidebar (off by default).
191
+ **Settings → Command Code** covers the API key, API base URL, working directory, and request/stream timeouts; once a key is saved, a live **Account usage** card appears at the top of the page. The **Advanced** card holds the toggles — hide out-of-plan models, serve web search with Command Code, show the quota card in the sidebar (off by default), the AI command guard (off by default, see below), and zero data retention (off by default, see below).
192
192
 
193
193
  The same options live in `$DSH_HOME/settings.yaml` (changes apply immediately, no restart):
194
194
 
@@ -201,6 +201,10 @@ llm-commandcode:
201
201
  requestTimeoutMs: 60000 # default 60s
202
202
  streamIdleTimeoutMs: 300000 # default 300s
203
203
  showSidebarQuota: true # optional: show the plans & quota card in the sidebar (default off)
204
+ commandGuard: true # optional: let the decision model auto-approve safe shell commands (default off)
205
+ commandGuardThreshold: 0.9 # optional: probability of "safe" needed to skip the prompt (0.5-1, default 0.9)
206
+ commandGuardTimeoutMs: 1500 # optional: decision budget in ms (200-10000, default 1500)
207
+ zdr: true # optional: route requests only through zero-data-retention upstreams (default off)
204
208
  ```
205
209
 
206
210
  ## Web search
@@ -215,6 +219,32 @@ When your deployment's dsh shell mounts the web capability (`@deepseek-ai/dsh-we
215
219
 
216
220
  > This reuses the Command Code Provider API directly (like the official CLI's built-in `web_search`), so it is distinct from a DeepSeek-native search backend.
217
221
 
222
+ ## AI command guard
223
+
224
+ When dsh is about to ask you to approve a shell command (`bash`/`pwsh`) — a permission preset, a `PreToolUse` hook, or a sandbox escalation — this plugin can ask the Command Code decision model `typesafe/jev` whether the command is safe to run without asking. A confident *yes* turns into a one-shot `allowed-once` grant; everything else asks you exactly as before.
225
+
226
+ **Off by default.** Turn it on with the *"Let AI judge shell commands"* toggle (`commandGuard`) in the Advanced card, or in your profile config. What the guard does and does not do:
227
+
228
+ - It only ever sees commands dsh had **already decided to ask about** — it never widens a permission policy, and it never answers a session whose policy is "never ask".
229
+ - Only a `yes` with probability at least `commandGuardThreshold` (default 0.9) skips the prompt. A `no`, a low-confidence answer, a timeout (default 1500 ms), a rate limit, a missing key, an unreadable answer, a command longer than 6000 characters, or a match against the built-in denylist (`sudo`, `rm -rf /`, `git push`, publish/upload commands, piping a download into a shell, …) all fall back to the normal prompt.
230
+ - The **command text** (plus the agent's own one-line description, the working directory and the asker's reason) is sent to Command Code to be judged — with the same API key and base URL as chat.
231
+ - The decision model has **no zero-data-retention upstream**, so this feature is incompatible with a ZDR-only policy. Leave it off if that matters to you.
232
+ - The endpoint is free through 2026-09-24; after that it bills $0.042 per 1M input tokens (output free) — a single decision is a few hundred tokens, and an identical command with the same approval context in the same agent reuses the previous verdict for 10 minutes.
233
+
234
+ Every AI decision is logged on the Host — `llm-commandcode: command guard auto-approved a bash call (safe probability 0.97, model): <the command>` for a grant, `… delegated a bash call: <reason> — <the command>` for everything that fell through — and the outcome lands in the session's own approval audit (`approval/asked` / `approval/decided`), so a grant can always be traced back to the verdict that produced it. The log goes to the Host's console (the terminal running `dsh web`, or the desktop app's log), not to the browser.
235
+
236
+ ## Zero data retention (ZDR)
237
+
238
+ Command Code can serve a request only through upstreams that retain no prompts or completions and never train on them — the same opt-in the official CLI exposes as `CMD_ZDR=1`, and the `x-cmd-zdr: 1` header on the Provider API ([official docs](https://commandcode.ai/docs/resources/zdr)).
239
+
240
+ **Off by default.** Turn it on with the *"Zero data retention (ZDR)"* toggle (`zdr`) in the Advanced card, or in your profile config. How this plugin implements it:
241
+
242
+ - The header is sent on **every chat request** while ZDR is on. The plugin maintains an informational exception list (`KNOWN_NON_ZDR_MODELS`, synced from the official CLI — about 20 models, e.g. `xai/grok-4.5`, `stepfun/Step-3.7-Flash`, `meta/muse-spark-1.3`). A model without an available ZDR upstream fails with `422 cmd_zdr_no_providers`; the request is never retried without the header.
243
+ - If a refusal still happens (coverage churn, or no ZDR upstream with spare capacity at that moment), the error names the cause and how to turn ZDR off, instead of surfacing as a bare HTTP 422.
244
+ - **ZDR usually costs more**: capacity is limited and billed at each upstream's pass-through rates, and which upstream serves a request can change per request. The session-cost readout keeps quoting the ordinary catalog rates; the real per-request price shows in Command Code's Studio usage page.
245
+ - The decision model behind the **AI command guard** has no ZDR upstream, so its requests never carry the header even with `zdr` on — the two features stay independent.
246
+ - Works on every plan; plan credits meter ZDR requests at the plan's default allowance.
247
+
218
248
  ## Notes & limitations
219
249
 
220
250
  - **Image input is model-gated** — only Vision models accept images; text-only models refuse them.
@@ -227,7 +257,7 @@ When your deployment's dsh shell mounts the web capability (`@deepseek-ai/dsh-we
227
257
 
228
258
  ## Permissions & privacy
229
259
 
230
- The plugin only communicates between your local dsh profile and your Command Code account: locally it touches only the credential store and the models cache (plus `~/.commandcode/auth.json` as a last-resort fallback); on the network it calls only the Command Code API. No telemetry.
260
+ The plugin only communicates between your local dsh profile and your Command Code account: locally it touches only the credential store and the models cache (plus `~/.commandcode/auth.json` as a last-resort fallback); on the network it calls only the Command Code API. No telemetry. The one optional exception is the **AI command guard** (off by default), which sends the shell command it is asked to judge — see above. With **zero data retention** on (also off by default), chat requests require a ZDR upstream; models without one fail instead of running without the protection.
231
261
 
232
262
  ## Disabling / uninstalling
233
263
 
package/README.zh-CN.md CHANGED
@@ -52,7 +52,7 @@
52
52
  **pnpm 11 会拦下刚发布的新版本。** 它的 `minimumReleaseAge` 默认为 1440 分钟,发布不足一天的版本会被跳过,`@latest` 解析到**上一个**版本 —— 而且是静默的,命令照样以成功退出。想装 24 小时内发布的版本,必须写精确版本号:
53
53
 
54
54
  ```sh
55
- dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.11.1
55
+ dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.11.9
56
56
  ```
57
57
 
58
58
  这一点对每个 profile 都成立,包括下面的终端界面。
@@ -71,7 +71,7 @@ dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@0.9.1 #
71
71
  每个 profile 各自更新 —— 终端界面有独立的插件列表(见下文):
72
72
 
73
73
  ```sh
74
- dsh plugin --profile dsh-tui update @mars-sea/dsh-commandcode-provider@0.11.1
74
+ dsh plugin --profile dsh-tui update @mars-sea/dsh-commandcode-provider@0.11.9
75
75
  ```
76
76
 
77
77
  要更新到发布不足 24 小时的版本,和上面的安装一样写精确版本号;pnpm 11 的年龄门禁会把 `@latest` 解析成上一个版本。
@@ -100,7 +100,7 @@ cmd login # macOS/Linux;Windows 原生版:cmdc login
100
100
  插件同样支持终端前端。**每个 dsh profile 有独立的插件列表**,所以上面那条 Web 安装命令不会装到终端里 —— 还要把插件装进 `dsh-tui` profile:
101
101
 
102
102
  ```sh
103
- dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider@0.11.1
103
+ dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider@0.11.9
104
104
  ```
105
105
 
106
106
  这里请写精确版本号。新版本发布后的 24 小时内,只写包名(或 `@latest`)会被静默解析到上一个版本 —— 安装命令照样成功,但 profile 里拿到的是旧版本,结果就是全新的终端安装里既没有 **`/settings` → Command Code** 页面,也看不到任何 `commandcode` 模型。
@@ -186,7 +186,7 @@ llm-commandcode:
186
186
 
187
187
  ## 配置
188
188
 
189
- **设置 → Command Code** 可配置 API key、API 地址、工作目录与请求/流超时;配置好 key 后,页面顶部会显示实时「账户用量」卡片。**高级设置**卡片里集中了各个开关:隐藏套餐外模型、用 Command Code 承载联网搜索,以及在侧边栏显示额度卡片(默认关闭)。
189
+ **设置 → Command Code** 可配置 API key、API 地址、工作目录与请求/流超时;配置好 key 后,页面顶部会显示实时「账户用量」卡片。**高级设置**卡片里集中了各个开关:隐藏套餐外模型、用 Command Code 承载联网搜索、在侧边栏显示额度卡片(默认关闭)、AI 命令安全预判(默认关闭,见下),以及零数据保留 ZDR(默认关闭,见下)。
190
190
 
191
191
  同一组选项也位于 `$DSH_HOME/settings.yaml`(修改即刻生效,无需重启):
192
192
 
@@ -199,6 +199,10 @@ llm-commandcode:
199
199
  requestTimeoutMs: 60000 # 默认 60s
200
200
  streamIdleTimeoutMs: 300000 # 默认 300s
201
201
  showSidebarQuota: true # 可选:在侧边栏显示套餐与配额卡片(默认关闭)
202
+ commandGuard: true # 可选:让决策模型自动放行安全的 shell 命令(默认关闭)
203
+ commandGuardThreshold: 0.9 # 可选:跳过弹窗所需的「安全」概率(0.5-1,默认 0.9)
204
+ commandGuardTimeoutMs: 1500 # 可选:决策等待上限(毫秒,200-10000,默认 1500)
205
+ zdr: true # 可选:请求只经由零数据保留上游(默认关闭)
202
206
  ```
203
207
 
204
208
  ## 联网搜索
@@ -213,6 +217,32 @@ llm-commandcode:
213
217
 
214
218
  > 这里直接使用 Command Code Provider API(与官方 CLI 内置的 `web_search` 相同),因此与 DeepSeek 原生搜索后端不同。
215
219
 
220
+ ## AI 命令安全预判
221
+
222
+ 当 dsh 准备就某条 shell 命令(`bash`/`pwsh`)征求你同意时——无论是权限预设、`PreToolUse` 钩子还是沙箱提权——本插件可以先让 Command Code 的决策模型 `typesafe/jev` 判断这条命令是否安全到无需询问。判断为「安全」且把握足够高时,直接放行这一次调用;其余情况一律照旧询问你。
223
+
224
+ **默认关闭。** 在高级设置里打开「用 AI 预判命令是否安全」(`commandGuard`),或写进 profile 配置。它的行为边界:
225
+
226
+ - 它只会看到 dsh **本来就要询问你**的命令——不会放宽任何权限策略,也不会替「从不询问」(policy 为 `never`)的会话作答。
227
+ - 只有「安全」概率不低于 `commandGuardThreshold`(默认 0.9)时才跳过弹窗。判为不安全、把握不足、超时(默认 1500ms)、限流、缺 key、返回体读不懂、命令超过 6000 字符,或命中内置危险名单(`sudo`、`rm -rf /`、`git push`、发布/上传类命令、把下载内容管进 shell 等),全部回到普通弹窗。
228
+ - 送往 Command Code 判断的内容包括**命令原文**、模型自己写的一行描述、工作目录与询问原因;使用与聊天相同的 API key 与 base URL。
229
+ - 该决策模型**没有零数据保留(ZDR)上游**,因此本功能与「只走 ZDR」的合规要求不兼容;如果你在意这一点,请保持关闭。
230
+ - 该端点 2026-09-24 前免费,之后按输入 $0.042/1M tokens 计费(输出免费)——单次判断只有几百 tokens;同一 agent 中命令和审批场景都相同时,10 分钟内复用上一次结论。
231
+
232
+ 每次 AI 决策都会在 Host 侧写日志——放行是 `llm-commandcode: command guard auto-approved a bash call (safe probability 0.97, model): <命令>`,其余是 `… delegated a bash call: <原因> — <命令>`——并进入会话自身的审批审计(`approval/asked` / `approval/decided`),所以任何一次放行都能追溯到产生它的判断。日志打到 Host 控制台(运行 `dsh web` 的终端,或桌面端日志),不进浏览器。
233
+
234
+ ## 零数据保留(ZDR)
235
+
236
+ Command Code 可以让请求只经由「不留存提示词与回复、也不用于训练」的上游——官方 CLI 的开关是 `CMD_ZDR=1`,Provider API 上则是在请求头发 `x-cmd-zdr: 1`([官方文档](https://commandcode.ai/docs/resources/zdr))。
237
+
238
+ **默认关闭。** 在高级设置里打开「零数据保留(ZDR)」(`zdr`),或写进 profile 配置。本插件的实现方式:
239
+
240
+ - 开启后,**每次聊天请求**都会带上 ZDR 请求头。插件仍维护官方 CLI 的例外名单(`KNOWN_NON_ZDR_MODELS`,约 20 个模型,例如 `xai/grok-4.5`、`stepfun/Step-3.7-Flash`、`meta/muse-spark-1.3`)供查询。没有可用 ZDR 上游时,服务端返回 `422 cmd_zdr_no_providers`;插件不会去掉请求头重试。
241
+ - 万一仍被拒绝(名单过期,或那一刻没有空闲的 ZDR 上游容量),错误信息会说明原因并给出关闭 ZDR 的办法,而不是抛出一个光秃秃的 HTTP 422。
242
+ - **ZDR 通常更贵**:容量有限,按各上游实价透传计费,且每次请求落在哪个上游可能不同。会话费用读数仍按常规目录价估算;真实单价见 Command Code Studio 的用量页。
243
+ - **AI 命令安全预判**背后的决策模型没有 ZDR 上游,因此即使打开 `zdr`,它的请求也永远不带该请求头——两个功能互不影响。
244
+ - 各套餐均可使用;ZDR 请求按套餐的默认额度(而非提升额度)计量。
245
+
216
246
  ## 注意事项与限制
217
247
 
218
248
  - **图片输入按模型能力限制**:仅 Vision 模型接受图片,纯文本模型会直接拒绝。
@@ -224,7 +254,7 @@ llm-commandcode:
224
254
 
225
255
  ## 权限与隐私
226
256
 
227
- 本插件只在本地与你的 Command Code 账号之间通信:本地仅读写凭据存储与模型缓存文件(兜底读取 `~/.commandcode/auth.json`);网络仅访问 Command Code API。无遥测。
257
+ 本插件只在本地与你的 Command Code 账号之间通信:本地仅读写凭据存储与模型缓存文件(兜底读取 `~/.commandcode/auth.json`);网络仅访问 Command Code API。无遥测。唯一的可选例外是 **AI 命令安全预判**(默认关闭),它会把待判断的命令文本发送出去——见上文。打开**零数据保留**(同样默认关闭)后,聊天请求必须经由 ZDR 上游;没有可用 ZDR 上游的模型会报错,不会按常规方式继续发送。
228
258
 
229
259
  ## 关闭 / 卸载
230
260
 
package/icon.svg ADDED
@@ -0,0 +1 @@
1
+ <svg fill="none" height="137" viewBox="0 0 137 137" width="137" xmlns="http://www.w3.org/2000/svg"><path d="m0 66.7959c0-31.4879 0-47.2318 9.78204-57.01386 9.78206-9.78204 25.52596-9.78204 57.01396-9.78204h2.5357c31.4883 0 47.2323 0 57.0143 9.78204 9.782 9.78206 9.782 25.52596 9.782 57.01396v2.5357c0 31.4883 0 47.2323-9.782 57.0143s-25.526 9.782-57.0144 9.782h-2.5357c-31.4879 0-47.2318 0-57.01386-9.782-9.78204-9.782-9.78204-25.526-9.78204-57.0144z" fill="#000"/><g fill="#fff"><path clip-rule="evenodd" d="m69.3317 5.56633h-2.5357c-15.9014 0-27.2674.01182-35.905 1.17312-8.4775 1.13977-13.4886 3.29415-17.173 6.97855s-5.83878 8.6955-6.97855 17.173c-1.1613 8.6376-1.17312 20.0036-1.17312 35.9049v2.5357c0 15.9014.01182 27.2674 1.17312 35.9054 1.13977 8.477 3.29415 13.488 6.97855 17.173 3.6844 3.684 8.6955 5.838 17.173 6.978 8.6376 1.161 20.0036 1.173 35.9049 1.173h2.5357c15.9014 0 27.2674-.012 35.9054-1.173 8.477-1.14 13.488-3.294 17.173-6.978 3.684-3.685 5.838-8.696 6.978-17.173 1.161-8.638 1.173-20.004 1.173-35.9053v-2.5357c0-15.9014-.012-27.2674-1.173-35.905-1.14-8.4775-3.294-13.4886-6.978-17.173-3.685-3.6844-8.696-5.83878-17.173-6.97855-8.638-1.1613-20.004-1.17312-35.9053-1.17312zm-59.54966 4.21571c-9.78204 9.78206-9.78204 25.52596-9.78204 57.01386v2.5357c0 31.4884 0 47.2324 9.78204 57.0144 9.78206 9.782 25.52596 9.782 57.01386 9.782h2.5357c31.4884 0 47.2324 0 57.0144-9.782s9.782-25.526 9.782-57.0143v-2.5357c0-31.488 0-47.2319-9.782-57.01396-9.782-9.78204-25.526-9.78204-57.0143-9.78204h-2.5357c-31.488 0-47.2319 0-57.01396 9.78204z" fill-rule="evenodd"/><path d="m93.6604 26.1784c-8.982 0-16.2887 7.3067-16.2887 16.2888v6.9809h-18.6158v-6.9809c0-8.9821-7.3067-16.2888-16.2887-16.2888-8.9821 0-16.2888 7.3067-16.2888 16.2888s7.3067 16.2887 16.2888 16.2887h6.9809v18.6158h-6.9809c-8.9821 0-16.2888 7.3067-16.2888 16.2888 0 8.9825 7.3067 16.2885 16.2888 16.2885 8.982 0 16.2887-7.306 16.2887-16.2885v-6.981h18.6158v6.981c0 8.9825 7.3067 16.2885 16.2887 16.2885 8.9826 0 16.2886-7.306 16.2886-16.2885 0-8.9821-7.306-16.2888-16.2886-16.2888h-6.9809v-18.6158h6.9809c8.9826 0 16.2886-7.3066 16.2886-16.2887s-7.306-16.2888-16.2886-16.2888zm-6.9809 23.2697v-6.9809c0-3.8628 3.1182-6.9809 6.9809-6.9809 3.8628 0 6.9806 3.1181 6.9806 6.9809 0 3.8627-3.1178 6.9809-6.9806 6.9809zm-44.2123 0c-3.8628 0-6.9809-3.1182-6.9809-6.9809 0-3.8628 3.1181-6.9809 6.9809-6.9809 3.8627 0 6.9809 3.1181 6.9809 6.9809v6.9809zm16.2887 27.9236v-18.6158h18.6158v18.6158zm34.9045 23.2693c-3.8627 0-6.9809-3.1178-6.9809-6.9805v-6.981h6.9809c3.8628 0 6.9806 3.1182 6.9806 6.981 0 3.8627-3.1178 6.9805-6.9806 6.9805zm-51.1932 0c-3.8628 0-6.9809-3.1178-6.9809-6.9805 0-3.8628 3.1181-6.981 6.9809-6.981h6.9809v6.981c0 3.8627-3.1182 6.9805-6.9809 6.9805z"/></g></svg>