Connects locally signed-in WorkBuddy models to DSH, supports per-model image input opt-in, account switching, and a read-only credits overview with daily check-in.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-connect-workbuddy
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:dingminhua/dsh-connect-workbuddy
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
English | 中文
A DeepSeek Harness (DSH) bundle plugin that connects locally signed-in WorkBuddy models to the DSH model picker — both the CN app and the international WorkBuddy AI app are supported — with an account pool and selectable model management. The CN and international sides are two parallel providers (workbuddy / workbuddy-global) that can be used at the same time; the settings card separates them with tabs for convenient management.
Features
Dual parallel providers (CN + international) — the CN side registers as the
workbuddyprovider (GLM-5.3,DeepSeek-V4-Pro,Kimi-K3,MiniMax-M3,Hy3, etc.), the international side asworkbuddy-global(GPT-5.6,Gemini-3.5-Flash,GLM-5.3,Kimi-K3, etc.). A region's models appear in the DSH model picker when that region has an account: with accounts on both sides, both rosters are selectable at once, and different sessions can each pick a side without interfering. A region with no local sign-in lists no models — it cannot serve a single request, so listing them would offer nothing but failures. Sign in once for that region and its models come back on "detect accounts again" or after a restart; the provider itself stays registered either way, because its settings card is how you sign in (re-detection lives in the pool block). Model names carry the upstream credit multiplier (e.g.GLM-5.3 · x0.79), matching WorkBuddy's own model menu.Tabbed settings card — the card's top carries a "Domestic / Global" tab bar; each tab holds its own account pool and model management. Account, directory, selection, and unsaved drafts are fully isolated per tab — switching accounts or refreshing models on one tab never touches the other side's runtime catalog or sessions.
Switch either provider off on its own — each tab carries an on/off checkbox (both checked by default). Unchecking one withdraws that provider from DSH's model picker entirely (it is not merely a hidden tab): its adapter route and its "Settings → Models" directory entry are both pulled, and startup no longer sends pointless requests for it. Account, credits, directory, and selection are all kept, and re-checking restores everything. The typical use is "I have no use for the international side" — switch it off and keep the picker clean. Note: a session that had already selected a model from the switched-off provider will report
NO_ADAPTERon its next call; the card states this plainly on the switched-off tab instead of failing silently.Model management — refresh the full catalog from upstream and enable or disable each model individually; refresh is a draft operation that only takes effect on save. Upstream also reports credit multiplier, context/output limits, and reasoning efforts; image input is decided by a vendor-verified table: a refresh pre-checks only models the vendor's own documentation calls natively multimodal, leaves documented text-only models unchecked, and treats anything without a vendor source as unverified and unchecked rather than guessing. Upstream's
supportsImagesis a platform declaration, not the model's capability, so it does not drive that decision at all. You can still tick any box by hand, and your selection is kept locally. Every model row also carries a “Can disable” (off) checkbox: it decides whether that model gets the “turn thinking off entirely” level. The default comes from a reviewed built-in table (the 9 models that claim the capability but answer HTTP 400 for it start unticked), and you can change it per model — untick for a newly refusing upstream model, tick for one the table wrongly hides or you have verified works; your ticks are saved on first save and recomputed no further.Model availability testing, including cooldown recovery times — working out when a limited model frees up used to mean guessing through repeated failures. Now every model row has a Test button that gives you a direct answer for whether the model works right now and, if not, when it comes back. It sends a real-volume request (about 25k input tokens), rather than "ping it and see if it answers" — because the upstream throttles on request SIZE (measured: about 20k tokens pass, about 30k are refused), so a small request looks fine while every request in a long conversation may be refused. That is exactly where "the test passes but I am limited" came from. Where the recovery time comes from: the upstream's 429 carries no
Retry-Afterheader (and noX-RateLimit-*), but it writes the reset time into the response body —{"code":6004,"msg":"…将在 2026-09-30 02:30:30 UTC+8 重置…"}. The plugin parses that time and converts it using the UTC offset the message states, so the result does not depend on your machine's timezone; the card then shows “✗ Rate limited · usable again after 2026/9/30 02:30:30”. When none of the sources states a time, the plugin never invents a countdown and says plainly that no time was given. A rate limit deliberately does not borrow the monthly refresh time, which would tell you to wait weeks for a limit that clears in seconds. Probes run in the Host, so the credential never reaches the page, and results are not written to settings — your saved selection is untouched.Two things to know. ① It really does spend credits — one measured probe reported
credit0.02 (a refused one 429s and costs almost nothing). So testing is manual and per-model only: there is deliberately no “Test selected” batch button, because sweeping the whole list from one click is an uncontrolled spend — and the Host enforces the same rule, accepting a single model only and refusing multi-model requests. ② The upstream advertises a 1M context yet throttles far below it, and the plugin advertises each model's native window to DSH (no longer clamped to 200K as of 3.5.0), so DSH compacts very late — hitting the throttle in a long session follows directly from that. To make DSH compact sooner, set a “DSH context budget” cap on the model's row (200K / 500K / the model's native window).Local account switching — discovers the multiple sign-in credentials WorkBuddy's desktop app leaves behind and lets you switch per region. Tokens are never written to DSH settings.
Credit readout in the composer toolbar (follows the selected model, per region) — a clickable readout sits at the left of the composer toolbar (e.g.
WB CN · 1,072) and switches region automatically with the model this session is using: pick a WB CN model and it shows the domestic balance, pick a WB AI model and it shows the international one; with any other provider's model it does not appear at all. It refreshes every 5 minutes and can also be refreshed by hand. Clicking it opens a panel: a table of balances per account, where clicking a row switches to that account (the same setting the card's account picker writes), and an account that cannot serve right now is labelled in its row (e.g. rate-limited, out of credits) using the same wording as the account-pool table. The per-account figures are fetched only while the panel is open, so the 5-minute background refresh never multiplies by the account count. To turn a side off, untick “Show WB CN / WB AI credits in the composer” in that region's tab on the card; while a region's provider is switched off, the readout disappears and its switch is disabled.Account pool: batch actions across several accounts, plus automatic failover — tick the accounts you want into a pool and you get one-click check-in for every account, one-click testing of every account against a model you choose (defaulting to the region's zero-multiplier free model), and with Enable the account pool switched on, automatic failover: a request that fails upstream is retried against the pool's other usable accounts, in order, until one serves it or none is left. Membership starts empty and is an explicit opt-in: "signed in on this machine" is not the same as "spend this account's credits and claim its rewards". The pool's ranking decides who serves every request (usable first, then higher credits, soonest-expiring, freshest credential), and on a failure the next usable member is tried. It never writes back your saved selection — switch the pool off and your own account applies again immediately. The switch only decides whether the PLUGIN chooses — with it off, the plugin stops picking and switching accounts for you, but check-in and testing stay available (they are manual actions). Testing reuses the model rows' real-volume probe, so it does cost real credits; automatic check-in is deliberately not offered — a check-in claims real rewards, so it only ever runs when you press the button. See Account pool.
Actionable advice when a credential is refused — when the upstream rejects the selected account's token, the plugin actually probes the other local sign-ins: if one still answers, it tells you to switch to it (with a one-click switch) instead of vaguely asking you to sign in again — re-authenticating fixes nothing while you are pinned to a revoked backup. Only when there is genuinely no other local account does it ask you to sign in again.
Add accounts by QR sign-in, import/export them in bulk, test or delete them one by one — the WorkBuddy desktop app is no longer a prerequisite: the card's Add account by QR code (labelled Sign in on the web for the international build — that region uses a browser login page, there is no QR) opens a dialog and takes you straight to the authorization page in your browser; if the browser blocks that, the dialog's link, copy and Open page again are the fallback. Every credential lands in the plugin's own vault (
$DSH_HOME/workbuddy-vault/<region>/<accountId>.json, one file per account), and the desktop app's auth files stay read-only and are never modified. Each row of the account table gains Test and Remove: a row test reuses the same probe as the batch run, so the two are comparable; removing deletes only the copy the plugin stored and hides the account: it disappears at once and the automatic poll cannot bring it back, while Detect accounts again restores it (signing in or importing the same account clears the hide too). The desktop app's sign-in is never touched — removing means "this plugin no longer lists it", not "sign out of the app". Export / import accounts moves credentials to another machine: pick accounts to export a JSON file → choose a file and read a redacted preview (field names and whether a token is present; the token itself never enters the preview) → tick what to import. An import overwrites per account rather than duplicating, and only a record positively identified as belonging to that region is stored — a domain the plugin cannot classify is skipped and counted as skipped. See Account pool.Credits live in the account pool — the member table shows each account's remaining credits and nearest expiry, and credits are the ranking's second key (more credits serves first). The queries are read-only and consume no credits.
Multi-candidate credential paths — probes the platform defaults for macOS / Windows / Linux in turn, overridable by environment variable or directly in the card. When nothing is found, the card lists which paths were probed and why each one failed: five distinct reasons (absent / unreadable / no usable token / encrypted with no key available / belongs to the other region), so the easiest case to misdiagnose — signed in, but the desktop app is not present — says the app must be there instead of telling you to sign in again. The belongs to the other region reason is different in kind: that sign-in is real and usable, it is simply filed under the other tab, so the card states the fix outright — switch tabs, no need to sign in again. The list says it once: the paragraph states the conclusion, the collapsed list supplies the per-path detail, and the two never repeat each other.
Secure loopback shim — one random port + in-process random secret per region; the real WorkBuddy token is never handed to pi-ai.
Command-line diagnostics —
status/doctor/logout, reporting sign-in and credits per region, without a browser.
How it works
DSH PiAiAdapter (one stack per provider)
-> secure loopback shim (one random port + in-process random secret per region)
-> WorkBuddyUpstreamClient
-> CN: https://copilot.tencent.com/v2/chat/completions
-> Global: https://www.workbuddy.ai/v2/chat/completions
-> WorkBuddy SSE
-> DSH executes local tools and returns their results
The CN and international sides each own a complete runtime stack — credential store, model catalog, loopback shim, adapter — with visibility filtered by credential domain (workbuddy.ai / codebuddy.ai → international, anything else → CN), so both regions' accounts can be signed in and used by different sessions at the same time. The international product has two brand domains: the desktop app signs in at workbuddy.ai, while the CodeBuddy CLI signs in at codebuddy.ai — both are international.
The model catalog comes from each region's correct source, and both read the same /v3/config document the WorkBuddy app itself consumes; they differ only in which client identity requests it: CN uses the CodeBuddy CLI user agent, international uses the desktop user agent (the config service serves a different roster per client channel — only the CN CLI channel's roster carries the free hy4-preview-f, while the international side needs the desktop agent to get the account's real 20-model list, including the free deepseek-v4.1-flash). If CN cannot read /v3/config, it falls back to the legacy /v2/enterprises/personal/models and logs a line — that legacy roster is the CLI channel's, where the same slot carries the paid hy4-preview (x0.29) rather than the free hy4-preview-f (x0.00) the app shows, so it is used only while degraded. The credits overview comes from https://www.codebuddy.cn/v2/billing/meter/get-user-resource (international accounts auto-route to the global gateway their credential belongs to, workbuddy.ai or codebuddy.ai — tokens are not interchangeable across the two brand domains); all are read-only.
Credentials are read (read-only) from the WorkBuddy desktop app's own auth file. Refreshed tokens are kept per region in $DSH_HOME/.workbuddy-auth.cn.json and $DSH_HOME/.workbuddy-auth.global.json (two simultaneously signed-in accounts never overwrite each other; the legacy single file .workbuddy-auth.json is still read as a migration source); the desktop app's file is never written.
Encrypted credential fields: newer desktop builds (Windows first) replace the auth file's accessToken / refreshToken strings with an AES-256-GCM envelope ({"$wbEncrypted":1,"envelope":"…"}), and the account's display name (nickname) is encrypted the same way — left unopened it degrades to a bare uin number. The envelope is sealed with a field key compiled into the app itself, not a user secret — so the plugin ships no key copy. When it meets an encrypted document it runs the installed app with ELECTRON_RUN_AS_NODE and calls that app's own native binding (electron_browser_workbuddy_storage.loggerGet()) to fetch the same payload and derive the key locally: it asks the very build that wrote the file. The key is cached in process memory only — never on disk, never in logs. Plain-string documents (macOS, older Windows builds) take the original path and never spawn a child process. account.phoneNumber is encrypted there too, and the plugin deliberately neither reads nor displays it. If the app lives somewhere unusual, point WORKBUDDY_APP_EXECUTABLE at its executable.
Account pool
A machine usually carries more than one WorkBuddy sign-in. The account pool turns that into a single click: tick the accounts you want into the pool, then press Check in all accounts or Test all accounts; switch on Enable the account pool and the plugin will carry on through a failure by itself.
Each region (domestic / international) has its own independent pool — membership and target model never affect the other side, because the two sides' accounts belong to different upstream stacks.
The two batch actions
- Check in all accounts — signs in account by account. It reads today's status first, so an account that is already checked in is marked as such and receives no write request at all (idempotent — no duplicate claims); a single account failing marks only that row and never aborts the batch.
- Test all accounts — sends one real-volume probe per member (about 25k input tokens; the same
probeModelthe model rows use, so it inherits both measured facts: throttling is triggered by request size, and the reset time is written into the response body rather than a header), reporting whether that account can serve that model right now.
Which model is tested: by default the zero-multiplier free model in that region's catalog (measured: the domestic catalog currently has exactly one, hy3; the name is deliberately not hard-coded, because the upstream changes multipliers — deepseek-v4.1-flash has already moved from 0 to 0.11), or a specific model you pick in the dropdown. If the model you named has left the catalog, the manual action refuses outright (target-model-stale) instead of quietly substituting another one — you asked to test that model, and swapping it answers a question you did not ask.
Cost and boundaries: testing is a real request. Pointed at the free model (multiplier 0) it costs virtually nothing, but it is not a free no-op; check-in claims that day's real reward. Both actions run only when you press the button — the plugin does not offer automatic check-in, and the card does not show a switch that can never be flipped.
Membership is an explicit opt-in, empty by default
"signed in on this machine" is not "spend this account's credits and claim its rewards". Membership starts empty and must be ticked one by one (or with Select all); an empty pool never degrades into "all accounts" — with nothing ticked, both batch actions are refused rather than running every account on your behalf.
Ticking boxes is a draft edit: it lands on Save, the save button carries a dirty marker until then, and the draft survives switching tabs or closing the card.
Automatic routing and failover: what "Enable the account pool" turns on
This plugin's standing rule is that it never rewrites who pays — your saved choice is yours. What the switch changes is who serves, decided by the pool's ranking, request by request:
- The ranking picks the serving account for every request — so the card's "in use" account is the ranking's answer, which is not necessarily the one you picked above it. The ranking is below.
- On a failure the next usable member is tried, then the next, until one serves it or none is left. Only then is the error reported — annotated with how many accounts were tried, so a pool that failed over and still lost does not read like a single account failing.
- Your saved selection is never rewritten. Turning the pool off restores it immediately and completely (the pool steers through a runtime override, never through the settings). That is also why the card states which account is actually in use: it may be a different one, and you should be able to see that.
- Which failures trigger a retry: 429 rate limits, 402 exhausted credits, 401 dead sessions, 502 gateway/network errors — all of these describe one account's state, and another may well survive them.
- Which do not: a malformed request (HTTP 400). Every account answers the same 400, so walking the pool would only multiply the wait before the user sees an error they must act on anyway. A stream that dies mid-flight is not retried either — bytes are already on the wire, and replaying would splice two answers together.
- Members already known to be unusable are skipped — a rate-limited account still inside its stated cooldown, or one whose credential the upstream rejected. An account that was never tested counts as usable: a freshly discovered sign-in must not be invisible just because nobody has measured it yet.
- A live failure is RECORDED as a measurement (with the reset time the upstream stated), so the NEXT request starts from an available account instead of re-hitting the account that just refused it. Before this, only the manual batch test wrote measurements — so a just-limited account still read as untested and kept being picked first. HTTP 400 is the exception: that is the REQUEST being rejected, which says nothing about the account, and recording it would sideline a good one. Accounts not checked into the pool are not measured either, since the tick is what grants "you may spend this account's credits".
- Every retry leaves a
warnline in the host log naming the failure and the account that follows.
What the switch actually controls
It controls whether the PLUGIN decides by itself — not whether YOU may act.
| Pool on | Pool off | |
|---|---|---|
| Who serves | Chosen automatically by the ranking (usable → credits → soonest-expiring → freshest credential) | The account you selected; the plugin does not choose |
| On a failure | Automatically retried on the next usable account | Reported as-is, no retry |
| Check-in all / test all | Available | Still available |
| Target test model | Editable | Still editable |
The last two rows are deliberate: check-in and testing are MANUAL actions over the members you ticked, which is a different question from "does the plugin route automatically". Gating them on the switch meant switching the pool off ALSO removed the ability to check in or test — while "off" should mean "I'll do it by hand", not "nothing is allowed".
The one hard rule: with nothing checked in the pool, batch actions are refused. An empty pool never degrades into "then run on every account" — that is the one reading which would spend credits you never authorized.
Where manual selection lives: in the account pool section. With the pool off that section shows an "Account in use" dropdown; picking one takes effect immediately (it writes the region's account slot through the same verified path the credential-recovery "switch to a usable account" action uses). With the pool on the row is absent — the ranking decides there, and a dropdown that changes nothing is worse than no dropdown. Account discovery ("detect accounts again"), membership, check-in and testing all sit in that one section, so accounts are managed in exactly one place.
The ranking (what decides who serves)
- Usable first — an account a measurement rules out (limited with its cooldown unexpired, credential rejected, upstream unreachable) sorts last and is not chosen;
- Higher credit balance first. Credits take part only when a fresh reading exists (what the card's poll, a batch test or a save collected; valid for 10 minutes). The request path never fetches credits — that is an upstream call per member, and one page of chat would turn into N extra requests;
- Soonest-expiring credits first — spend what lapses in three days before it is lost;
- Freshest credential first;
- The account id closes the order, making it total and reproducible rather than jittering with the roster's order.
Why an account leaves the pool, and when it comes back
| Probe outcome | What it means for failover |
|---|---|
| Usable | Eligible |
| Rate limited | Out of the pool temporarily, returning on the reset time the upstream stated (and when it states none, the card says so rather than inventing a countdown) |
| Out of credit | Out of the pool, returning at the monthly package's refresh time |
| Credential rejected / model not found / upstream unreachable / failed | Out of the pool until a later probe refreshes the result |
The key line: "rate limited" is not "unusable". It is precisely the state most worth waiting on — treating it as permanently dead throws away the accounts that would have served you best.
Manual selection: there is no account dropdown any more
Accounts are managed in the account pool: membership checkboxes are the selection surface, and re-detection lives there too. The old "account" dropdown had become self-contradictory once the pool existed — with the pool on, the ranking decides who serves, so changing it did nothing yet looked authoritative, and the status line above could name one account while the dropdown showed another.
The region status line changed with it: while the pool is on it does not name a single account (several signed in at once is the normal case, so "Signed in: A" was simply the wrong claim) — it states that the region is usable and that the pool picks. While the pool is off it does name the account, because then it really is the only answer. The dot and the token expiry beside it are about "can this region be used", which is a different question from "who is being billed" — the pool block answers that one.
Saving and drafts: why the card has two Save buttons
One for model management, one for the pool — deliberately kept apart rather than merged: the two draft domains differ, and merging them means one failed write drags the other down (a catalog that will not save would stop you saving pool preferences too); a shared Discard would also throw away both drafts on a single misclick. Both sides use a verified write — a write that did not land never discards the draft, or your edits are lost for good.
One class of state in the pool does not go through a draft: probe results, cooldowns, and the account that served a request. Those are observations, written frequently by the plugin itself. The reason is practical — drafts are overwrite-based, so if observations lived in one, a single manual Save could roll the result a timer had just written back to a several-minute-old value.
Install
⚠️ Version requirement: DSH 0.1.7-rc.1 or newer (current window:
0.1.7-rc.1–0.2.x). Since v2.1.0 this plugin only supports hosts on DSH 0.1.7-rc.1 and above (older hosts cannot resolve the dependencies to install this version); hosts on 0.1.5 and earlier should stay on v2.0.15. Since v2.1.2 the upper bound is<0.3.0-0: the whole 0.2.0 line, GA included, is verified; the 0.3.0 line is explicitly refused. See Supported host range below.
Supported host range
DSH's shipped gate (evaluatePluginCompatibility in packages/boot/app-boot/src/plugin-compatibility.ts) checks every declared dsh-* peer range; one unsatisfied peer skips the entire bundle (it lands in skippedBundles and is only printed to stderr) — no provider is registered, no card appears, the model list goes empty, and the page shows no error at all.
| Host version | Verdict |
|---|---|
0.1.7-rc.1 – 0.1.7-rc.2 |
✅ supported |
0.2.0-rc.1 – 0.2.x (0.2.0 GA included) |
✅ supported (verified from 2.1.2) |
0.3.0-0 and above |
❌ explicitly refused (that line is unverified) |
0.1.6 and earlier |
❌ explicitly refused (0.1.5 users should stay on v2.0.15) |
The upper bound is written
<0.3.0-0, not<0.3.0: every DSH tag to date is a prerelease (-alpha.N/-rc.N), so<0.3.0alone would admit the entire unverified0.3.0-alpha.1/0.3.0-rc.1line, leaving the guardrail inert — exactly the defect shape 2.1.2 fixes.tests/dsh-line.spec.tsguards this contract.
🪟 Platform support: Windows, macOS, and Linux are supported side by side. Windows is a first-class target, not an afterthought: CI runs the full suite on
windows-latest, and four areas carry Windows-specific branches with their own tests — credential paths, process heartbeat, desktop-app executable lookup, and settings writes. When fixing a defect that only reproduces on Windows, the Windows reporter's field evidence and an encryption vector captured from a real Windows install are what the fix is judged against (see the 2.0.6 / 2.0.11 / 2.0.12 sections of CHANGELOG). See the Platform support section for the per-platform differences and known limitations.
Prerequisite: the WorkBuddy desktop app is installed and signed in (the plugin reuses the app's sign-in state).
dsh plugin --profile desktop add dsh-connect-workbuddy
Or directly via npm:
npm install dsh-connect-workbuddy
Restart the DSH process after install/update/uninstall.
⚠️ Pin an EXACT version for a fresh release; do not rely on a floating range. pnpm enforces a release-age window (
minimumReleaseAge, 24h by default), so shortly after a version ships a floating range is silently resolved back to the previous one:^4.4.2still resolves to4.4.2, while an exact4.4.3gets4.4.3immediately. Both spellings "install successfully", so the symptom is "the upgrade command finished and the version did not change" — easily misread as a failed release. Measured (isolated directory, pnpm 12.3.4,--lockfile-only, touching nothing of the user's environment):
Target spelling Resolved version floating ^4.4.24.4.2 ⚠️ exact 4.4.34.4.3 floating + --config.minimum-release-age=04.4.3 To pick up a new release right away, pin it:
dsh plugin --profile desktop add dsh-connect-workbuddy@4.4.4. A floating range catches up once the window elapses.
The plugin also runs under Web and TUI; pick the command matching your profile:
dsh plugin --profile web add dsh-connect-workbuddy # Web
dsh plugin --profile dsh-tui add dsh-connect-workbuddy # TUI
Platform support
All three platforms are supported targets; the differences below are implementation facts, not polite ways of saying "not yet supported". To change this section, change docs/WINDOWS.md too — that file is where platform-related constraints are registered.
| Aspect | Windows | macOS | Linux |
|---|---|---|---|
| Default credential directory | %LOCALAPPDATA% / %APPDATA% first, falling back to <home>\AppData when unset (so a redirected profile still resolves) |
~/Library/Application Support/CodeBuddyExtension/Data/Public/auth |
$XDG_CONFIG_HOME first, falling back to ~/.config |
| Desktop-app executable | %LOCALAPPDATA%\Programs\WorkBuddy, %LOCALAPPDATA%\WorkBuddy, and both Program Files (located as WorkBuddy.exe) |
Read from the bundle's Info.plist (CFBundleExecutable) — the real value is Electron, never guessed from the app name; covers the international WorkBuddy AI.app and apps filed under a subdirectory of /Applications |
Not applicable (no corresponding app for encrypted credentials on Linux; point WORKBUDDY_AUTH_FILE at the credential) |
| Process-heartbeat timestamp source | PowerShell Get-Process StartTime (POSIX has no ps -lstart equivalent) |
ps -o lstart= |
ps -o lstart= |
| Child-process window | windowsHide is required: the host itself has no console (it is a GUI process), so without it every probe flashes a visible black window on screen |
Not applicable (the option only takes effect on Windows) | Not applicable |
| Transient settings-file contention | Does hit it: an antivirus scanner / OneDrive / an editor locking the profile's cordis.patch.yml briefly makes the rename replace fail, and @deepseek-ai/dsh-atomic-write's retries are enabled only on win32 |
Does not: POSIX rename replaces outright, so the same code never enters the retry branch | Same as macOS |
| CI coverage | windows-latest, full suite |
The developer's own Mac | ubuntu-latest, full suite |
Three Windows-specific things worth knowing:
- An account change may fail to save (the profile's
cordis.patch.ymlheld open). The plugin reads the value back after writing: it reports "cleared" only once the write is confirmed on disk, and otherwise reports the failure and keeps your previous choice. Close whatever holds the file and retry. See Known limitations. - Credential paths are partly verified on real Windows hardware. The candidate list is derived from platform conventions and pinned by unit tests (including both the unset and the blank-string fallback for
LOCALAPPDATA/APPDATA). On a real Windows 11 machine,%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\authand%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exedid resolve, and the encrypted credential'skeyIdmatched the registered vector verbatim. The remaining candidates are still not verified per-item — if your credential or app lives elsewhere, pointWORKBUDDY_AUTH_FILE/WORKBUDDY_APP_EXECUTABLEat it and please report what you see. - Encrypted credentials (from 5.6.0) require the desktop app to be present, on Windows exactly as on macOS; if the app lives in a custom directory (e.g.
E:\WorkBuddy\WorkBuddy.exe), pointWORKBUDDY_APP_EXECUTABLEat it.
Using it on a server: carry the credential over
On a headless server (or any machine where you would rather not install the WorkBuddy desktop app), the plugin has no local credential to read. The way around it is to copy the credential directory over from a machine that has already signed in to WorkBuddy — the plugin only reads that file and does not require the app to be present (provided the credential itself is not encrypted, see below):
# 1) On the machine that is already signed in to WorkBuddy, locate and pack the credential dir
# macOS: ~/Library/Application Support/CodeBuddyExtension
# Linux: ${XDG_CONFIG_HOME:-~/.config}/CodeBuddyExtension
# Windows: %LOCALAPPDATA%\CodeBuddyExtension and %APPDATA%\CodeBuddyExtension
tar czf codebuddy-cred.tgz -C <the parent directory above> CodeBuddyExtension
# 2) Move codebuddy-cred.tgz to the server and unpack it into the directory the plugin reads
# Linux:
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}"
tar xzf codebuddy-cred.tgz -C "${XDG_CONFIG_HOME:-$HOME/.config}"
# 3) Confirm the plugin sees it
dsh plugin --profile web exec dsh-connect-workbuddy status
⚠️ On Linux there is no
~/.local/sharepath. The candidate directories come fromdefaultDesktopAuthDirs()insrc/auth.ts, and Linux reads only$XDG_CONFIG_HOME(defaulting to~/.config):
Platform The credential directory the plugin actually reads macOS ~/Library/Application Support/CodeBuddyExtension/Data/Public/authLinux ${XDG_CONFIG_HOME:-~/.config}/CodeBuddyExtension/Data/Public/authWindows CodeBuddyExtension\Data\Public\authunder%LOCALAPPDATA%and%APPDATA%So unpacking into
~/.local/sharealone will not be seen — either unpack into the directories listed above, or pointWORKBUDDY_AUTH_FILEstraight at the auth file (when set explicitly, sibling backup files in that directory are still scanned, so account switching keeps working).
🔐 Encrypted credentials (WorkBuddy 5.6.0 and later) cannot be carried over this way. Recent desktop builds encrypt the token fields inside the auth file, and the key is fetched at runtime from the app's native binding and never written to disk. On a server without the app, copying the encrypted auth file over will not be readable (the plugin reports not-signed-in honestly rather than degrading into reading a truncated token). What works: carry the credential from a machine still running a pre-5.6.0 version or an unencrypted credential, or install the app on that server to obtain the key.
Two
doctorlines tell you which case you are in:Desktop auth file: present (…/CodeBuddyExtension/Data/Public/auth/.workbuddy-auth.json) Encrypted-credential support: unavailable (…)
Desktop auth file: presentmeans the file was found; ifstatusstill reports not-signed-in whileEncrypted-credential supportisunavailable(no app on the server), that is the encrypted-credential-without-key case — carrying the file over cannot fix it.
🔒 The credential is the account: that file is a directly usable session. Transfer it over an encrypted channel (
scp/ private network), tighten it withchmod 600once on the server, and keep it out of anything that gets packaged or committed.
Command line
dsh plugin --profile <web|desktop|dsh-tui> exec dsh-connect-workbuddy status # sign-in state and remaining credit
dsh plugin --profile <web|desktop|dsh-tui> exec dsh-connect-workbuddy doctor # credential paths and host diagnostics
dsh plugin --profile <web|desktop|dsh-tui> exec dsh-connect-workbuddy logout # clear the plugin-held credential copy
status and doctor accept --json for machine-readable output.
Why these three commands work on a machine with nothing installed (issue #47)
The plugin declares
@deepseek-ai/dsh-atomic-write,@deepseek-ai/dsh-home-paths, and the rest of the DSH packages as peers: the host supplies them and the plugin ships no copy. That works inside a booted DSH process (the host's runtime interception layer resolves those names against the installation), butdsh plugin … execis not that — it runs onepnpm execin the profile directory, and that child uses stock Node resolution. The profile is created withautoInstallPeers: false, so the peers are never installed there and the two packages cannot be found.Up to 4.3.1 this was a load-time crash (
ERR_MODULE_NOT_FOUND), naming a file that looked unrelated to the command:Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@deepseek-ai/dsh-atomic-write' imported from …/profiles/dsh-tui/node_modules/dsh-connect-workbuddy/lib/host-heartbeat-*.jsThe reason is bundling:
auth,host-heartbeat,at-rest, andpi-ai-runtimeare emitted as one shared chunk, so the momentlib/bin.jsimported it, the top-level import inauth.tswas evaluated — even for a read-onlystatus. From 4.3.2 those two packages are host-first and loaded on demand: the host's copy still wins where the host is present (keeping one cross-process write protocol shared with the host), and a small built-in implementation takes over where it is not.exectherefore works on a clean machine, anddoctorprints which one is in use:Atomic writes: built-in copy (host peer not resolvable here; normal outside a booted DSH)Inside the repository checkout (peers present) it reports
host-provided (@deepseek-ai/dsh-atomic-write)instead. The guard lives intests/host-peers.spec.ts: it scans the built output and fails as soon aslib/bin.js's dependency graph contains a bare package name again.
Development
pnpm install
pnpm run check # typecheck + test + build
Local dev via a link: install to the desktop profile (restart DSH Desktop after editing):
dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
Calling chatStream() from a script: it returns an envelope, not a stream
const envelope = await client.chatStream(credential, bodyJson)
if (!envelope.ok) { /* failure: envelope.status / .kind / .message / .detail */ }
else { await envelope.response.text() /* or consume it as SSE */ }
Check envelope.ok before consuming. It returns { ok: true, response } or { ok: false, status, kind, message, detail } — not something you can for await directly. Skip that check and the usual outcome is every failure reported as a success (there is no response, reading it throws or is swallowed, and "all models passed"). Recorded here on the strength of the issue #56 reporter's own note, so the next person does not lose the same hour.
kind on a failed envelope is the failure classification used throughout this document (soft_rate / model_busy / hard_credit / session_dead / policy_reject / not_found / server / client), and detail carries the upstream upstreamCode / requestId / displayMsg.
Marketplace listing
The plugin ships an installable dsh.bundle manifest and is published to npm. Community marketplaces generally sync entries from the Awesome DSH Plugin registry; the submission draft lives at awesome-dsh-plugin-submission/dingminhua__dsh-connect-workbuddy.yml, and the formal submission is PR #3812 (data/plugins/dingminhua__dsh-connect-workbuddy.yml, CI and Submission gate green, awaiting maintainer merge).
Listing directory rules (per contributing.md):
- One YAML per plugin:
data/plugins/<owner>__<repo>.ymlwithurl(exactly matching the repo),name(owner/repo),category(one of the valid values; this project usesmodel), anddescription.en(required, ending with a period) /description.zh(optional); a description containing:must be quoted. - The repo must declare a
dsh.bundlemanifest (heredsh.bundle.patch: ./cordis.patch.yml) and carry thedsh-plugintopic; the ≥ 10 commits and ≥ 1 day age are checked automatically by CI. - Both READMEs (
README.md/README.zh.md) are generated — never edit by hand; after changing the YAML, regenerate withnode scripts/generate-readme.mjs. - At most 3 entries per PR; change only your own entry, never touch other plugins.
Marketplace screenshots and GitHub README badges are two separate mechanisms:
- GitHub badges are produced by the Shields/dshfind image links at the top of this README.
- Marketplace screenshots follow the current convention and are declared in the plugin's own root
screenshots.json(this project declares its usage-card screenshot there). The registry'sdata/screenshots.jsonis a legacy fallback and no longer accepts new keys (this project removed its edit before submitting). - Marketplace icons/placeholders follow each marketplace's own display rules; they are not a generic npm
package.jsonfield nor a README badge.
Known limitations
- The plugin depends on WorkBuddy client endpoints (not an official public API), so a WorkBuddy update may require adjustments.
- Account switching relies on the historical auth files the WorkBuddy desktop app leaves behind (the app's own backups), not an official multi-account API. Following the app's current sign-in remains the default; switching is an explicit opt-in, and historical credentials can be invalidated by the app's cleanup or sign-out.
- If an explicitly selected account disappears locally (the app replaced its login or cleaned up its backups), the plugin does not silently switch to another account — that would bill a different account. The card says the saved account no longer exists; pick an account again, or use "Follow the app's sign-in" to clear the saved choice. Signing in again in the desktop app will not fix this: the credentials are fine, it is the saved account id that no longer matches.
- Users upgrading from before 2.0.0 may still carry the old top-level
accountId(the migration source). It applies only to its own region and stops applying once that region is explicitly cleared (accounts.<region> = "") — otherwise the account you cleared would be silently restored at the next startup. The card shows whether a region is "using the saved account" or "following the app's current sign-in": the restored default is usually the very account you just cleared, so without that line there is no way to tell a successful clear from a no-op.
- On Windows, an account change may fail to save while the profile's
cordis.patch.ymlis held open. DSH updates that file by writing a temp file and replacing the target; an antivirus scanner, a sync client (OneDrive), or an open editor can lock it briefly, and once the replacement retries are exhausted the failed write does not makeset()reject. The plugin therefore reads the value back after writing: it shows "cleared" only once the write is confirmed on disk, and on failure it reports the error and keeps your previous choice (never a false confirmation that the old selection silently contradicts). Close whatever holds the file and retry. - Windows and Linux credential paths are derived from platform conventions. On a real Windows 11 machine,
%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\authand%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exedid resolve, but the remaining candidates and the Linux paths are still not verified on real hardware; setWORKBUDDY_AUTH_FILEif yours lives elsewhere. - The standalone CLI (
dsh plugin … exec) does not carry the host-supplied peer packages, so outside the host the plugin falls back to a built-in atomic-write implementation (src/host-peers.ts). It transcribes the host's behavioural contract (the<file>.locknaming, the WindowsEACCES/EBUSY/EPERMretries and their cadence) and is used only when those two packages cannot be resolved — inside the host, and in this checkout, the host's or the dependency's copy is still used.doctorreports which one is in effect. See the issue #47 note under Command line. - Encrypted credentials require the desktop app to be present. Newer desktop builds (from 5.6.0, on macOS as well as Windows) encrypt the token fields in the auth file, and the plugin needs that app's own native binding to obtain the field key. If the app is not installed, was uninstalled, or lives outside the probed paths (on Windows the install registration is consulted automatically, so a non-default install is found without configuration; set
WORKBUDDY_APP_EXECUTABLEif that misses too), an encrypted file cannot be read — the account then shows as signed out, and the plugin never degrades into emitting a truncated token.doctorreports whether this capability is available. The key is a build-time constant that may rotate with app releases; the plugin fetches it from the local app each run and caches nothing on disk, so an app upgrade needs no plugin upgrade.- The executable is located from the bundle's own declaration, not guessed from the app name. The WorkBuddy macOS bundles declare
CFBundleExecutableasElectron(notWorkBuddy), so the plugin reads each bundle'sInfo.plistto learn the binary's
- The executable is located from the bundle's own declaration, not guessed from the app name. The WorkBuddy macOS bundles declare
…
Links
More in this category
corrinehu/dsh-workbuddy-connect★ 776
Brings the models in the WorkBuddy desktop app straight into DeepSeek Harness — zero configuration in the DSH chat.
V1ki/dsh-plugin-subscriptions★ 458
Use ChatGPT (Codex), Claude, and Grok subscriptions as DeepSeek Harness LLM providers, with Settings login, model catalogs, usage, plus image_generate, video_generate, and x_search tools.
Mars-Sea/dsh-commandcode-provider★ 388
Unofficial Command Code LLM provider: registers a `commandcode` route with a live model catalog and reasoning-effort support.
cv-superding/dsh-deepseek-web-login★ 265
Adds a deepseek-web provider that uses chat.deepseek.com web models in DSH, with browser login capture, PoW request signing, SSE streaming, and prompting-based tool calls.
FishBottle7/opencode2dsh★ 191
Exposes OpenCode Zen free models to DeepSeek Harness, with no API key.
WSL043/dsh-codex-subscription★ 172
ChatGPT OAuth provider for Codex models with subscription web search, quota and safe reset, image tools, Fast mode, and model-aware context; no API key or Codex CLI.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.