# codebuddy-first-bridge

[简体中文](README.md) · **English**

> A Cordis plugin for the **DeepSeek Harness (DSH)** that makes the model **prefer the local `codebuddy` CLI** (Tencent CodeBuddy Code) for real work (coding, builds, debugging, multi-file investigation) across **every mode**. DSH stays fully in control of codebuddy (`--permission-mode bypassPermissions`, so codebuddy never prompts). When codebuddy is **rate-limited or the network is down**, it pops a confirmation dialog asking whether to fall back to the **DSH local API config**, and it shows a **live status light** in the session header indicating whether codebuddy is currently working.

![codebuddy status light states](assets/indicator-states.svg)

---

## What it is

`codebuddy-first-bridge` contributes two things to a running DSH session:

1. **Three model tools** — `codebuddy_run`, `codebuddy_continue` and `codebuddy_status`, which hand tasks to the local `codebuddy` CLI.
2. **An codebuddy-first policy prompt section** — instructs the model to prefer codebuddy for real work in **all modes** (normal / plan / accept-edits / subagent / workflow / ralph / goal rounds); native tools are reserved for read-only lookups and final verification.

On top of that it implements the key capabilities that motivated the project:

- **Four backends:** `codebuddy` (domestic npm CLI, default), `codebuddy-intl` (the **international face of that same npm CLI** — `product.ioa.json` catalogue; aliases `codebuddy-ioa` / `codebuddy-international` / `codebuddy-oversea`), `codebuddy-en` (WorkBuddy international desktop CLI; needs a `codebuddyEnToken`) and `workbuddy` (WorkBuddy domestic desktop CLI, zero config). `codebuddy-intl` and `codebuddy-en` are two **distinct** options. Endpoints align automatically to each product's login domain. Sessions route back to their owning backend automatically; the plugin settings let the user pick the preferred CLI, a default model and the international token. Model candidates are read **at runtime** from each install's own product descriptor (v1.4.0+), falling back to a built-in measured list.
- **Fallback mechanism (confirmation dialog):** when codebuddy looks rate-limited or the network is unreachable, a dialog pops up offering *"use the DSH local API config (fall back)" / "retry codebuddy once" / "do not fall back"*.
- **Live status lights (per project, start-up persistent):** coloured indicators on the right of the browser session header — **one per project (working directory)** — reflecting each project's codebuddy activity in real time (working / ok / failed / fallback); hovering shows the step that project's codebuddy is currently executing. The light is a **home-level plugin** ([`home-plugin/codebuddy-indicator/`](home-plugin/codebuddy-indicator/), registered via `cordis.patch.yml`): it loads automatically with DSH, appears in **every session**, and needs **no approval**.
- **Live observation (`codebuddy_status`):** the `codebuddy_status` tool returns a snapshot of what codebuddy is doing RIGHT NOW, sectioned per project — each project's current step (tool name + arguments, or thinking/typing), recent step trail, last completed run. Optional `cwd` filters to one project. Callable mid-flight, no waiting.

## Four forms

The same logic ships in four forms; pick per need:

| Form | Location | Capabilities | Survives restart | Status light |
| --- | --- | --- | --- | --- |
| **Persistent agent preset** (recommended) | [`preset/codebuddy-first/`](preset/codebuddy-first/) | tools + policy + fallback dialog + `codebuddy_status` | ✅ yes (persisted preset) | ❌ no (host-plane composition has no browser UI) |
| **Home-level status-light plugin** (start-up persistent) | [`home-plugin/codebuddy-indicator/`](home-plugin/codebuddy-indicator/) | status light (every session, no approval) | ✅ yes (cordis.patch.yml) | ✅ yes |
| **Dynamic Cordis plugin** (in-session) | [`dynamic/`](dynamic/) | tools + policy + fallback dialog + **status light** + `codebuddy_status` | ❌ no (process-local) | ✅ yes (one-time approval) |
| **MCP server** (any MCP host) | [`mcp/`](mcp/) | `codebuddy_run` / `codebuddy_continue` / `codebuddy_status` auto-discovered via `tools/list` by Claude Code, Codex, Cherry Studio, … | ✅ yes (registered in client config) | ❌ no |

> **Why does the status light need a home-level plugin?** An agent preset is a **host-plane** composition (`agent.cordis.yml` mounts host plugins), and its `.mjs` runs only on the Node side, so it inherently has no browser UI. The live status light is a **client-plane** (browser Slot) component. A **home-level plugin** (registered via `cordis.patch.yml`, e.g. [`home-plugin/codebuddy-indicator/`](home-plugin/codebuddy-indicator/)) provides both a host half (collecting codebuddy status pushed by each session + an HTTP route) and a client half (the browser poll-and-render light), so it loads with DSH, appears in every session and needs no approval. The dynamic form (one-time GUI approval) and the home-level light coexist: both feed the home-level collector (the dynamic form via `codebuddyCollector.mergeSnapshot`, the preset form via `ctx.emit('codebuddy/status')` events).
>
> The fallback dialog is a host-side capability and is present in **both** the preset and dynamic forms. The MCP form has no UI; on rate-limit it appends a "don't loop-retry" note to the result text and lets the calling agent decide.

See [docs/en/ARCHITECTURE.md](docs/en/ARCHITECTURE.md).

## Quick start

### Option A: install as a persistent agent preset (recommended)

> **From v1.2.0 we recommend installing Option B alongside** (`dsh plugin --profile web add codebuddy-first-bridge`): Option A's registration channel is now carried by the status-light plugin — on new DSH (≥ 0.1.7-alpha.1) the preset is registered automatically into the `agentPresets` registry by the indicator (declarative presets), while old DSH (≤ 0.1.6-alpha.2) keeps using the directory-based install below. Either way, installing Option B alone makes the preset available out of the box.

Copy the whole `preset/codebuddy-first/` directory into your DSH user preset root (**only needed on dsh ≤ 0.1.6-alpha.2**; from 0.1.7-alpha.1 the `.agent-presets` directory is no longer read — the preset is declared by the Option B indicator plugin instead):

```
${DSH_HOME:-$HOME/.dsh}/.agent-presets/codebuddy-first/
```

Windows example (this repo's dev environment):

```powershell
Copy-Item -Recurse .\preset\codebuddy-first "$env:DSH_HOME\.agent-presets\codebuddy-first"
```

Then start a new DSH session and select the preset named **`CodeBuddy-First 执行代理`** (id: `codebuddy-first`). It inherits everything from the `standard` preset and adds the `codebuddy_run` / `codebuddy_continue` / `codebuddy_status` tools, the codebuddy-first policy, and the rate-limit/network fallback dialog.

> ⚠️ **Do not** edit the shipped `agent-presets` install that ships with the deployment (an upgrade overwrites it). Always install under your **user** preset root as a separate subdirectory.

Full steps and validation are in [docs/en/INSTALL.md](docs/en/INSTALL.md).

### Option B: install the home-level status-light plugin (start-up persistent, every session)

**Standard install (recommended, v1.1.7+)** — the home-level light is bundled into the main package `codebuddy-first-bridge` (its `dsh.bundle.patch` points at the indicator's bundle patch layer), so a single command installs it:

```powershell
dsh plugin --profile web add codebuddy-first-bridge
```

The DSH plugin system mounts the home-level light automatically after installing the main package: the host half resolves the bare package name to `main → lib/index.mjs`, and the client half is auto-registered in the browser roster via the `dsh.client` declaration — no manual copy, no junctions, no `cordis.patch.yml` edits.

**Legacy manual install (kept for reference)**:

```powershell
# 1) copy the plugin source
$dshHome = "$env:APPDATA\DSH Desktop\dsh-home"
Copy-Item -Recurse .\home-plugin\codebuddy-indicator "$dshHome\plugins\codebuddy-indicator"

# 2) create junctions (needed by both host resolution and the browser roster; 3 in total)
New-Item -ItemType Junction -Path "$dshHome\node_modules\codebuddy-indicator" -Target "$dshHome\plugins\codebuddy-indicator"
New-Item -ItemType Junction -Path "$dshHome\profiles\node_modules\codebuddy-indicator" -Target "$dshHome\plugins\codebuddy-indicator"
New-Item -ItemType Junction -Path "$dshHome\profiles\web\node_modules\codebuddy-indicator" -Target "$dshHome\plugins\codebuddy-indicator"

# 3) append one row to cordis.patch.yml (bare name resolves via junction to lib/index.mjs):
#    - insert:
#        - id: codebuddy-indicator
#          name: codebuddy-indicator
```

Pair it with the **preset form** (Option A): the preset's `codebuddy-first-bridge.mjs` emits `ctx.emit('codebuddy/status')` on every state change, which the home-level collector merges and the light renders. After editing `lib/index.mjs`, restart DSH (or temporarily switch to `name: codebuddy-indicator?v=N` to hot-reload); after editing `lib/client.js`, refresh the browser.

### Option C: run as a dynamic Cordis plugin (with the status light)

In a DSH session that has Cordis capabilities loaded, define and activate the plugin with `cordis_define` + `cordis_run`, using [`dynamic/host.js`](dynamic/host.js) for the host half and [`dynamic/client.js`](dynamic/client.js) for the client half. The first time the client half runs, the DSH GUI asks for a one-time approval; once granted, the status light appears in the session header.

## Requirements

- **DeepSeek Harness (DSH)** with the needed host services mounted: `tools`, `subprocess`, `systemPrompt`, `timer` (optional: `jobs`, `planMode`, `sandboxPolicy`, `userQuestions`).
- A local **`codebuddy` CLI** (CodeBuddy Code, `npm i -g @tencent-ai/codebuddy-code`; verified against v2.143.0 during development). **No PATH requirement**: the bridge resolves `subprocess.resolveExecutable('codebuddy')` → `node + CODEBUDDY_BIN` → `node + %APPDATA%\npm\node_modules\@tencent-ai\codebuddy-code\bin\codebuddy`, so a plain npm global install is found.
- Optional: the **WorkBuddy desktop app** (for `backend="workbuddy"` office dispatch; its CLI ships at `C:\Program Files\WorkBuddy\resources\app.asar.unpacked\cli\bin\codebuddy`, overridable via `WORKBUDDY_BIN`). Without it that backend returns an actionable error; the default codebuddy backend is unaffected.
- Optional: an **international account**. **The two international faces are two distinct options (v1.4.0+)**:
  - `backend="codebuddy-intl"` (panel name "**CodeBuddy 国际版（npm CLI · 国际面）**"; aliases `codebuddy-ioa` / `codebuddy-international` / `codebuddy-oversea`) — the **international face of the same npm CLI** as `codebuddy` (`product.ioa.json` catalogue: `claude-sonnet-5`, `claude-opus-5`, `gemini-3.1-pro`, `gpt-6-astra`, `hy3-ioa`, …). Note that the international models are **gated server-side per account**: measured on the real machine, `--model claude-sonnet-5` returns `400 model [...] is only available for authorized users` while `--model hy4-preview` (domestic face) returns `PONG`. A domestic account therefore gets a clear authorization error rather than a confusing configuration failure.
  - `backend="codebuddy-en"` (panel name "**WorkBuddy 国际版（WorkBuddyAI 桌面 CLI）**"; aliases `workbuddy-en` / `workbuddy-ai` accepted since v1.3.3). The international edition is a **separate install** — the CLI bundled with the WorkBuddy AI desktop app (`C:\Program Files\WorkBuddyAI\resources\app.asar.unpacked\cli\bin\codebuddy`, overridable via `CODEBUDDY_EN_BIN`; v1.3.0 wrongly claimed it reuses the same npm CLI, disproven by the real machine's `product.json`). **Credentials are reused automatically, so no configuration is normally needed**: the international edition's own login token is sealed by the desktop app's protector key, which is delivered only over the sidecar channel and is **never written to disk**, so the headless CLI cannot read it (the CLI itself reports `category:"missing-key"`). The bridge therefore tries, in order: the **`codebuddyEnToken` setting → the `CODEBUDDY_AUTH_TOKEN` env var → the DSH credential store** (the `WORKBUDDY_TOKEN` pointing at `workbuddy.ai/v2` in `dsh-home`'s `.credentials.yaml` / `.env` — i.e. the key you already configured for DSH's workbuddy provider). Verified on the real machine: as long as a workbuddy key is configured in DSH, `codebuddy-en` works out of the box; only when all three channels are empty does it return `AUTH_REQUIRED` with guidance instead of a silent 401. For office scenarios, `backend="workbuddy"` (domestic desktop CLI, also zero config) is equally fine.
- **Automatic endpoint alignment (v1.3.1):** before each call the bridge reads each product's `auth.domain` from its auth store (plaintext) and injects `CODEBUDDY_BASE_URL` — **only when the login domain differs from the product endpoint**. This fixes a pre-existing defect in the default `codebuddy` backend: the npm CLI's product endpoint is `www.codebuddy.ai` while the machine's login token domain is `www.codebuddy.cn`, so without the override it always 401s. No manual configuration is normally needed; use the `endpointOverride` setting to force one.
- The DSH Web GUI (client plane) is additionally required for the status light.

## Tool usage

`codebuddy_run(prompt, mode?, model?, effort?, maxTurns?, cwd?, addDirs?, timeoutSec?, background?, backend?)`

- `backend`: **four-backend dispatch**. `codebuddy` (default — domestic CodeBuddy npm CLI, product endpoint `www.codebuddy.ai`, coding scenarios), `codebuddy-intl` (**CodeBuddy international** — the international face of that same npm CLI, `product.ioa.json` catalogue; aliases `codebuddy-ioa` / `codebuddy-international` / `codebuddy-oversea`; the international models are gated server-side per account), `codebuddy-en` (WorkBuddy international desktop CLI, product endpoint `www.workbuddy.ai`; requires `codebuddyEnToken`), `workbuddy` (WorkBuddy domestic desktop CLI, product endpoint `copilot.tencent.com`, zero config; office face: documents/slides/spreadsheets, knowledge-base lookups, image/video generation, WeChat/WeCom replies). `codebuddy-intl` and `codebuddy-en` are **two distinct options**. Sessions are archived per backend (each product face's login is exclusive); `codebuddy_continue` routes back to the backend owning the sessionId automatically — an explicit `backend` always wins, and new calls without history follow the **user-preferred default backend** (plugin settings).
- `mode`: `auto` (default — follows DSH plan state, choosing `plan`/`accept-edits`), `plan`, `accept-edits`.
- `model`: optional model id; when omitted, the **user-preferred default model** (plugin settings, if set) applies, otherwise the CLI-configured default. **Every id carries its multiplier and any live promotion** (`hy3 - free - limited-time free`, `hy4-preview - x0.29 - free at night`), and the settings panel adds a five-row summary (free / priced / unrated / promotions / unauthorized) so cost is easy to judge.
  **Which layer the data comes from** (fixed in v1.6.1 - the real root cause of the repeatedly wrong model lists): model data has **three layers**, and only layer 1 was ever read before.
  | Layer | Location | Notes |
  |---|---|---|
  | 1. product-face defaults | install dir `product.${env}.json` / `product.json` | factory catalogue only, **carries no promotions** |
  | 2. account-level config | `cache/acc-product-config-v3.json` under `~/.workbuddy`, `~/.workbuddy-ai` | account-specific, **contains `modelPromotions`** |
  | 3. client live snapshot | `local_storage/entry_*.info` in the same folder | **exactly what the user menu renders** |

  Resolution falls back 2 then 3 then 1, **field by field** (multipliers and promotions may come from different files). Two traps: `entry_*.info` is a top-level `[{userId, data}, ...]` **array** (a single file can hold several accounts; the last entry is the newest) while `cache/` holds a bare object; and an international `cache/` file can be **badly stale** (37 entries, no promotions, and neither `hy4-preview` nor `deepseek-v4.1-flash` here) yet sit first in the chain, shadowing the newer `local_storage` snapshot - so the chain now takes the first file that **has content for each field**, not the first readable file.
  **Promotions (`modelPromotions`) are time-gated**: "free at night" applies 23:00-08:00 with `factor=0` (genuinely free in that window) and only shows a badge during the day, while "limited-time free" has no window. The panel and dropdown show only **currently active** promotions, matching the CLI menu. Promotion prices are written `0.00x` (digits first), the **reverse** of the `credits` field's `x0.29 credits`, and both forms are accepted.
  **The descriptor is a catalogue, not a usable list**: measured on the real machine, the `-ioa` ids (`glm-5.3-ioa`, `hy3-ioa`, `deepseek-v4-pro-ioa`) are all rejected for this account with `400 ... is only available for authorized users`, while the unsuffixed `glm-5.3` and `hy3` work - the two are different id sets.
  **To get your account exact live list**, pass a deliberately invalid model id and the server replies with `Currently supported models for your account:` followed by one `  - <id>` per line (`parseAccountModels()` in core parses it). Since v1.6.0 this parsing runs on the real execution path (`buildResult` into the `rememberAccountCatalog()` cache) at **zero extra cost**, and the panel splits ids into authorized (usable) and unauthorized (a 400 if selected), listing only the former in the dropdown.
  **Multipliers match across naming schemes**: `lookupModelCredits()` lets the account id `glm-5.3` pick up the `x0.79` recorded for `glm-5.3-ioa` in the descriptor, with an alias table on top (`kimi-k3-1` to `kimi-k3`, `hy3-x` to `hy3`, `default` to `codewise-default-model-v2`).
  The two 400s mean different things: `model [...] service info not found` = the id is not in this account list; `model [...] is only available for authorized users` = it is in the catalogue but this account is not licensed (`claude-opus-4.8`, `gpt-5.4` and `gpt-6-astra` are all in this group - **they appear in the CLI menu but fail the moment you select them**).
  Measured here (2026-09-25, all four backends):
  - `workbuddy` (domestic, 53 entries): `hy3 - free - limited-time free`, `hy4-preview - x0.29 - free at night` (free during the night window), `hy3-b`/`hy3-c - limited-time free`, `deepseek-v4-flash - x0.17 - night discount`, `deepseek-v4-pro - x0.51 - night discount`, `glm-5.2 - x0.79 - night discount`.
  - `codebuddy-en` (international, 26 entries): `hy3`, `hy4-preview-f` and `deepseek-v4.1-flash` are all **`free - Free now`**.
  - `codebuddy` / `codebuddy-intl` (npm CLI, 53 entries): no account-level config on this machine, so they resolve at the descriptor layer with no promotions.
  **Note**: since v1.6.0 the npm-face static-table union is gone (reversing the v1.4.1/v1.4.2 design): the static table both missed ids present in the real CLI menu and kept retired ones, so it now serves only as a fallback when no file can be read.
- `effort`: `minimal / low / medium / high / xhigh / max`.
- `background: true`: run as a background job and return a `jobId`; collect via `job_output`.
- Returns: `{ ok, status, response, sessionId, durationSeconds, numTurns, totalTokens, exitCode, mode, backend, stderr }`; on fallback it is `{ ok:false, fallback:true, status:'FALLBACK_TO_DSH', ... }`.

`codebuddy_continue(prompt, sessionId? | latest?, ...)` — continue an existing conversation (`--resume <sessionId>` / `--continue`); other parameters as above; without an explicit `backend` it routes to the CLI owning the sessionId.

`codebuddy_status(cwd?)` — **live observation + usage accounting**: returns a snapshot of what codebuddy is doing right now (`{ state, running, current, trail, lastStatus, lastSessionId, runs, totalTokens, updatedAt, projects[] }`). `projects[]` is sectioned per project (working directory): `current` is the step that project is executing right now (tool name + arguments, or thinking/typing); `trail` is its recent step history; `runs`/`totalTokens` are the **project-cumulative call count and token usage** (codebuddy has no plan/quota API, so token metering is the available substitute). Optional `cwd` filters to one project. Callable while `codebuddy_run`/`codebuddy_continue` is in flight, no waiting.

## DSH fully controls codebuddy

Every codebuddy call is forced with `-p`, `--output-format stream-json` and `--permission-mode bypassPermissions` (`--permission-mode plan` in plan mode), so **codebuddy never prompts and never asks before editing files**; mode, model, effort, working directory, timeout, background, and cancellation are all decided by DSH, and a run can be cancelled through `exec.signal` + `handle.terminate()`. codebuddy has no `--print-timeout`; a DSH-side hang guard force-terminates at `timeoutSec+60s` (`HUNG_TIMEOUT`). Each stream-json event (assistant `tool_use`/`thinking`/`text`, user `tool_result`) feeds the `codebuddy_status` snapshot in real time.

## Fallback & status light

See [docs/en/FALLBACK-AND-INDICATOR.md](docs/en/FALLBACK-AND-INDICATOR.md). Highlights:

- Failure detection: non-zero exit, or `stderr/status` matching `rate limit / 429 / quota / ECONN* / network / timeout / …` (plus Chinese equivalents) — **stderr and status only, never the answer text** (investigation answers almost always mention connection/dns/timeout; numeric codes are word-bounded so "1500" does not match 500).
- The dialog uses DSH's `userQuestions.ask()`; when there is no live human answerer (e.g. a delegated subagent) the dialog is skipped and an error is returned, avoiding a permanent block.
- Each rate-limit/network failure opens the 3-way dialog once; "Retry" is offered only while retries remain (at most 2 attempts); background failures do not open the dialog (re-run in the foreground to be prompted).
- The status light polls the home-level HTTP route `GET /codebuddy-indicator/status` every 1.2s; colours come from theme tokens and adapt to light/dark.

## Visual configuration panel (v1.3.2)

A new **CodeBuddy Bridge** section appears in DSH Settings (registered into the official `settings.section` list slot by the browser half — same mechanism as dsh-dream-skin / dsh-mobile-companion). Four preferences, one panel:

| Field | Key | Notes |
|---|---|---|
| Preferred CLI | `preferredBackend` | Dropdown: `codebuddy` / `codebuddy-intl` / `codebuddy-en` / `workbuddy`. Only affects **default dispatch**; an explicit `backend` argument in a session still wins |
| Default model | `defaultModel` | Free text with per-backend datalist suggestions (verified model lists). Empty = each CLI's own default |
| Endpoint override | `endpointOverride` | Leave empty in normal use (endpoints auto-align to the login domain). Debug only |
| International token | `codebuddyEnToken` | Password field; the page only ever shows a masked tail (last 4 chars), plaintext is never sent back to the browser. Saving empty = keep current value; a "Clear" checkbox removes it |

Below the form, a **per-backend diagnostics table** shows the effective endpoint (and its source: override / auth-domain / product), the auth library login domain, the credential source (panel / env var / auto-reuse of DSH's credential store), and actionable warnings when a 401 is likely.

**Saving takes effect for all three forms immediately, no restart**: the panel POSTs to `/codebuddy-indicator/settings`, which atomically writes `<dsh-home>/codebuddy-bridge-settings.json` (single source of truth). The preset bridge, the dynamic plugin and the MCP subprocess **re-read that file on every call** (MCP with a 5s cache). Precedence: settings file > preset row config > defaults.

> Why a self-drawn panel instead of the official auto-form: the `Config` projection only covers entries whose fiber is an `include`; preset bridges mount through `PresetTree` and never qualify (verified line-by-line in `dsh-config-editor`). The self-drawn section + own read/write route is the only path where one save reaches all three forms — and it can show live diagnostics on top.

## Repository layout

```
codebuddy-first-bridge/
├─ README.md / README.en.md
├─ LICENSE
├─ .gitignore
├─ package.json                    # version metadata (v1.1.0, Node >=18) + scripts (build/test/check)
├─ MCP-POLICY.md                   # disclose-and-prefer policy for external agents (also ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md)
├─ .github/workflows/ci.yml       # node --check + test suite + version/YAML structural validation (Node 18/20/22)
├─ assets/indicator-states.svg    # status-light states diagram
├─ core/
│  └─ codebuddy-core.mjs           # ★ shared core (single source of truth): pure functions + status engine
│                                  #   + line stream + run orchestration + shared copy
├─ scripts/
│  ├─ build.mjs                    # generates derived artifacts (dynamic/host.js text injection + preset core copy)
│  └─ verify.mjs                   # version quad-lock (package.json ↔ indicator package.json ↔ MCP VERSION ↔ CHANGELOG) + YAML/structure assertions
├─ test/                           # node:test suite (125 cases: pure functions / sandbox sim / preset / MCP e2e / settings UI / sync lock)
│  ├─ helpers/mockdsh.mjs          #   DSH host-shape stand-ins (ctx/harness/subprocess/userQuestions)
│  ├─ fixtures/fake-codebuddy.mjs  #   fake codebuddy CLI (MCP e2e fixture)
│  └─ *.test.mjs
├─ preset/
│  └─ codebuddy-first/                   # persistent agent preset (recommended)
│     ├─ preset.yml
│     ├─ agent.cordis.yml          #   standard + one codebuddy plugin row
│     ├─ codebuddy-first-bridge.mjs      #   host adapter (tool registration / event publish / env resolution)
│     └─ codebuddy-core.mjs        #   [generated] core copy — the preset install dir is self-contained
├─ home-plugin/
│  └─ codebuddy-indicator/               # home-level status-light plugin (start-up persistent)
│     ├─ package.json              #   dsh.client declaration (browser roster)
│     └─ lib/
│        ├─ index.mjs              #   host half: collects codebuddy/status events + HTTP route
│        ├─ client-entry.mjs       #   bare-name placeholder entry (prevents double-loading index.mjs)
│        └─ client.js              #   browser half: poll + render per-project light
├─ dynamic/                        # dynamic Cordis plugin form (with status light)
│  ├─ host.template.mjs            #   adapter template (with the /*__CORE__*/ injection point)
│  ├─ host.js                      #   [generated] code.host body (core text injected — do not edit by hand)
│  └─ client.js                    #   code.client body (empty skeleton: UI unified in the home-level light)
├─ mcp/                            # MCP server (discoverable by any MCP host)
│  ├─ codebuddy-mcp-server.mjs           #   zero-dependency stdio MCP server (host adapter layer)
│  └─ README.md                    #   registration (Claude Code / Codex / DSH / generic) + security guardrail
└─ docs/
   ├─ INSTALL.md / ARCHITECTURE.md / FALLBACK-AND-INDICATOR.md / CHANGELOG.md   (中文)
   └─ en/INSTALL.md / ARCHITECTURE.md / FALLBACK-AND-INDICATOR.md               (English)
```

> **Derived-artifact convention**: `dynamic/host.js` and `preset/codebuddy-first/codebuddy-core.mjs` are generated. Change shared logic in `core/`, dynamic adapters in `host.template.mjs`, then run `npm run build` (the sync lock inside `npm test` fails the commit if you forget).

## Compatibility & support statement

Regression-tested against **all 26** published `@deepseek-ai/dsh` versions (0.0.1-rc.1 → 0.1.7-rc.1); the full matrix and evidence live in [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md). Highlights:

- **Preset form (Option A)**: **works on every version (fixed in v1.2.0)** — dsh ≤ 0.1.6-alpha.2 uses the directory-based preset (`dsh ≥ 0.1.3-alpha.2` enforces the persona `prefix:`/`suffix:` schema); from dsh ≥ 0.1.7-alpha.1 the `.agent-presets` directory is no longer read (declarative presets), and the preset is registered into the `agentPresets` registry by the status-light plugin (Option B). **On new DSH, install Option A together with Option B.**
- **Home-level light via bundle install (Option B)**: `dsh plugin --profile web add codebuddy-first-bridge` works on **all 26 versions** (the plugin CLI / pnpm forwarder / `dsh.bundle.patch` / bundles reconciliation exist since 0.0.1-rc.1). Requires `codebuddy-first-bridge ≥ 1.1.8` (1.1.7's client registration-id mismatch trips a startup fatal screen on **every** dsh version).
- **MCP server (Option D)**: zero-dependency standalone process, independent of the dsh version.
- When a new dsh version ships, re-run the regression via `npm run compat:dsh` (see [docs/RELEASE-SOP.md](docs/RELEASE-SOP.md) §3).

## Versions & releases

Semantic versioning via `package.json` + Git tags + GitHub Releases; per-version npm↔git content audit via `npm run audit:npm` (also wired into CI; see [docs/CHANGELOG.md](docs/CHANGELOG.md)):

| Version | DSH compat | Highlights |
| --- | --- | --- |
| [v1.6.2](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.6.2) | dsh 0.1.7-rc.1 verified / all | **The default-model picker collapsed to a single entry, making the model list look lost.** On your report that only `deepseek-v4.1-flash` showed up, we confirmed the **data was complete** (the live route returns 53 / 53 / 26 / 53 options for `codebuddy` / `codebuddy-intl` / `codebuddy-en` / `workbuddy`, nothing missing) - the fault was in **how it rendered**: the field was an `<input list>` + `<datalist>`, a free-text box whose dropdown the browser **prefix-filters by the current text**. Your `defaultModel` was `deepseek-v4.1-flash`, which also happened to sort first (it carries the Free now promotion at an effective x0.00, so it floats to the top of the free group) - so opening the dropdown left exactly that one entry. Replaced with an explicit **`<select>`** listing every usable id, no longer affected by the typed text, plus a **custom...** entry that reveals a manual input so arbitrary ids remain possible. The note now shows the **actual candidate count** (e.g. "26 in total") so an empty list is obvious at a glance. Also fixed a style-concatenation glitch where `.cbs-rate-promo` and `.cbs-rate-ids` had been merged onto one line, whose missing trailing comma broke CSS parsing and disabled the orange promotion tag. |
| [v1.6.1](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.6.1) | dsh 0.1.7-rc.1 verified / all | **The data-source layer was still wrong - v1.6.0 fixed *which file* but not *which layer*.** On your fourth report ("WorkBuddy international frees are Hy4 preview / Hy3 / Deepseek-V4.1-Flash, all Free now 0.00x; domestic frees are Hy4 preview (free at night, 0.29x) and Hy3 (limited-time free, 0.00x)") we verified the caches and **you were right - v1.6.0 read the wrong layer from the start**. The real three-layer structure: (1) **install-dir `product.json` / `product.ioa.json`** = product-face **factory defaults** (the only layer v1.6.0 read; it has **no `modelPromotions` field at all**, so no amount of reading it could ever surface the menu's Free now / free-at-night / limited-time-free badges); (2) **`cache/acc-product-config-v3.json` under `~/.workbuddy` and `~/.workbuddy-ai`** = account-level config, **which does contain `modelPromotions`**; (3) **`local_storage/entry_*.info` in the same folder** = the desktop client live snapshot, **exactly what the user menu renders**. Resolution now falls back 2 then 3 then 1. Three implementation traps: (a) `entry_*.info` is a top-level `[{userId, data}, ...]` **array** (the international file here holds two accounts, 527fed08 and dc632238) and the last entry must win, while `cache/` holds a bare object - new `normalizeProductConfig()` accepts both; (b) promotion prices are written `0.00x` (digits first), the **exact reverse** of the `credits` field's `x0.29 credits`, so `parseCreditValue()` now also accepts `"0x"` / `"0.00x"` or the Free now price reads as null; (c) the international `cache/` file **was badly stale** (01:33 here: 37 entries, **no promotions, and neither hy4-preview nor deepseek-v4.1-flash**) yet sat first in the chain, shadowing the newer `local_storage` snapshot (26 entries) - so the chain now resolves **field by field** (multipliers and promotions may come from different files) and prefers the file **that has promotions** over merely the first readable one. New `extractModelPromotions()` / `isPromotionActive()` / `activePromotionFor()` parse promotions and test **validity dates plus wraparound time windows** (free-at-night runs 23:00-08:00: a badge only by day, `factor=0` genuinely free at night; limited-time-free has no window). The panel adds a **promotions ×N** row with labels like `hy4-preview · x0.29 · 夜间免费`, and the original price is passed through as `baseCredits`. **Measured across all four backends**: `workbuddy` returns 53 entries including `hy3 · 免费 · 限时免费` and `hy4-preview · x0.29 · 夜间免费`; `codebuddy-en` returns 26 with `hy3` / `hy4-preview-f` / `deepseek-v4.1-flash` **all `免费 · Free now`** - **matching your menu line for line**. Tests 148→155 |
| [v1.6.0](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.6.0) | dsh 0.1.7-rc.1 verified / all | **Fixed two bugs at once: the wrong descriptor file was read, and unusable ids were mixed in.** After the third report of wrong model lists, reverse-engineering `npm\dist\codebuddy.js` found the root cause. (1) **Wrong file**: the CLI loads **`product.${env}.json`** (`ClientInternetEnviromentProductProvider` builds `` `product.${em}.json` ``; `env` comes from `CODEBUDDY_INTERNET_ENVIRONMENT` / settings `env` / the account `productConfigEnv`, resolving to `ioa` here) and only falls back to `product.json` when that read fails; all four backends previously read `product.json` only, so every list came from the wrong file. Now `product.ioa.json` is preferred with `product.json` as fallback (WorkBuddyAI ships only the latter, so the chain is load-bearing there): `codebuddy` 30 to 67, `codebuddy-intl` 36 to 53, `codebuddy-en` 37 (fallback), `workbuddy` 48 to 93, and `claude-opus-4.8` / `gpt-5.4` appear correctly for the first time. (2) **The descriptor is a catalogue, not a usable list**: measured on the real machine, the `-ioa` ids (`glm-5.3-ioa`, `hy3-ioa`, `deepseek-v4-pro-ioa`) are all `400 only available for authorized users` for this account, while the unsuffixed `glm-5.3` / `hy3` work, so the two are different id sets. `parseAccountModels()` (written back in v1.4.1 but never wired up) is now called on the real execution path: any 400 is parsed for `Currently supported models for your account:` and cached via `rememberAccountCatalog()`, giving the authoritative list at zero extra cost. `modelCatalogDetailed()` merges both sources, marking account ids `authorized:true` first and catalogue-only ids `authorized:false` with 未授权 in the label. (3) New `lookupModelCredits()` matches multipliers across `-ioa` naming and an alias table (`glm-5.3` to `glm-5.3-ioa`, `kimi-k3-1` to `kimi-k3`, `hy3-x` to `hy3`, `default` to `codewise-default-model-v2`). (4) **Removed the npm-face static-table union** (reversing v1.4.1/v1.4.2): the static table both missed ids present in the real CLI menu and kept retired ones, so it is now only a fallback when no file can be read. (5) The panel dropdown lists only usable ids and the rate block gained an 未授权 count row. (6) Self-review caught that `backendSettingsMeta()` did not forward `authorized` in `modelOptions`, which would have made the panel render no models at all - fixed with a regression test. Measured here: three backends resolve the same **16 usable models with multipliers on all 16** (`glm-5.2` x0.79, `kimi-k2.6` x0.52, `minimax-m2.7` x0.19, `hy4-preview` free), matching the reported menu line by line. Tests 144 to 148 |
| [v1.5.0](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.5.0) | dsh 0.1.7-rc.1 verified on the real machine / all versions | **Every model now shows its credit multiplier so cost is judgeable at a glance.** The CLI `--help` only prints ids and has no rate subcommand; the multiplier lives in each install's **top-level `models[].credits`** of `product.json` — the very `0.00x` the UI shows (`agents.cli.models` is a plain string array and carries no rate). ① New `extractModelCredits` / `parseCreditValue`: only `x<number>` is accepted, and **`0` (free) is kept strictly distinct from `null` (unrated)** — a missing rate is never guessed as free. ② New `modelCatalogDetailed()` returning `{id,credits,free,label}` with labels like `hy3 · 免费` / `glm-5.3 · x0.79`; **rated models sort first and ascending**, so free ones float to the top. ③ The panel dropdown uses those labels and adds a **rate overview** (free ×N / paid / unrated). ④ **Sync model: read the product descriptor at runtime** — upgrading the CLI and restarting is enough, no plugin change needed. ⑤ Cost guidance added to the policy and all six schema descriptions. Also added a **quote state-machine guard** against the bare-apostrophe-in-a-single-quoted-string trap that bit us once each in v1.4.1, v1.4.2 and v1.5.0. Tests 136→144 |
| [v1.4.2](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.4.2) | dsh 0.1.7-rc.1 verified on the real machine / all versions | **Which product face carries which models is now pinned down.** You reported that the WorkBuddy international UI lists `Hy4 preview` / `Hy3` / `Deepseek-V4.1-Flash` as **Free now (0.00x)** yet they were missing from the dropdown. Reading the `credits` field of each `product.json`: `hy3` and `deepseek-v4.1-flash` are `x0.00` and `hy4-preview` is `x0.29` — all three belong to the **npm CLI face** (`codebuddy`/`codebuddy-intl`). **WorkBuddyAI does not carry `hy4-preview` or `deepseek-v4.1-flash` at all**; `hy3` is its only free model. ① New `BACKEND_STRICT_CATALOG`: the two desktop backends (`codebuddy-en`/`workbuddy`) are now bounded **strictly by their own install's catalogue** with no static-table union — v1.4.1's union leaked npm-face ids into the desktop backends, where selecting them could only fail; the npm faces keep the union. ② The `codebuddy-en` static fallback was rewritten **free-first** by `credits` (leading with `hy3`) and had the two absent ids removed. ③ Policy now reads "identify the product face first, then the account authorization". Tests 134→136 |
| [v1.4.1](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.4.1) | dsh 0.1.7-rc.1 verified on the real machine / all versions | **The authoritative model list is per ACCOUNT, not per install.** You flagged that "the codebuddy vs workbuddy models look wrong" — **they were, and v1.4.0's whole approach was wrong**. ① v1.4.0 treated each install's `product.json` as authoritative; probing those ids against the CLIs one by one showed many do not exist at all — `default-model` / `primary-model` / `gpt-6-astra` all return `400 model [...] service info not found`. It is a **release-builtin candidate table**, not the **account's available set** — and `--help`'s static list is byte-identical across all three installs, proving availability is account-driven, not install-driven. ② The real authority: pass a non-existent model and the server answers with `Currently supported models for your account:` plus one `- <id>` per line, live per account. New `parseAccountModels()` parses it. ③ **Priority inverted**: `backendModelCatalog()` now puts the static (account-measured) table first and only unions extra ids from the product file — v1.4.0 had it backwards. ④ Fixed `workbuddy` reading the wrong file: it used `product.cloudhosted.json` (both `endpoint` and `auth.id` **empty** — just a variant fragment); now correctly the install's real face `product.json` (`applicationName=WorkBuddy`, `auth.id=workbuddy-desktop`). ⑤ Policy model paragraph rewritten, spelling out the two distinct 400s. Tests 132→134 |
| [v1.4.0](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.4.0) | dsh 0.1.7-rc.1 verified on the real machine / all versions | **Model catalogues read at runtime + CodeBuddy international as its own option**. A user report that "the model lists are wrong for all four product faces" proved correct on inspection: the old `codebuddy-en`/`workbuddy` lists had been copied from the **domestic `product.internal.json`** (including model ids absent on this machine). ① New `readBackendModelCatalog()`/`extractModelIds()` read each install's own product descriptor **at runtime** (whichever of `agents.cli.models` vs the top-level `models` is richer — WorkBuddyAI's `agents.cli.models` holds only 4 role aliases, while its 37 real ids live in the top-level `models`); when unreadable it falls back to the built-in measured list. ② **New `codebuddy-intl` backend (CodeBuddy international)**, split from "WorkBuddy international" (`codebuddy-en`) into two distinct panel options; canonical ids are only added, never changed. ③ **Measured finding**: international models are gated server-side per account (`claude-sonnet-5` → 400, `hy4-preview` → fine). ④ Policy, six schema sites and the panel hint updated. ⚠ Approach ① was overturned by v1.4.1. Tests 128→132 |
| [v1.3.3](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.3.3) | dsh 0.1.7-rc.1 verified on the real machine / all versions | **WorkBuddy international edition renamed + backend aliases**. Confirmed `C:\Program Files\WorkBuddyAI` (WorkBuddy international) has been wired up since v1.3.1 = the `codebuddy-en` backend; the old "CodeBuddy international" label made it look missing. Display name is now "WorkBuddy 国际版（WorkBuddyAI 桌面 CLI）"; `backend="workbuddy-en"` / `"workbuddy-ai"` normalize to the canonical id `codebuddy-en` (normalizeBackend + BACKEND_ALIASES across all three routing channels and settings-file cleaning; historical session archives unaffected); policy text sheds a stale v1.3.0-era claim. Tests 125→128 |
| [v1.3.2](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.3.2) | dsh 0.1.7-rc.1 verified on the real machine / all versions | **Visual configuration panel**. New "CodeBuddy Bridge" section in DSH Settings (self-drawn `settings.section` — the official Config auto-form is unreachable for preset-mounted bridges): preferred CLI / default model / endpoint override / international token (masked, plaintext never returned to the browser) + per-backend diagnostics (login domain → effective endpoint → credential source → 401 warning). Single source of truth `<dsh-home>/codebuddy-bridge-settings.json` (core primitives + indicator GET/POST routes); the preset / dynamic / MCP forms **re-read it on every call → saving takes effect without a restart**; deploy.mjs now also syncs the dsh-home core+bin copies. +14 tests (125 total); verify gains version-lockstep and structure gates |
| [v1.3.1](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.3.1) | dsh 0.1.7-rc.1 verified on the real machine / all versions (settings-compat matrix in [COMPATIBILITY](docs/COMPATIBILITY.md)) | **Endpoint & credential root-cause fix** (overturns v1.3.0's international-edition conclusion). ① **The three backends are three separate installs** (verified from each real `product.json`): `codebuddy` = npm `@tencent-ai/codebuddy-code` (CodeBuddy, endpoint `www.codebuddy.ai`), `codebuddy-en` = `C:\Program Files\WorkBuddyAI\...` (WorkBuddy AI, `www.workbuddy.ai`), `workbuddy` = `C:\Program Files\WorkBuddy\...` (WorkBuddy, `copilot.tencent.com`). ② **Fixed a pre-existing 401 in the default `codebuddy` backend**: the npm CLI's product endpoint is `www.codebuddy.ai` while the login token domain is `www.codebuddy.cn`, so the mismatch always 401s (the root cause of 312 × `www.codebuddy.ai` and 116 × `Authentication required` in the user's logs); verified on the real machine that injecting `CODEBUDDY_BASE_URL=https://www.codebuddy.cn/v2` makes both `codebuddy` and `workbuddy` return PONG. ③ **Automatic endpoint alignment**: new `resolveEndpoint()`/`endpointEnv()`/`endpointMismatchHint()` plus `BACKEND_ENDPOINTS`/`AUTH_DOMAIN_ENDPOINTS`/`BACKEND_AUTH_IDS` (replacing `intlEndpointEnv`); the host reads the **plaintext** `auth.domain` from the auth store and injects **only when it differs from the product endpoint**; the semantically wrong `CODEBUDDY_INTERNET_ENVIROMENT=cloudhosted` is removed (`www.workbuddy.ai` is an `externalDomain`). ④ **`codebuddy-en` credential tri-channel (works out of the box)**: the international token is sealed by a protector key that is never written to disk (51612-file full-disk scan + all DPAPI blob unwrap attempts found zero hits; the CLI itself reports `category:"missing-key"` ×21) — but the user has usually already configured the same token in DSH (the provider pointing at `workbuddy.ai/v2`, `WORKBUDDY_TOKEN`), and on the real machine handing that token to the WorkBuddyAI CLI returns PONG. New `resolveEnToken()` therefore tries the `codebuddyEnToken` setting → the `CODEBUDDY_AUTH_TOKEN` env var → the **DSH credential store**, so `codebuddy-en` needs no manual setup; only when all three are empty does it return `AUTH_REQUIRED` with actionable guidance instead of a silent 401. `codebuddyEnBaseUrl` is removed (its default was itself wrong) in favour of `endpointOverride`. Tests 100→111, all green |
| [v1.3.0](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.3.0) | dsh 0.1.7-rc.1 verified on the real machine / all versions (settings-compat matrix in [COMPATIBILITY](docs/COMPATIBILITY.md)) | **CodeBuddy international edition + plugin settings**. ① Tri-backend: new `codebuddy-en` (international CodeBuddy) — ~~the international edition has no separate npm package, it is the same `@tencent-ai/codebuddy-code` CLI with an endpoint switch~~ (**overturned by v1.3.1 on the real machine**, see above); the endpoint variables `CODEBUDDY_BASE_URL=https://www.workbuddy.ai/v2` and `CODEBUDDY_INTERNET_ENVIROMENT=cloudhosted` (**both removed**: the former's default was wrong for the default backend, the latter was semantically wrong). Backend routing unified into `resolveBackend()` (explicit > session ownership > user preference > default), tool schemas (six sites across three forms) and the policy text extended to three backends, model lists split per product face (both lists verified from the two installed CLIs' `--help`). ② Plugin settings (settings panel): the bridge row exports a duck `Config` schema (the preset sandbox cannot import schemastery; `toJSON()` emits official refs JSON rebuildable by the official `z()`, SettingsForms projection contract verified function-by-function), volatile fields with hot edit; old DSH (≤0.1.6) gets a `ctx.settings` provider duck-probe + watch hot-sync, silently degrading to row config when the service is absent; MCP/dynamic forms share `<dsh-home>/codebuddy-bridge-settings.json`. Tests 85→100 |
| [v1.2.0](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.2.0) | dsh 0.1.7-rc.1 verified on the real machine / all versions (26-version matrix) | **Form-A fix: presets silently dead on new DSH (≥ 0.1.7-alpha.1)** — dsh 0.1.7-alpha.1 switched presets from directory-based (`.agent-presets/`) to **declarative** (runtime registration into the `agentPresets` registry); the old directory is never read, so Option A died silently with no error. Fix = **indicator self-registration**: the status-light plugin's `apply()` does `ctx.inject(['agentPresets'])` and calls `register(definition)` once the registry appears (a faithful port of the official standard preset + the bridge row; on old DSH the injector stays pending with zero side effects and no new loader rows — the old loader treats import failures as fatal). The bridge row uses the bare-specifier subpath `codebuddy-first-bridge/preset-bridge` (the registry resolves relative names against its own baseUrl, so relative paths can never work). Verified on the real machine: local tarball through a real `dsh plugin add` install + boot, roster probe confirms `codebuddy-first` listed alongside the four official presets with no `broken` field. Matrix 22 → 26 versions (new C8 declarative-preset contract probe + 0.1.5-rc.3 / 0.1.7-alpha.1/.2 / 0.1.7-rc.1) |
| [v1.1.12](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.12) | see support statement (22-version regression) | **Engineering patch: fix the dsh all-version regression baseline + extend the matrix to 22 versions** (no plugin runtime change) — ① drop `--legacy-peer-deps` (it skips `dsh-app-boot`'s runtime-required peerDependencies, leaving a sandbox that cannot boot); ② add `--before=<publish-time>` date anchoring (prevents internal `^0.1.x-rc.y` caret ranges from pulling `0.1.6-alpha.x` into historical trees to form a mixed tree); ③ `PROBE_VER` aligned to 1.1.11. `docs/COMPATIBILITY.md` gains `0.1.6-alpha.1/.2` rows and a re-run matrix |
| [v1.1.11](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.11) | see support statement (all-version regression) | **Engineering patch: observation accuracy in the two release/regression tools** (no plugin runtime change) — ① `audit-npm-sync.mjs` now passes `--prefer-online` when listing versions: `npm view <pkg> versions` hits npm's local metadata cache, so auditing right after `npm publish` still showed only the older version list (observed; same for CI on a freshly pushed tag); ② `dsh-compat.mjs` reports **functional failures** as their own bucket: `functional.ok === false` rows used to be counted as `untested`, conflating a real failure with "never ran" (7 "CLI environment incompatible" rows out of 20 were reported inaccurately). Now `untested = total − pass − failed − skip − pending`, aligned with the COMPATIBILITY §3.1 buckets |
| [v1.1.10](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.10) | see support statement (all-version regression) | **Engineering: sync audit + all-version regression + support statement + handover docs**: adds `audit-npm-sync.mjs` (per-published-version npm tarball ↔ git tag tree file-by-file sha256, comparing raw commit bytes via `git -c core.autocrlf=false archive`; verdict: 1.1.6–1.1.9 have zero content drift) and `dsh-compat.mjs` (7 contract probes over all 20 dsh versions + sandboxed real install tests); new docs `COMPATIBILITY.md` / `RELEASE-SOP.md` / `HANDOVER.md`; CI gains an npm↔git audit job; CRLF normalization + `.gitattributes` (`* text=auto eol=lf`) so tarballs and tags are byte-identical from 1.1.10 on |
| [v1.1.9](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.9) | dsh 0.1.5-rc.1 verified / 0.1.2-alpha.4+ | **Release-gate hardening (prevents the v1.1.8 incident class)**: `verify.mjs` statically checks the client registration id — it resolves the client file from `exports["./client"]`, extracts `__ModuleLoader__.load({ id })`, and **requires it to equal the package name `codebuddy-first-bridge`** (a mismatch trips client-modules' `loaded without registering "<packageName>"` and crashes the whole combo). `prepack` is strengthened to `verify + npm test`, running the full test suite before publishing, so test-version drift (as happened during the hotfix) can no longer reach npm |
| [v1.1.8](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.8) | dsh 0.1.5-rc.1 verified / 0.1.2-alpha.4+ | **Emergency fix for a DSH startup fatal screen**: after v1.1.7 merged the indicator into the main package, client.js still registered `__ModuleLoader__.load` with the old standalone id `codebuddy-indicator`, which no longer matched the graph row id derived from the package name → client-modules rejected the combo (`Failed to load plugins`, desktop shell entered safe mode). The id is now `codebuddy-first-bridge` |
| [v1.1.7](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.7) | dsh 0.1.3-alpha.2+ / Desktop 0.3.4+ | **The home-level status light now ships in the standard npm distribution shape** (aligned with agy-first-bridge v1.6.0): the main package's `main` points straight at `home-plugin/codebuddy-indicator/lib/index.mjs` (the real host entry), `dsh.client.platform: web` auto-registers the client half in the browser roster, `dsh.bundle.patch` auto-mounts the indicator bundle layer on install, and `bin.codebuddy-mcp-server` is declared. A new bundle patch layer loads the indicator via a single bare-package-name row — no more `file://` row, no more client-entry placeholder, single instance, no double-registration crash. The local user-layer `cordis.patch.yml` was switched from `file://...?v=6` to the bare package name. Cross-device install: `dsh plugin --profile web add codebuddy-first-bridge` |
| [v1.1.6](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.6) | dsh 0.1.3-alpha.2+ (`@deepseek-ai/dsh-persona` 0.1.3-alpha.2+) | **Adapts to the dsh-persona config-schema upgrade**: after the backend auto-updated to 0.1.3-alpha.2, the official `@deepseek-ai/dsh-persona` enforced its Schemastery schema from the legacy `text:` field to `prefix: z.string().required()` + `suffix: z.string().default("")`, and the validator rejected the old shape at mount (resuming a session threw `invalid config: - $.prefix missing required value`). The persona row now uses the `prefix:` + `suffix:` split exactly like the official standard preset; `verify.mjs` gained regression guards (no legacy `text:` allowed, `prefix:`/`suffix:` required); and the package is now **npm-publishable** (dropped `private`, `files` whitelist adds README/LICENSE, `prepack` runs full validation before publishing) — `npm install -g codebuddy-first-bridge` works |
| [v1.1.5](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.5) | Desktop 0.3.4+ (verified 0.3.14) / dsh 0.1.2-alpha.4+ | **Fixes the status light being blind to standard mode**: standard (non-codebuddy-first) sessions dispatch through the **global MCP row**, and the MCP server is a separate subprocess — no `ctx.emit`, no access to the `codebuddyCollector` service, and `createStatusEngine(null)` disabled publishing outright, so the home plugin never received any data and the light never appeared for the whole call. Replaced with a **file channel** (MCP atomically writes `<dsh-home>/codebuddy-indicator-mcp.json`; the plugin merges it when serving the endpoint — both sides derive the path from their own module location, so no port is needed: `webServer.register` does not expose one and the port was observed to change). With the channel fixed, activity detail was still empty: `child.stdout` had no encoding set, so `'data'` delivered a `Buffer`, `pushChunk` silently dropped it, and `foldEvent` never fired (while tokens/session stayed correct, since the result is parsed from the accumulated string) — fixed with `setEncoding('utf8')`. Also replaced the Greek `Σ` in status output with `total`. Verified: the endpoint reports `state=running` throughout, with detail advancing to `cur=Bash#6` and the trail growing 1→12. Tests 68→73 |
| [v1.1.4](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.4) | Desktop 0.3.4+ (verified 0.3.14) / dsh 0.1.2-alpha.4+ | **Fixes rough dispatch in standard mode**: the CLI's own `error_during_execution` transient failure was previously neither retried nor explained (a failure returned a single head line), so the caller burned a `codebuddy_status` call and then guessed its way to an explicit `model`. Verified by reproduction: the same task on the same default model (`hy4-preview`) succeeded on a rerun, confirming a CLI/service-side transient fault rather than a bad model or a bad task. Now: ① transient errors are **silently retried once** (both the preset and MCP paths; kept separate from rate-limit/network failures, which still ask the user because retrying may just burn quota; resume/continue calls are never auto-retried); ② failures always carry **actionable guidance** (retry / switch model / finish with native tools); ③ the result head records the **model actually used** (`model=…`) plus `retried=1`, so diagnosing the default model no longer requires guesswork. Tests 66→68 |
| [v1.1.3](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.3) | Desktop 0.3.4+ (verified 0.3.14) / dsh 0.1.2-alpha.4+ | **Two fixes**: ① **plan-mode Bash gate** — under DSH plan mode the CLI denies Bash in `-p` non-interactive + `plan` (that tier needs interactive approval, and nobody can grant it non-interactively), so codebuddy reported "the Bash tool is not authorized in non-interactive mode (denied), so I used PowerShell instead" — the same shell class, gated inconsistently, burning turns and sometimes abandoning the investigation; plan mode now also passes `--allowedTools Bash` to **pre-approve** read-only shell. Verified: Bash works again while **writes stay blocked by plan mode itself** (the read-only guarantee holds), and `Read`/`Grep` are unaffected. ② **per-session light detection moved to host-side live enumeration** — no longer depending on the DSH client summary's `agentPreset` (default sessions simply lack that field → "unknown" → global-lease fallback → normal sessions still lit; and the field's location drifts across DSH versions): the home plugin now computes the codebuddy-first session list itself via `agents.list()` + `agentPresets.composedPreset()` and returns it from the endpoint, **taking effect immediately for already-open sessions**; that list and the client-summary channel form a dual channel where **either positive wins**, a safety valve so a sessionId mismatch degrades to "light off" instead of "never lights"; the preset also reports its sessionId as a fallback, and closing a session clears its light at once. Tests 58→66 |
| [v1.1.2](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.2) | Desktop 0.3.4+ (verified 0.3.5 & 0.3.14) / dsh 0.1.2-alpha.4+ | **Adapted to DSH Desktop 0.3.14 / dsh 0.1.2-alpha.5**. After the DSH plugin-system rework the client session summary's `agentPreset` moved into `projectionValues` and slot `inject` became zero-arg, silently breaking v1.1.1's per-session idle-light check (regressing to a global light); fixed with a **dual channel**: framework standard props first (`sessionId` + the `useSessions` selector hook reading `projectionValues.agentPreset`), falling back to the legacy `inject(sessionId)` + `sessions.list` snapshot, both readers accepting old and new summary shapes; the home plugin's package.json now declares `dsh.compat` (desktop/backend/verified); README version table gains a DSH-compat column and past releases are labeled with their DSH versions; combo-roster + HMR content-fingerprint verification byte-exact on 0.3.14 |
| [v1.1.1](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.1) | Desktop 0.3.5 (developed/verified); compatible with 0.3.14 (per-session channel needs v1.1.2) | **Fix: status light stuck on in non-codebuddy-first sessions** (both layers fixed): ① the home-level plugin's `presetActive` was a sticky flag — once any session had loaded the codebuddy-first preset since DSH started, every session showed a permanent "CB ready" light; now a **heartbeat lease** (preset announces every 30s; TTL 75s; `active:false` clears immediately) that goes out within ≤75s after the last codebuddy-first session closes. ② the idle "CB ready" light is now **per-session** — the client reads its own session's `agentPreset` from the `sessions.list` snapshot and shows the idle light only in sessions that are actually codebuddy-first, while activity pills stay global; the host-half state machine extracted into a testable `createIndicatorState()` with first-ever test coverage. Tests 49→58 |
| [v1.1.0](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.1.0) | Desktop 0.3.5 | **Dual backend: WorkBuddy**. `backend="workbuddy"` dispatches office tasks (documents/slides/spreadsheets, knowledge base, image/video generation, WeChat/WeCom replies) to Tencent WorkBuddy — a **same-engine twin CLI** of CodeBuddy (identical stream-json protocol, verified field-by-field), sharing login with the desktop app. **Session-aware backend routing**: the two CLIs keep separate session stores; continuing a session routes back to the owning CLI automatically. Results carry a `backend` field; status shows a `[workbuddy]` tag; the MCP server returns an install-hint error when WorkBuddy is missing. Test suite extended to 49 |
| [v1.0.0](https://github.com/new-256/codebuddy-bridge/releases/tag/v1.0.0) | Desktop 0.3.5 | **First official release**: `codebuddy_run` / `codebuddy_continue` / `codebuddy_status` tools + codebuddy-first policy + fallback dialog + home-level live status light + dynamic form + zero-dependency MCP server (with the `CODEBUDDY_MCP_ALLOWED_ROOTS` whitelist guard) + per-project token usage accounting (codebuddy exposes no quota API — token metering as the substitute) + `model`/`effort`/`maxTurns` selection; a shared `core/` (single source of truth + generated derived artifacts + sync-lock); a reliability baseline (status tool survives every failure path, session-aware cwd fallback, single-shot dialog, background hang guard, narrowed rate-limit detection, cross-chunk half-line-safe parsing) locked by 44 fault-injection regression tests, CI syntax matrix + tests + version triple-lock |

## Security notes

- `--permission-mode bypassPermissions` means codebuddy will edit files and run commands without asking again. This is the direct implementation of the "DSH fully controls codebuddy" requirement; use it only where you trust codebuddy's execution environment. The MCP form can restrict working directories via `CODEBUDDY_MCP_ALLOWED_ROOTS` (see [`mcp/README.md`](mcp/README.md)).
- The plugin only registers into the host `tools` / `systemPrompt` registries and exposes one package-private read-only `codebuddy_status` JSON method; it publishes no cross-session service, so it is safe on the preset plane (no isolate realm needed).
- All side effects (tool registration, prompt section, styles, timers) are attached to the current fiber via `ctx.effect` / `ctx.tools.register` / `ctx.timeout`, and are cleaned up automatically on stop / update / undefine.

## License

[MIT](LICENSE) © 2026 chenglong