dsh-connect-workbuddy 2.0.10 → 2.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,43 @@
1
1
  # Changelog
2
2
 
3
- ## 2.0.10 (2026-09-26)
3
+ ## 2.0.11 (2026-09-23)
4
+
5
+ ### Bug Fixes
6
+
7
+ - **macOS 上「App 装着、账号也登录着,插件却说你没登录」**(issue #15,真机复现)。WorkBuddy 桌面端 **5.6.0 起**把 auth 文件的 token 字段改为 `$wbEncrypted` 加密信封,**macOS 与 Windows 同样如此**——此前「加密是 Windows 先行」的判断已不成立。而解密的密钥只能向本机 App 现取,于是「能不能找到 App」直接决定了「读不读得出凭据」。
8
+
9
+ - **根因:macOS 可执行文件名是写死的,而且写错了。** `workbuddyAppExecutableCandidates()` 把路径拼成 `<bundle>/Contents/MacOS/WorkBuddy`,但两个真实 bundle(`WorkBuddy.app` / `WorkBuddy AI.app`)的 `CFBundleExecutable` 都是 **`Electron`**——该路径并不存在。于是 `findWorkbuddyAppExecutable()` 返回 undefined,密钥取不到,加密凭据一律读不出来。真机实测:修正后 `doctor` 从 `unavailable` 变为 `available (/Applications/WorkBuddy.app/Contents/MacOS/Electron)`,`keyId=9127dea1b44020a7` 与真实构建一致。
10
+ - **二进制名改为向 bundle 自己问**(新增 `macosBundleExecutable()`,读 `Info.plist` 的 `CFBundleExecutable`)。App 改名、或换个二进制名,都不再需要改插件;plist 缺失或名字为空、含路径分隔符、为 `.`/`..` 时返回 undefined,**绝不猜一个路径**(plist 是磁盘数据,穿越会让 `execFile` 拿到 bundle 之外的东西)。
11
+ - **候选补齐国际版与「App 被归入子目录」两种情况**:新增 `WorkBuddy AI.app`(只装国际版的机器此前零命中),并对 `/Applications` 与 `~/Applications` **向下扫一层**,覆盖 `/Applications/IDE/WorkBuddy.app` 这类整理习惯。扫到的候选必须先用 `CFBundleIdentifier` 确认身份(`com.tencent.workbuddy*` / `com.workbuddy.*`,按点号边界匹配)才会被使用——**每个 Electron 应用的二进制都叫 `Electron`**,只按名字匹配就有可能启动另一个产品;拒绝一个候选只损失一次回退,接受错的那个是在用户机器上跑别的程序。这条有独立测试钉住。
12
+ - **Windows 候选补 `%LOCALAPPDATA%\WorkBuddy\WorkBuddy.exe`**:此前只探 `%LOCALAPPDATA%\Programs\WorkBuddy` 与两个 `Program Files`,装在用户级目录(同源 issue 里就有 `E:\WorkBuddy\WorkBuddy.exe` 这类自定义位置)的机器找不到 App。
13
+ - **macOS 分支改为可注入(`readBundleExecutable`)**:它与 `platform`/`home`/`env` 同一性质——会读真实文件系统,不留缝的话「期望的候选列表」会取决于跑测试的机器恰好装没装 App(本机装了、CI 没装),正是 2.0.6 已经修过的那类红灯。
14
+
15
+ - **「未登录」与「登录信息读不出来」不再共用同一句话**(同一 issue 的用户可见症状)。这两件事在 `readAll()` 之后都表现为「账号数为 0」,此前一律抛 `no signed-in WorkBuddy account found … sign in once in the WorkBuddy desktop app`。但加密文件读不出来时用户**本来就是登录着的**,那句「重新登录一次」指向的是唯一无效的动作——issue #15 报告者的原话正是这句。
16
+
17
+ - 新增 `WorkBuddyEncryptedCredentialError`(以 `ENCRYPTED_CREDENTIAL_CODE` 标记而非 `instanceof`,跨打包边界仍可靠,与 `WorkBuddyCredentialRejectedError` 同一约定):`resolve()` 在**失败路径上**跑一次 `diagnose()`,只要有一个桌面文件归因 `encrypted`,就改抛这条,文案给出「安装桌面 App,或用 `WORKBUDDY_APP_EXECUTABLE` 指定其位置」,并明说重新登录无效。**健康路径一个探针都不跑。**
18
+ - **诊断自身抛错不得顶掉真正的错误**:探测会读盘,失败时退回原来那句通用文案(有测试)。
19
+ - **顺带删掉一句不可达的过滤**:初版写了 `reason === 'encrypted' && source === 'desktop'`,但 `encrypted` 只由 `probeAuthFile()` 赋予,而插件自有副本从不经过它(按自有文档形状解析,损坏时是 `invalid`/`unreadable`)——该条件永远为真。留着它会让代码看起来在防守一件它并没防的事;测试改为直接钉住真正的不变量(自有副本必须归 `invalid`,以及不存在 `source === 'dsh'` 的 `encrypted`),变异验证确认可咬住。
20
+ - **卡片文案同步**:`signedOutNotice()` 让 `encrypted` 抢在通用提示之前(新增 `row.signedOutEncrypted`),因为通用提示说的正是那个无效动作。此前该建议藏在折叠区里的 `searchedEncryptedNotice`,而**段落**仍写着「重新登录一次」——用户多半只读那一段。既然段落已经承担,折叠区里那份重复提示连同其专属样式一并删除(2.0.10 刚立下的「段落说结论、列表给细节」分工,这次是它的延伸)。
21
+ - 优先级:`selectionLost` > `wrong-region` > `encrypted` > 通用提示。`wrong-region` 仍排在前面——那种情况下凭据是可读的,切个标签页即可,比装 App 更省事。
22
+
23
+ ### Tests
24
+
25
+ 测试总数 300 → 316(新增 16 例)。
26
+
27
+ - `tests/at-rest.spec.ts`(+10 例):`macosBundleExecutable` 必须读出 `Electron` 而不是按 App 名猜(**直接钉住本次根因**)、bundle 缺失时返回 undefined 而非猜路径、**四种穿越名(`../Evil` / `..` / `.` / `sub/Bin`)一律拒绝**、无 `CFBundleExecutable` 时返回 undefined;`isWorkbuddyBundle` 认国内版与国际版两个 id、**拒绝另一个 Electron 应用**(含 `com.workbuddyish` / `org.workbuddyevil` 这类仅前缀相似的)、拒绝不可读与无 id 的 bundle;候选列表含国际版与用户级目录、Windows 用户级安装被探到。macOS 相关断言全部走注入的 bundle reader,**套件结果不依赖跑测试的机器装没装 App**。
28
+ - `tests/auth.spec.ts`(+5 例):加密文件 + 取不到密钥必须抛 `WorkBuddyEncryptedCredentialError`、`paths` 指向该文件、文案含 `WORKBUDDY_APP_EXECUTABLE` 且**明确不含**「sign in again」与通用那句(对渲染出的字符串断言,不是对输入);真正没有凭据的机器仍走通用文案;密钥可用时照常解析成功;**自有副本损坏必须归 `invalid` 且不得产生加密结论**;`diagnose()` 抛错时退回通用文案。
29
+ - `tests/searched-paths.spec.ts`(+1 例,改 2 例):加密原因必须在**段落**里被回答、且不回落去回显 `resolve()` 的消息;对渲染后的字符串断言「重新登录」这句话没有以任何形式残留。删掉了 `SearchedView.encrypted` 的两条断言——该字段已随折叠区那份重复提示一并删除(见下)。
30
+
31
+ **变异验证(实测)**:共 8 处定向改写,7 处被测试咬住。**其中 1 处(`source === 'desktop'` 过滤)逃脱**——正因如此才发现它是不可达的死代码并删除,同时把测试改为钉住真正的不变量,再次变异确认可咬住。其余:把 macOS 二进制名改回 `'WorkBuddy'`、从候选里删掉 `WorkBuddy AI.app`、让身份校验恒为 `true`、关掉加密归类、去掉穿越防护、把 `encrypted` 移出段落优先级、调换 `wrong-region` 与 `encrypted` 的次序——逐一挂掉对应用例。
32
+
33
+ **顺带清掉两处「看着像在防守、其实没有消费者」的代码**(与上面那句同源):`describeMissingCredential()` 里不可达的 `source === 'desktop'` 过滤,以及 `SearchedView.encrypted`。后者的消费者是折叠区那份重复提示;提示既已由段落取代,字段留着只会让「列表」与「段落」在下次改动时再次漂移。
34
+
35
+ ### Docs
36
+
37
+ - `README.md` / `README.en.md`:已知限制改口径——加密自 **5.6.0** 起、**macOS 同样在内**;补「二进制名向 bundle 现问」「含国际版 + 子目录扫描 + 身份校验」「未登录 ≠ 读不出来」三条。
38
+ - `docs/DESIGN.md` §6 风险与边界:新增第 6 条,记录本次取证结论与三个决定(向 bundle 问名字、扫子目录但必须校验身份、失败分类下沉到 `resolve()`),并记下那句不可达过滤为何被删。
39
+
40
+ ## 2.0.10 (2026-09-22)
4
41
 
5
42
  ### Bug Fixes
6
43
 
@@ -31,7 +68,7 @@
31
68
 
32
69
  - `docs/DESIGN.md` §3.4:记录「段落说结论、列表给细节」的分工,以及 `wrong-region` 为什么是**发现**而不是**缺席**。
33
70
 
34
- ## 2.0.9 (2026-09-25)
71
+ ## 2.0.9 (2026-09-22)
35
72
 
36
73
  ### Features
37
74
 
@@ -60,7 +97,7 @@
60
97
  - `README.md` / `README.en.md`:功能特性新增「未登录时列出探测过的路径与原因」一条。
61
98
  - `docs/DESIGN.md` §3.4:记录未登录诊断的四档原因划分,以及「`encrypted` 必须独立成档」的理由。
62
99
 
63
- ## 2.0.8 (2026-09-24)
100
+ ## 2.0.8 (2026-09-22)
64
101
 
65
102
  ### Features
66
103
 
@@ -90,7 +127,7 @@
90
127
  - `README.md` / `README.en.md`:「功能特性」新增「可单独关闭任一版本供应商」一条,并在双供应商那条说明关闭后的效果与代价。
91
128
  - `docs/DESIGN.md`:说明区域开关为何必须落在注册层(`replace([])`),以及它与 issue #12「无账号隐藏」的维度差异。
92
129
 
93
- ## 2.0.7 (2026-09-23)
130
+ ## 2.0.7 (2026-09-22)
94
131
 
95
132
  ### Features
96
133
 
@@ -249,6 +286,21 @@
249
286
 
250
287
  - `README.md` / `README.en.md` 的「已知限制 / Known limitations」补充账号失效后的表现与恢复方式,并说明重新登录桌面端无法修复该状态。
251
288
 
289
+ ## 2.0.3 (2026-09-17)
290
+
291
+ ### Fixes
292
+
293
+ - **解析单数 `effort` 形态的推理声明,单数模型不再注册为非推理**(issue #7)。上游 `reasoning` 元数据存在两种拼写:复数 `supportedEfforts` 形态,与单数 `effort` 形态(`deepseek-v4.1-flash`、`kimi-k3`、`gemini-3.5-flash` 等)。原 `parseReasoning` 只读复数,单数形态整档被丢弃,模型被注册为 `reasoning: false`,thinking 永远无法启用。
294
+
295
+ - **实测证实单数 `effort` 是默认档而非唯一档**:两个网关(CN `copilot.tencent.com` / 国际 `www.workbuddy.ai`)均确认——`deepseek-v4.1-flash` 对 `low`/`high`/`max` 全部返回 200 且 `reasoning_content` 各异;`kimi-k3-1` 接受未声明的 `high` 档;不发 `reasoning_effort` 时思考字符数为 0,即上游默认关闭思考。
296
+ - **依据实证在解析层把单数形态折叠为复数**:`supportedEfforts` 展开为全阶梯 `low`/`medium`/`high`/`xhigh`/`max`(复数载荷观测到的词表,`minimal` 从未出现)、`effort` 折入 `defaultEffort`、`canDisableThinking` 视为 `true`(`off` 可用)。
297
+ - **复数形态逐字段透传不受影响**(`hy4-preview` 仍仅 `high` 且锁定 `off`);未识别的单数 `effort` 值按单档透传,交由 adapter 的已知档位过滤。
298
+ - **adapter / 卡片 / 持久化零改动即自动生效**:改动只落在解析层。
299
+
300
+ ### Tests
301
+
302
+ - `tests/upstream.spec.ts` 新增 55 行、`tests/adapter.spec.ts` 新增 20 行,共 6 例:覆盖单数折叠、混合形态优先级、以及线上真实载荷的集成断言。
303
+
252
304
  ## 2.0.2 (2026-09-15)
253
305
 
254
306
  ### Fixes
package/README.en.md CHANGED
@@ -123,7 +123,9 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
123
123
  - 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.
124
124
  - **On Windows, an account change may fail to save while `settings.yaml` is 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** make `set()` 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.
125
125
  - Windows and Linux credential paths are derived from platform conventions and are not verified on real hardware; set `WORKBUDDY_AUTH_FILE` if yours lives elsewhere.
126
- - **Encrypted credentials require the desktop app to be present.** Newer desktop builds 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 (set `WORKBUDDY_APP_EXECUTABLE`), an encrypted file cannot be read — the account then shows as signed out, and the plugin **never** degrades into emitting a truncated token. `doctor` reports 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.
126
+ - **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 (set `WORKBUDDY_APP_EXECUTABLE`), an encrypted file cannot be read — the account then shows as signed out, and the plugin **never** degrades into emitting a truncated token. `doctor` reports 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.
127
+ - **The executable is located from the bundle's own declaration, not guessed from the app name.** The WorkBuddy macOS bundles declare `CFBundleExecutable` as `Electron` (not `WorkBuddy`), so the plugin reads each bundle's `Info.plist` to learn the binary's name. Both the domestic `WorkBuddy.app` and the international `WorkBuddy AI.app` are candidates, and an app filed into a subdirectory of an applications folder (e.g. `/Applications/IDE/WorkBuddy.app`) is found by a one-level scan that confirms each candidate's bundle identifier before use — so a different Electron app can never be launched by mistake.
128
+ - **"Signed out" and "your sign-in cannot be read" are reported separately.** When an encrypted file cannot be read, the advice is to install the desktop app or point `WORKBUDDY_APP_EXECUTABLE` at it — **not** to sign in again, which cannot help: the credential is fine, the key is what is missing.
127
129
 
128
130
  ## Disclaimer
129
131
 
package/README.md CHANGED
@@ -131,7 +131,9 @@ dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
131
131
  - 从 2.0.0 之前的版本升级而来的用户,设置里可能仍留有旧版的顶层 `accountId`(迁移来源)。它只作用于其所属区域,且**该区域被显式清除(`accounts.<区域> = ""`)后即不再生效**——否则清除掉的账号会在下次启动时被静默恢复。卡片会显示每个区域当前是「使用保存的账号」还是「跟随 App 当前登录」:清除后回到的默认账号往往与刚清掉的账号是同一个,没有这行提示就无法分辨清除是否生效。
132
132
  - **Windows:设置文件 `settings.yaml` 被占用时,账号改动可能无法保存**。DSH 用「写临时文件 + 覆盖替换」的方式更新该文件,而杀毒软件、OneDrive 等同步盘或正在打开该文件的编辑器会短暂锁住它;重试耗尽后,写入失败**不会**让 `set()` 报错。插件因此会在写入后回读校验:**确认落盘才显示「已清除」**,失败则给出明确错误并保留原来的选择(不会显示一个旧选择默默反驳的假确认)。遇到该提示时,关闭占用该文件的程序后重试即可。
133
133
  - Windows / Linux 的凭据默认路径为按平台约定推导,未经真机验证;必要时可通过环境变量 `WORKBUDDY_AUTH_FILE` 指定实际位置。
134
- - **加密凭据依赖桌面 App 在场**:新版桌面端加密了 auth 文件里的 token 字段,插件需要调用该 App 自身的原生绑定才能取回字段密钥。因此 App 若未安装、被卸载,或安装在探测路径之外(用 `WORKBUDDY_APP_EXECUTABLE` 指定),加密文件就读不出来——此时账号会显示为未登录,而**不会**退化成读出一个残缺的 token。`doctor` 会报告这项能力是否可用。该密钥是构建期常量,随 App 版本可能轮换;插件每次向本机 App 现取,不缓存到磁盘,所以 App 升级后无需升级插件。
134
+ - **加密凭据依赖桌面 App 在场**:新版桌面端(**5.6.0 起,macOS 与 Windows 同样如此**)加密了 auth 文件里的 token 字段,插件需要调用该 App 自身的原生绑定才能取回字段密钥。因此 App 若未安装、被卸载,或安装在探测路径之外(用 `WORKBUDDY_APP_EXECUTABLE` 指定),加密文件就读不出来——此时账号会显示为未登录,而**不会**退化成读出一个残缺的 token。`doctor` 会报告这项能力是否可用。该密钥是构建期常量,随 App 版本可能轮换;插件每次向本机 App 现取,不缓存到磁盘,所以 App 升级后无需升级插件。
135
+ - **App 可执行文件按 bundle 自身声明定位,不按 App 名猜**:WorkBuddy 的 macOS bundle 里 `CFBundleExecutable` 是 `Electron`(不是 `WorkBuddy`),因此插件读取 bundle 的 `Info.plist` 来决定二进制名。国内版 `WorkBuddy.app` 与国际版 `WorkBuddy AI.app` 都在候选内;App 被归入 `/Applications` 子目录(如 `/Applications/IDE/WorkBuddy.app`)时,插件还会向下扫一层并用 bundle id 确认身份后才使用——**不会**因为同名的其他 Electron 应用而误启动它。
136
+ - **「未登录」与「登录信息读不出来」是两件事**:加密文件读不出时,插件给出的建议是「安装桌面 App / 用 `WORKBUDDY_APP_EXECUTABLE` 指定其位置」,而**不是**「重新登录一次」——后者对这种情况无效(凭据本身是好的,缺的是密钥)。
135
137
 
136
138
  ## 免责声明
137
139
 
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { I as WORKBUDDY_APP_EXECUTABLE_ENV, O as WorkBuddyUpstreamClient, S as legacyWorkbuddyOwnAuthPath, T as workbuddyOwnAuthPath, V as findWorkbuddyAppExecutable, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, g as WorkBuddyCredentialStore, h as WORKBUDDY_AUTH_FILE_ENV, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as defaultDesktopAuthCandidates, y as defaultDesktopAuthDirs } from "./host-heartbeat-DPbL9oi2.js";
2
+ import { O as workbuddyOwnAuthPath, T as legacyWorkbuddyOwnAuthPath, W as findWorkbuddyAppExecutable, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, g as WORKBUDDY_AUTH_FILE_ENV, j as WorkBuddyUpstreamClient, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, x as defaultDesktopAuthDirs, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-BJdDmMSn.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
package/lib/client.js CHANGED
@@ -291,7 +291,6 @@ window.__ModuleLoader__.load({
291
291
  .dsm-workbuddy-searched-reason{color:var(--dsw-alias-label-tertiary,#999);font-size:11px}
292
292
  .dsm-workbuddy-searched-reason-encrypted{color:var(--dsw-alias-state-warning-primary,#e0a13a)}
293
293
  .dsm-workbuddy-searched-reason-wrong-region{color:var(--dsw-alias-brand-primary,#5686fe)}
294
- .dsm-workbuddy-searched-notice{margin:6px 0 0;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:8px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px}
295
294
  .dsm-workbuddy-searched-more{margin-top:6px;padding:0;border:0;background:none;color:var(--dsw-alias-brand-primary,#5686fe);font:inherit;font-size:12px;line-height:18px;cursor:pointer;text-align:left}
296
295
  .dsm-workbuddy-searched-more:hover{text-decoration:underline}
297
296
  .dsm-workbuddy-usage-error{margin:0;font-size:14px;line-height:22px;color:var(--dsw-alias-state-error-primary,#ef4444)}
@@ -386,6 +385,12 @@ window.__ModuleLoader__.load({
386
385
  * Order within each group is preserved from the Host: the store reports
387
386
  * candidates in probe order, which is meaningful (the live file before its
388
387
  * timestamped backups).
388
+ *
389
+ * There is deliberately no `encrypted` flag here. There used to be, for a
390
+ * notice rendered inside the list; that notice is gone because
391
+ * {@link signedOutNotice} now answers the encrypted cause in the paragraph, so
392
+ * the flag would have had no consumer — and a derived field kept "just in
393
+ * case" is how the list and the paragraph drift apart again.
389
394
  */
390
395
  function searchedView(items) {
391
396
  const interesting = items.filter((item) => INTERESTING_REASONS.includes(item.reason));
@@ -393,7 +398,6 @@ window.__ModuleLoader__.load({
393
398
  interesting,
394
399
  missing: items.filter((item) => !INTERESTING_REASONS.includes(item.reason)),
395
400
  missingOpen: interesting.length === 0,
396
- encrypted: items.some((item) => item.reason === "encrypted"),
397
401
  total: items.length
398
402
  };
399
403
  }
@@ -440,10 +444,17 @@ window.__ModuleLoader__.load({
440
444
  *
441
445
  * `selectionLost` still wins outright: there the tokens are healthy and the
442
446
  * advice is to re-pick an account, which no path list can replace.
447
+ *
448
+ * `encrypted` also outranks the generic hint, and for the same reason
449
+ * `wrong-region` does: the generic copy tells the user to sign in again, which
450
+ * is precisely the action that cannot work when the credential exists but is
451
+ * encrypted. Leaving it in the collapsed list meant the headline contradicted
452
+ * the detail below it — and the headline is the only part most users read.
443
453
  */
444
454
  function signedOutNotice(input) {
445
455
  if (input.selectionLost) return { key: "row.selectionLostMessage" };
446
456
  if (input.searched.some((item) => item.reason === "wrong-region")) return { key: "row.signedOutWrongRegion" };
457
+ if (input.searched.some((item) => item.reason === "encrypted")) return { key: "row.signedOutEncrypted" };
447
458
  if (input.searched.length > 0) return { key: "row.signedOutHint" };
448
459
  return input.message === void 0 ? { key: "row.signedOutHint" } : {
449
460
  key: "row.signedOutHint",
@@ -521,9 +532,12 @@ window.__ModuleLoader__.load({
521
532
  * buried under a dozen "not found" lines. When nothing interesting was found
522
533
  * the absent list IS the explanation, so it opens directly.
523
534
  *
524
- * An `encrypted` failure additionally raises a notice: it is the one cause
525
- * where the user is very likely already signed in, and telling them to sign in
526
- * again sends them to an action that cannot work.
535
+ * An `encrypted` failure deliberately adds NO notice of its own. It used to,
536
+ * because the paragraph above still said "sign in once in the desktop app" —
537
+ * the one action that cannot work when the credential exists but is encrypted.
538
+ * `signedOutNotice` now answers that cause in the paragraph itself, so a second
539
+ * copy here would restate it: the paragraph states the situation, this list
540
+ * supplies the detail, and the per-entry label already says what each path is.
527
541
  */
528
542
  function SearchedPaths({ items, t }) {
529
543
  const view = searchedView(items);
@@ -542,11 +556,6 @@ window.__ModuleLoader__.load({
542
556
  className: "dsm-workbuddy-searched-hint",
543
557
  children: t("row.searchedHint")
544
558
  }),
545
- view.encrypted ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
546
- className: "dsm-workbuddy-searched-notice",
547
- role: "status",
548
- children: t("row.searchedEncryptedNotice")
549
- }) : null,
550
559
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
551
560
  className: "dsm-workbuddy-searched-list",
552
561
  children: [view.interesting.map((item) => renderSearchedItem(item, t)), showMissing ? view.missing.map((item) => renderSearchedItem(item, t)) : null]
@@ -1395,6 +1404,7 @@ window.__ModuleLoader__.load({
1395
1404
  "row.signedOut": "Not signed in",
1396
1405
  "row.signedOutHint": "Sign in once in the WorkBuddy desktop app; this plugin follows that sign-in automatically.",
1397
1406
  "row.signedOutWrongRegion": "Your WorkBuddy sign-in was found, but it belongs to the other region tab. Switch tabs above to use it — there is nothing to sign in again.",
1407
+ "row.signedOutEncrypted": "Your WorkBuddy sign-in is present, but its token fields are encrypted and the plugin could not read the key. Install the WorkBuddy desktop app, or set WORKBUDDY_APP_EXECUTABLE to its executable — signing in again will not change this.",
1398
1408
  "row.searchedTitle": "Paths checked",
1399
1409
  "row.searchedHint": "If your WorkBuddy keeps its login somewhere else, point the plugin at it with the WORKBUDDY_AUTH_FILE environment variable.",
1400
1410
  "row.reasonMissing": "not found",
@@ -1404,7 +1414,6 @@ window.__ModuleLoader__.load({
1404
1414
  "row.reasonWrongRegion": "belongs to the other region",
1405
1415
  "row.sourceDesktop": "desktop app",
1406
1416
  "row.sourceDsh": "plugin copy",
1407
- "row.searchedEncryptedNotice": "The credential file is there, but its token fields are encrypted and the key could not be read. Install the WorkBuddy desktop app, or set WORKBUDDY_APP_EXECUTABLE to its executable — signing in again on its own will not help.",
1408
1417
  "row.searchedMore": "Show the {count} path(s) that were not found",
1409
1418
  "row.signedIn": "Signed in: {accountName}",
1410
1419
  "row.tokenExpiry": "Access token expires {expiresAt} (auto-renewal)",
@@ -1470,6 +1479,7 @@ window.__ModuleLoader__.load({
1470
1479
  "row.signedOut": "未登录",
1471
1480
  "row.signedOutHint": "在 WorkBuddy 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
1472
1481
  "row.signedOutWrongRegion": "找到了你的 WorkBuddy 登录信息,但它属于另一个地区的标签页。切换到上方对应标签页即可使用——不需要重新登录。",
1482
+ "row.signedOutEncrypted": "你的 WorkBuddy 登录信息在,但其中的 token 字段是加密的,插件拿不到密钥。请安装 WorkBuddy 桌面 App,或用环境变量 WORKBUDDY_APP_EXECUTABLE 指定它的可执行文件——重新登录无法解决这个问题。",
1473
1483
  "row.searchedTitle": "已检查的路径",
1474
1484
  "row.searchedHint": "如果你的 WorkBuddy 把登录信息存在别处,可以用环境变量 WORKBUDDY_AUTH_FILE 指定。",
1475
1485
  "row.reasonMissing": "不存在",
@@ -1479,7 +1489,6 @@ window.__ModuleLoader__.load({
1479
1489
  "row.reasonWrongRegion": "属于另一个地区",
1480
1490
  "row.sourceDesktop": "桌面 App",
1481
1491
  "row.sourceDsh": "插件副本",
1482
- "row.searchedEncryptedNotice": "凭据文件在,但其中的 token 字段是加密的,插件拿不到密钥。请安装 WorkBuddy 桌面 App,或用环境变量 WORKBUDDY_APP_EXECUTABLE 指定它的可执行文件——单纯重新登录解决不了这个问题。",
1483
1492
  "row.searchedMore": "显示另外 {count} 条「不存在」的路径",
1484
1493
  "row.signedIn": "已登录:{accountName}",
1485
1494
  "row.tokenExpiry": "访问令牌 {expiresAt} 过期(自动续期)",
@@ -5,14 +5,13 @@ import { dirname, join } from "node:path";
5
5
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
6
6
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
7
7
  import { execFile, execFileSync } from "node:child_process";
8
- import { existsSync } from "node:fs";
8
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
9
9
  //#region src/at-rest.ts
10
10
  /**
11
11
  * WorkBuddy desktop "at-rest" credential decryption.
12
12
  *
13
- * The WorkBuddy desktop app (2.x, Windows builds first) no longer stores
14
- * `auth.accessToken` / `auth.refreshToken` as plain strings. It now writes a
15
- * field wrapper:
13
+ * From 5.6.0 the WorkBuddy desktop app no longer stores `auth.accessToken` /
14
+ * `auth.refreshToken` as plain strings. It writes a field wrapper:
16
15
  *
17
16
  * { "$wbEncrypted": 1, "envelope": "<base64 of a JSON envelope>" }
18
17
  *
@@ -33,6 +32,16 @@ import { existsSync } from "node:fs";
33
32
  * payload, and caches it in memory for the process lifetime. Nothing is ever
34
33
  * written to disk, and the payload is never logged.
35
34
  *
35
+ * 改动:**「macOS 也加密」这一事实**(issue #15 真机取证)。本模块原先假设该
36
+ * policy 是 Windows 先行、macOS 只是「将来可能」,于是 macOS 的可执行文件
37
+ * 路径用 App 名拼成 `<bundle>/Contents/MacOS/WorkBuddy`——而两个真实 bundle
38
+ * 的 `CFBundleExecutable` 都是 `Electron`,该路径并不存在。结果是 macOS 上
39
+ * 加密凭据**永远**取不到密钥,用户却被报成「未登录」。现在二进制名向 bundle
40
+ * 自己问(`macosBundleExecutable()`),候选含国际版 `WorkBuddy AI.app`,
41
+ * 并允许 App 被归入 applications 目录的子目录——扫到的候选必须先用
42
+ * `CFBundleIdentifier` 确认身份才 `execFile`,因为**每个 Electron 应用的
43
+ * 二进制都叫 `Electron`**,只按名字匹配就可能启动另一个产品。
44
+ *
36
45
  * @module dsh-connect-workbuddy/at-rest
37
46
  */
38
47
  /** Envelope framing names, mapped to the single-byte AAD framing code. */
@@ -57,8 +66,19 @@ const SYMMETRIC_SCHEME = "sym-v1";
57
66
  const WORKBUDDY_APP_EXECUTABLE_ENV = "WORKBUDDY_APP_EXECUTABLE";
58
67
  /** How long the app is given to answer with its key payload. */
59
68
  const KEY_FETCH_TIMEOUT_MS = 1e4;
60
- /** File name of the WorkBuddy desktop executable, per platform. */
69
+ /** File name of the WorkBuddy desktop executable on Windows. */
61
70
  const APP_EXECUTABLE_NAME = "WorkBuddy.exe";
71
+ /**
72
+ * macOS bundles the desktop app may be installed as, in probe order.
73
+ *
74
+ * `WorkBuddy.app` is the domestic build; `WorkBuddy AI.app` is the
75
+ * international one, and a machine may carry either or both. The user-level
76
+ * `~/Applications` location is included because macOS lets an app live there,
77
+ * and installs have been observed under a subdirectory of /Applications too —
78
+ * hence {@link findWorkbuddyAppExecutable}'s parent scan, which covers those
79
+ * without guessing any particular folder name.
80
+ */
81
+ const MACOS_APP_BUNDLE_NAMES = ["WorkBuddy.app", "WorkBuddy AI.app"];
62
82
  function encodeUint32(value) {
63
83
  const bytes = Buffer.allocUnsafe(4);
64
84
  bytes.writeUInt32BE(value);
@@ -156,24 +176,127 @@ function openEncryptedField(field, key) {
156
176
  return Buffer.concat([decipher.update(Buffer.from(ciphertext, "base64")), decipher.final()]).toString("utf8");
157
177
  }
158
178
  /**
179
+ * The executable inside a macOS app bundle, read from the bundle's own
180
+ * `Info.plist`.
181
+ *
182
+ * The binary is NOT reliably named after the app: the WorkBuddy bundles ship
183
+ * with `CFBundleExecutable` set to `Electron`, so a path assembled as
184
+ * `<bundle>/Contents/MacOS/WorkBuddy` does not exist and the app looks absent
185
+ * even when it is installed in the default location. Because the bundle
186
+ * documents the real name, asking it is both correct and robust to a future
187
+ * build that renames the binary.
188
+ *
189
+ * Returns undefined when the plist is absent, unreadable, or carries no usable
190
+ * name — never a guessed path, so a caller can keep probing.
191
+ */
192
+ function macosBundleExecutable(bundle) {
193
+ let plist;
194
+ try {
195
+ plist = readFileSync(join(bundle, "Contents", "Info.plist"), "utf8");
196
+ } catch {
197
+ return;
198
+ }
199
+ const name = /<key>\s*CFBundleExecutable\s*<\/key>\s*<string>([^<]*)<\/string>/u.exec(plist)?.[1]?.trim();
200
+ if (name === void 0 || name === "" || name.includes("/") || name.includes("\\") || name === "." || name === "..") return;
201
+ return join(bundle, "Contents", "MacOS", name);
202
+ }
203
+ /**
159
204
  * Candidate paths of the WorkBuddy desktop executable, in probe order.
160
205
  *
161
206
  * The Windows build is the one that encrypts credentials, so Windows leads;
162
- * the macOS bundle is listed because the same native module ships there and a
163
- * future build may enable the policy, and `undefined` entries (an unset env
164
- * variable) are dropped.
207
+ * the macOS bundles are listed because the same native module ships there and
208
+ * the encryption policy is enabled on macOS builds too (observed from 5.6.x),
209
+ * and `undefined` entries (an unset env variable) are dropped.
210
+ *
211
+ * On macOS the executable name comes from each bundle (see
212
+ * {@link macosBundleExecutable}) rather than being assembled from the app name.
213
+ *
214
+ * `readBundleExecutable` is injectable, in the same spirit as `platform`/`home`/
215
+ * `env`: the macOS branch consults the real filesystem, so without a seam the
216
+ * expected candidates would depend on whether the host machine happens to have
217
+ * the app installed — and the test would pass on a developer's Mac while
218
+ * failing in CI.
165
219
  */
166
- function workbuddyAppExecutableCandidates(platform = process.platform, home = homedir(), env = process.env) {
220
+ function workbuddyAppExecutableCandidates(platform = process.platform, home = homedir(), env = process.env, readBundleExecutable = macosBundleExecutable) {
167
221
  const candidates = [env[WORKBUDDY_APP_EXECUTABLE_ENV]?.trim()];
168
222
  if (platform === "win32") {
169
223
  const local = env["LOCALAPPDATA"]?.trim();
170
224
  const programFiles = env["ProgramFiles"]?.trim();
171
225
  const programFilesX86 = env["ProgramFiles(x86)"]?.trim();
172
- candidates.push(local === void 0 || local === "" ? void 0 : join(local, "Programs", "WorkBuddy", APP_EXECUTABLE_NAME), programFiles === void 0 || programFiles === "" ? void 0 : join(programFiles, "WorkBuddy", APP_EXECUTABLE_NAME), programFilesX86 === void 0 || programFilesX86 === "" ? void 0 : join(programFilesX86, "WorkBuddy", APP_EXECUTABLE_NAME));
173
- } else if (platform === "darwin") candidates.push("/Applications/WorkBuddy.app/Contents/MacOS/WorkBuddy", join(home, "Applications", "WorkBuddy.app", "Contents", "MacOS", "WorkBuddy"));
226
+ candidates.push(local === void 0 || local === "" ? void 0 : join(local, "Programs", "WorkBuddy", APP_EXECUTABLE_NAME), local === void 0 || local === "" ? void 0 : join(local, "WorkBuddy", APP_EXECUTABLE_NAME), programFiles === void 0 || programFiles === "" ? void 0 : join(programFiles, "WorkBuddy", APP_EXECUTABLE_NAME), programFilesX86 === void 0 || programFilesX86 === "" ? void 0 : join(programFilesX86, "WorkBuddy", APP_EXECUTABLE_NAME));
227
+ } else if (platform === "darwin") for (const name of MACOS_APP_BUNDLE_NAMES) candidates.push(readBundleExecutable(join("/Applications", name)), readBundleExecutable(join(home, "Applications", name)));
174
228
  return candidates.filter((candidate) => candidate !== void 0 && candidate !== "");
175
229
  }
176
230
  /**
231
+ * Bundle identifier PREFIXES the desktop app is signed with — `com.tencent.
232
+ * workbuddy` (domestic, observed as `…workbuddy.mac`) and `com.workbuddy`
233
+ * (international, observed as `com.workbuddy.workbuddy-ai`).
234
+ *
235
+ * Used to CONFIRM that a discovered bundle really is WorkBuddy before it is
236
+ * launched. This matters because the discovery below scans directories and then
237
+ * execs what it finds: every Electron app is built around a binary called
238
+ * `Electron`, so a name-only match could pick a different product's bundle and
239
+ * run it. The identifier is the app's own claim about itself, so it is the
240
+ * check that makes the scan safe.
241
+ */
242
+ const APP_BUNDLE_IDENTIFIER_PREFIXES = ["com.tencent.workbuddy", "com.workbuddy"];
243
+ /**
244
+ * Whether a bundle identifies itself as the WorkBuddy desktop app.
245
+ *
246
+ * The match is on dot boundaries, so a hypothetical `com.workbuddyish` cannot
247
+ * pass as `com.workbuddy`.
248
+ *
249
+ * An unreadable or identifier-less plist is treated as NOT WorkBuddy: refusing
250
+ * a candidate only costs a fallback to another path, whereas accepting the
251
+ * wrong one would execute an unrelated application.
252
+ */
253
+ function isWorkbuddyBundle(bundle) {
254
+ let plist;
255
+ try {
256
+ plist = readFileSync(join(bundle, "Contents", "Info.plist"), "utf8");
257
+ } catch {
258
+ return false;
259
+ }
260
+ const identifier = /<key>\s*CFBundleIdentifier\s*<\/key>\s*<string>([^<]*)<\/string>/u.exec(plist)?.[1]?.trim().toLowerCase();
261
+ if (identifier === void 0 || identifier === "") return false;
262
+ return APP_BUNDLE_IDENTIFIER_PREFIXES.some((prefix) => identifier === prefix || identifier.startsWith(`${prefix}.`));
263
+ }
264
+ /**
265
+ * Bundles of the desktop app found one level BELOW a macOS applications
266
+ * directory.
267
+ *
268
+ * Users do file apps into subfolders (`/Applications/IDE/WorkBuddy.app`), and
269
+ * a hardcoded `/Applications/<name>` then reports the app as missing while it
270
+ * is installed and signed in. The scan is deliberately ONE level deep and
271
+ * matches the known bundle names only, so it stays predictable and cheap; each
272
+ * candidate is then confirmed by {@link isWorkbuddyBundle} before use.
273
+ *
274
+ * Returns [] when the parent is absent or unreadable — a missing directory is
275
+ * the normal case, not an error.
276
+ */
277
+ function macosNestedAppBundles(parent) {
278
+ let entries;
279
+ try {
280
+ entries = readdirSync(parent);
281
+ } catch {
282
+ return [];
283
+ }
284
+ const bundles = [];
285
+ for (const entry of entries) {
286
+ const nested = join(parent, entry);
287
+ for (const name of MACOS_APP_BUNDLE_NAMES) {
288
+ const bundle = join(nested, name);
289
+ try {
290
+ if (!statSync(bundle).isDirectory()) continue;
291
+ } catch {
292
+ continue;
293
+ }
294
+ if (isWorkbuddyBundle(bundle)) bundles.push(bundle);
295
+ }
296
+ }
297
+ return bundles;
298
+ }
299
+ /**
177
300
  * The first candidate that exists as a file, or undefined when the desktop app
178
301
  * is not installed where this platform expects it.
179
302
  */
@@ -181,6 +304,13 @@ function findWorkbuddyAppExecutable(platform = process.platform, home = homedir(
181
304
  for (const candidate of workbuddyAppExecutableCandidates(platform, home, env)) try {
182
305
  if (existsSync(candidate)) return candidate;
183
306
  } catch {}
307
+ if (platform === "darwin") for (const parent of ["/Applications", join(home, "Applications")]) for (const bundle of macosNestedAppBundles(parent)) {
308
+ const executable = macosBundleExecutable(bundle);
309
+ if (executable === void 0) continue;
310
+ try {
311
+ if (existsSync(executable)) return executable;
312
+ } catch {}
313
+ }
184
314
  }
185
315
  /**
186
316
  * Ask the installed desktop app for its key payload by running its own binary
@@ -905,9 +1035,52 @@ var WorkBuddyUpstreamClient = class {
905
1035
  * 一个 store,账号、刷新、选择完全隔离;插件自有刷新副本也按区域分
906
1036
  * 文件(`.workbuddy-auth.<region>.json`),双账号同时在线互不覆盖,
907
1037
  * 旧的单文件 `.workbuddy-auth.json` 作为迁移源保留读取。
1038
+ * 另(issue #15):`resolve()` 不再把「没登录」与「有登录但凭据读不出来」
1039
+ * 合并成同一句话。后者(加密字段 + 取不到密钥)改为抛
1040
+ * `WorkBuddyEncryptedCredentialError`,给出「装 App / 用
1041
+ * WORKBUDDY_APP_EXECUTABLE 指定」这条**可执行**的建议——对已登录的用户说
1042
+ * 「请重新登录一次」指向的是唯一无效的动作。判定只在失败路径上跑一次
1043
+ * `diagnose()`,健康路径不付任何代价。
908
1044
  *
909
1045
  * @module dsh-connect-workbuddy/auth
910
1046
  */
1047
+ /**
1048
+ * Marker for a credential that EXISTS but could not be decrypted.
1049
+ *
1050
+ * Identified by {@link ENCRYPTED_CREDENTIAL_CODE} rather than `instanceof`, so
1051
+ * the check keeps working when the thrower and the caller end up in different
1052
+ * module instances (bundled host half vs. a test's source import) — the same
1053
+ * convention `WorkBuddyCredentialRejectedError` uses.
1054
+ */
1055
+ const ENCRYPTED_CREDENTIAL_CODE = "WORKBUDDY_ENCRYPTED_CREDENTIAL";
1056
+ /**
1057
+ * The user IS signed in, but the credential file cannot be read.
1058
+ *
1059
+ * This exists because "signed out" and "signed in with an unreadable credential"
1060
+ * resolve to the same observable state — zero accounts — and were therefore
1061
+ * reported with the same message. That message told a correctly signed-in user
1062
+ * to sign in again, which cannot possibly help: from WorkBuddy 5.6.x the desktop
1063
+ * app encrypts its token fields, and reading them requires asking the installed
1064
+ * app for its key. If that app cannot be located, the fix is to point the plugin
1065
+ * at it (`WORKBUDDY_APP_EXECUTABLE`), never to sign in again.
1066
+ *
1067
+ * No token material is carried: only the paths and the environment variable.
1068
+ */
1069
+ var WorkBuddyEncryptedCredentialError = class extends Error {
1070
+ code = ENCRYPTED_CREDENTIAL_CODE;
1071
+ /** Files that hold an encrypted credential, for the user to recognize. */
1072
+ paths;
1073
+ constructor(paths) {
1074
+ const where = paths.length > 0 ? paths.join(", ") : "the credential file";
1075
+ super(`workbuddy: your WorkBuddy sign-in is present but encrypted, so it cannot be read (${where}). The WorkBuddy desktop app must be present to hand over its key; install it, or set ${WORKBUDDY_APP_EXECUTABLE_ENV} to its executable if it lives elsewhere. Signing in again will not change this.`);
1076
+ this.name = "WorkBuddyEncryptedCredentialError";
1077
+ this.paths = paths;
1078
+ }
1079
+ };
1080
+ /** Whether an error reports an unreadable (encrypted) credential. */
1081
+ function isEncryptedCredentialError(value) {
1082
+ return typeof value === "object" && value !== null && value.code === "WORKBUDDY_ENCRYPTED_CREDENTIAL";
1083
+ }
911
1084
  /** Legacy single-copy basename (pre-dual-provider); kept as migration source. */
912
1085
  const WORKBUDDY_AUTH_FILENAME = ".workbuddy-auth.json";
913
1086
  /** Env variable that overrides the desktop auth-file location. */
@@ -1510,17 +1683,38 @@ var WorkBuddyCredentialStore = class {
1510
1683
  /** The credential to send upstream: {@link current}, refreshed on demand. */
1511
1684
  async resolve() {
1512
1685
  const credential = await this.current();
1513
- if (credential === void 0) {
1514
- const candidates = this.resolveDesktopCandidates();
1515
- const desktop = candidates.length > 0 ? candidates.join(" or ") : "(no desktop path on this platform)";
1516
- throw new Error(`workbuddy: no signed-in WorkBuddy account found; sign in once in the WorkBuddy desktop app (expected ${desktop} or ${WORKBUDDY_AUTH_FILE_ENV}), or refresh an existing session`);
1517
- }
1686
+ if (credential === void 0) throw await this.describeMissingCredential();
1518
1687
  if (!this.needsRefresh(credential)) return credential;
1519
1688
  this.inflight ??= this.refreshNow(credential).finally(() => {
1520
1689
  this.inflight = void 0;
1521
1690
  });
1522
1691
  return this.inflight;
1523
1692
  }
1693
+ /**
1694
+ * WHY no credential resolved, as the error to throw.
1695
+ *
1696
+ * `resolve()` seeing zero accounts has two very different causes, and the
1697
+ * generic "sign in once" message is only correct for one of them. When a
1698
+ * probe finds a credential file whose fields are encrypted and the key could
1699
+ * not be obtained, the user is signed in and re-authenticating cannot help —
1700
+ * so that case gets {@link WorkBuddyEncryptedCredentialError} instead, naming
1701
+ * the file and the environment variable that actually fixes it.
1702
+ *
1703
+ * The probe is only run on the failure path, so the healthy case pays nothing.
1704
+ * A probe that itself throws must not replace the real error, hence the
1705
+ * fallback to the generic message.
1706
+ */
1707
+ async describeMissingCredential() {
1708
+ let failures = [];
1709
+ try {
1710
+ failures = (await this.diagnose()).failures;
1711
+ } catch {}
1712
+ const encrypted = failures.filter((failure) => failure.reason === "encrypted").map((failure) => failure.path);
1713
+ if (encrypted.length > 0) return new WorkBuddyEncryptedCredentialError(encrypted);
1714
+ const candidates = this.resolveDesktopCandidates();
1715
+ const desktop = candidates.length > 0 ? candidates.join(" or ") : "(no desktop path on this platform)";
1716
+ return /* @__PURE__ */ new Error(`workbuddy: no signed-in WorkBuddy account found; sign in once in the WorkBuddy desktop app (expected ${desktop} or ${WORKBUDDY_AUTH_FILE_ENV}), or refresh an existing session`);
1717
+ }
1524
1718
  /** Read-only sign-in summary; never refreshes and never throws. */
1525
1719
  async status() {
1526
1720
  try {
@@ -2014,7 +2208,7 @@ var WorkBuddyCatalog = class {
2014
2208
  //#endregion
2015
2209
  //#region src/version.ts
2016
2210
  /** The npm package version this build was produced from. */
2017
- const WORKBUDDY_CONNECT_VERSION = "2.0.10";
2211
+ const WORKBUDDY_CONNECT_VERSION = "2.0.11";
2018
2212
  //#endregion
2019
2213
  //#region src/host-heartbeat.ts
2020
2214
  /**
@@ -2120,4 +2314,4 @@ async function clearHostHeartbeat() {
2120
2314
  await rm(workbuddyHostHeartbeatPath(), { force: true });
2121
2315
  }
2122
2316
  //#endregion
2123
- export { isCredentialRejectedError as A, fetchAtRestKeyPayload as B, parseWorkBuddyAuth as C, WorkBuddyCredentialRejectedError as D, CREDENTIAL_REJECTED_CODE as E, regionOf as F, workbuddyAppExecutableCandidates as G, isEncryptedFieldWrapper as H, WORKBUDDY_APP_EXECUTABLE_ENV as I, clearAtRestKeyCache as L, parseReasoning as M, parseUpstreamModel as N, WorkBuddyUpstreamClient as O, prepareChatBody as P, deriveAtRestKey as R, legacyWorkbuddyOwnAuthPath as S, workbuddyOwnAuthPath as T, openEncryptedField as U, findWorkbuddyAppExecutable as V, readAtRestKey as W, authFileName as _, readHostHeartbeat as a, defaultDesktopAuthPath as b, WORKBUDDY_CONNECT_VERSION as c, WorkBuddyCatalog as d, deriveCatalog as f, WorkBuddyCredentialStore as g, WORKBUDDY_AUTH_FILE_ENV as h, processStartTimeMs as i, parseCreditMultiplier as j, classifyUpstreamError as k, FALLBACK_WORKBUDDY_MODELS as l, WORKBUDDY_AUTH_FILENAME as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, fallbackModelsFor as p, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, FALLBACK_WORKBUDDY_MODELS_GLOBAL as u, defaultDesktopAuthCandidates as v, workbuddyAccountId as w, hasEncryptedCredentialFields as x, defaultDesktopAuthDirs as y, deriveAtRestKeyId as z };
2317
+ export { WorkBuddyCredentialRejectedError as A, clearAtRestKeyCache as B, hasEncryptedCredentialFields as C, workbuddyAccountId as D, parseWorkBuddyAuth as E, parseReasoning as F, isEncryptedFieldWrapper as G, deriveAtRestKeyId as H, parseUpstreamModel as I, macosNestedAppBundles as J, isWorkbuddyBundle as K, prepareChatBody as L, classifyUpstreamError as M, isCredentialRejectedError as N, workbuddyOwnAuthPath as O, parseCreditMultiplier as P, regionOf as R, defaultDesktopAuthPath as S, legacyWorkbuddyOwnAuthPath as T, fetchAtRestKeyPayload as U, deriveAtRestKey as V, findWorkbuddyAppExecutable as W, readAtRestKey as X, openEncryptedField as Y, workbuddyAppExecutableCandidates as Z, WorkBuddyCredentialStore as _, readHostHeartbeat as a, defaultDesktopAuthCandidates as b, WORKBUDDY_CONNECT_VERSION as c, WorkBuddyCatalog as d, deriveCatalog as f, WORKBUDDY_AUTH_FILE_ENV as g, WORKBUDDY_AUTH_FILENAME as h, processStartTimeMs as i, WorkBuddyUpstreamClient as j, CREDENTIAL_REJECTED_CODE as k, FALLBACK_WORKBUDDY_MODELS as l, ENCRYPTED_CREDENTIAL_CODE as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, fallbackModelsFor as p, macosBundleExecutable as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, FALLBACK_WORKBUDDY_MODELS_GLOBAL as u, WorkBuddyEncryptedCredentialError as v, isEncryptedCredentialError as w, defaultDesktopAuthDirs as x, authFileName as y, WORKBUDDY_APP_EXECUTABLE_ENV as z };
package/lib/index.d.ts CHANGED
@@ -65,6 +65,36 @@ interface WorkBuddyAuthStatus {
65
65
  domain?: string;
66
66
  source?: 'desktop' | 'dsh';
67
67
  }
68
+ /**
69
+ * Marker for a credential that EXISTS but could not be decrypted.
70
+ *
71
+ * Identified by {@link ENCRYPTED_CREDENTIAL_CODE} rather than `instanceof`, so
72
+ * the check keeps working when the thrower and the caller end up in different
73
+ * module instances (bundled host half vs. a test's source import) — the same
74
+ * convention `WorkBuddyCredentialRejectedError` uses.
75
+ */
76
+ declare const ENCRYPTED_CREDENTIAL_CODE = "WORKBUDDY_ENCRYPTED_CREDENTIAL";
77
+ /**
78
+ * The user IS signed in, but the credential file cannot be read.
79
+ *
80
+ * This exists because "signed out" and "signed in with an unreadable credential"
81
+ * resolve to the same observable state — zero accounts — and were therefore
82
+ * reported with the same message. That message told a correctly signed-in user
83
+ * to sign in again, which cannot possibly help: from WorkBuddy 5.6.x the desktop
84
+ * app encrypts its token fields, and reading them requires asking the installed
85
+ * app for its key. If that app cannot be located, the fix is to point the plugin
86
+ * at it (`WORKBUDDY_APP_EXECUTABLE`), never to sign in again.
87
+ *
88
+ * No token material is carried: only the paths and the environment variable.
89
+ */
90
+ declare class WorkBuddyEncryptedCredentialError extends Error {
91
+ readonly code = "WORKBUDDY_ENCRYPTED_CREDENTIAL";
92
+ /** Files that hold an encrypted credential, for the user to recognize. */
93
+ readonly paths: readonly string[];
94
+ constructor(paths: readonly string[]);
95
+ }
96
+ /** Whether an error reports an unreadable (encrypted) credential. */
97
+ declare function isEncryptedCredentialError(value: unknown): value is WorkBuddyEncryptedCredentialError;
68
98
  /** Constructor options; only {@link refresh} is required. */
69
99
  interface WorkBuddyStoreOptions {
70
100
  /** Explicit desktop auth-file path, overriding env and platform defaults. */
@@ -328,6 +358,21 @@ declare class WorkBuddyCredentialStore {
328
358
  credentialFor(accountId: string): Promise<WorkBuddyCredential | undefined>;
329
359
  /** The credential to send upstream: {@link current}, refreshed on demand. */
330
360
  resolve(): Promise<WorkBuddyCredential>;
361
+ /**
362
+ * WHY no credential resolved, as the error to throw.
363
+ *
364
+ * `resolve()` seeing zero accounts has two very different causes, and the
365
+ * generic "sign in once" message is only correct for one of them. When a
366
+ * probe finds a credential file whose fields are encrypted and the key could
367
+ * not be obtained, the user is signed in and re-authenticating cannot help —
368
+ * so that case gets {@link WorkBuddyEncryptedCredentialError} instead, naming
369
+ * the file and the environment variable that actually fixes it.
370
+ *
371
+ * The probe is only run on the failure path, so the healthy case pays nothing.
372
+ * A probe that itself throws must not replace the real error, hence the
373
+ * fallback to the generic message.
374
+ */
375
+ private describeMissingCredential;
331
376
  /** Read-only sign-in summary; never refreshes and never throws. */
332
377
  status(): Promise<WorkBuddyAuthStatus>;
333
378
  /**
@@ -753,9 +798,8 @@ declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkB
753
798
  /**
754
799
  * WorkBuddy desktop "at-rest" credential decryption.
755
800
  *
756
- * The WorkBuddy desktop app (2.x, Windows builds first) no longer stores
757
- * `auth.accessToken` / `auth.refreshToken` as plain strings. It now writes a
758
- * field wrapper:
801
+ * From 5.6.0 the WorkBuddy desktop app no longer stores `auth.accessToken` /
802
+ * `auth.refreshToken` as plain strings. It writes a field wrapper:
759
803
  *
760
804
  * { "$wbEncrypted": 1, "envelope": "<base64 of a JSON envelope>" }
761
805
  *
@@ -776,6 +820,16 @@ declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkB
776
820
  * payload, and caches it in memory for the process lifetime. Nothing is ever
777
821
  * written to disk, and the payload is never logged.
778
822
  *
823
+ * 改动:**「macOS 也加密」这一事实**(issue #15 真机取证)。本模块原先假设该
824
+ * policy 是 Windows 先行、macOS 只是「将来可能」,于是 macOS 的可执行文件
825
+ * 路径用 App 名拼成 `<bundle>/Contents/MacOS/WorkBuddy`——而两个真实 bundle
826
+ * 的 `CFBundleExecutable` 都是 `Electron`,该路径并不存在。结果是 macOS 上
827
+ * 加密凭据**永远**取不到密钥,用户却被报成「未登录」。现在二进制名向 bundle
828
+ * 自己问(`macosBundleExecutable()`),候选含国际版 `WorkBuddy AI.app`,
829
+ * 并允许 App 被归入 applications 目录的子目录——扫到的候选必须先用
830
+ * `CFBundleIdentifier` 确认身份才 `execFile`,因为**每个 Electron 应用的
831
+ * 二进制都叫 `Electron`**,只按名字匹配就可能启动另一个产品。
832
+ *
779
833
  * @module dsh-connect-workbuddy/at-rest
780
834
  */
781
835
  /** A `{$wbEncrypted:1,envelope}` field wrapper, the only shape this module opens. */
@@ -809,15 +863,64 @@ declare function deriveAtRestKey(payloadJson: string): Buffer;
809
863
  * key is wrong, and it must never degrade into a truncated token.
810
864
  */
811
865
  declare function openEncryptedField(field: WorkBuddyEncryptedField, key: Buffer): string;
866
+ /**
867
+ * The executable inside a macOS app bundle, read from the bundle's own
868
+ * `Info.plist`.
869
+ *
870
+ * The binary is NOT reliably named after the app: the WorkBuddy bundles ship
871
+ * with `CFBundleExecutable` set to `Electron`, so a path assembled as
872
+ * `<bundle>/Contents/MacOS/WorkBuddy` does not exist and the app looks absent
873
+ * even when it is installed in the default location. Because the bundle
874
+ * documents the real name, asking it is both correct and robust to a future
875
+ * build that renames the binary.
876
+ *
877
+ * Returns undefined when the plist is absent, unreadable, or carries no usable
878
+ * name — never a guessed path, so a caller can keep probing.
879
+ */
880
+ declare function macosBundleExecutable(bundle: string): string | undefined;
812
881
  /**
813
882
  * Candidate paths of the WorkBuddy desktop executable, in probe order.
814
883
  *
815
884
  * The Windows build is the one that encrypts credentials, so Windows leads;
816
- * the macOS bundle is listed because the same native module ships there and a
817
- * future build may enable the policy, and `undefined` entries (an unset env
818
- * variable) are dropped.
885
+ * the macOS bundles are listed because the same native module ships there and
886
+ * the encryption policy is enabled on macOS builds too (observed from 5.6.x),
887
+ * and `undefined` entries (an unset env variable) are dropped.
888
+ *
889
+ * On macOS the executable name comes from each bundle (see
890
+ * {@link macosBundleExecutable}) rather than being assembled from the app name.
891
+ *
892
+ * `readBundleExecutable` is injectable, in the same spirit as `platform`/`home`/
893
+ * `env`: the macOS branch consults the real filesystem, so without a seam the
894
+ * expected candidates would depend on whether the host machine happens to have
895
+ * the app installed — and the test would pass on a developer's Mac while
896
+ * failing in CI.
897
+ */
898
+ declare function workbuddyAppExecutableCandidates(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv, readBundleExecutable?: (bundle: string) => string | undefined): string[];
899
+ /**
900
+ * Whether a bundle identifies itself as the WorkBuddy desktop app.
901
+ *
902
+ * The match is on dot boundaries, so a hypothetical `com.workbuddyish` cannot
903
+ * pass as `com.workbuddy`.
904
+ *
905
+ * An unreadable or identifier-less plist is treated as NOT WorkBuddy: refusing
906
+ * a candidate only costs a fallback to another path, whereas accepting the
907
+ * wrong one would execute an unrelated application.
908
+ */
909
+ declare function isWorkbuddyBundle(bundle: string): boolean;
910
+ /**
911
+ * Bundles of the desktop app found one level BELOW a macOS applications
912
+ * directory.
913
+ *
914
+ * Users do file apps into subfolders (`/Applications/IDE/WorkBuddy.app`), and
915
+ * a hardcoded `/Applications/<name>` then reports the app as missing while it
916
+ * is installed and signed in. The scan is deliberately ONE level deep and
917
+ * matches the known bundle names only, so it stays predictable and cheap; each
918
+ * candidate is then confirmed by {@link isWorkbuddyBundle} before use.
919
+ *
920
+ * Returns [] when the parent is absent or unreadable — a missing directory is
921
+ * the normal case, not an error.
819
922
  */
820
- declare function workbuddyAppExecutableCandidates(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): string[];
923
+ declare function macosNestedAppBundles(parent: string): string[];
821
924
  /**
822
925
  * The first candidate that exists as a file, or undefined when the desktop app
823
926
  * is not installed where this platform expects it.
@@ -1442,4 +1545,4 @@ declare function legacyAttributionRegion(value: Config, accountsFor: (region: Wo
1442
1545
  */
1443
1546
  declare function apply(ctx: Context, config: Config): void;
1444
1547
  //#endregion
1445
- export { CREDENTIAL_REJECTED_CODE, Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, type WorkBuddyWebAccount, type WorkBuddyWebCheckin, type WorkBuddyWebCredits, type WorkBuddyWebModel, type WorkBuddyWebPackage, type WorkBuddyWebRegion, type WorkBuddyWebSearchPath, type WorkBuddyWebUsage, apply, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, inject, isCredentialRejectedError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, name, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
1548
+ export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, type WorkBuddyWebAccount, type WorkBuddyWebCheckin, type WorkBuddyWebCredits, type WorkBuddyWebModel, type WorkBuddyWebPackage, type WorkBuddyWebRegion, type WorkBuddyWebSearchPath, type WorkBuddyWebUsage, apply, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as isCredentialRejectedError, B as fetchAtRestKeyPayload, C as parseWorkBuddyAuth, D as WorkBuddyCredentialRejectedError, E as CREDENTIAL_REJECTED_CODE, F as regionOf, G as workbuddyAppExecutableCandidates, H as isEncryptedFieldWrapper, I as WORKBUDDY_APP_EXECUTABLE_ENV, L as clearAtRestKeyCache, M as parseReasoning, N as parseUpstreamModel, O as WorkBuddyUpstreamClient, P as prepareChatBody, R as deriveAtRestKey, S as legacyWorkbuddyOwnAuthPath, T as workbuddyOwnAuthPath, U as openEncryptedField, V as findWorkbuddyAppExecutable, W as readAtRestKey, _ as authFileName, a as readHostHeartbeat, b as defaultDesktopAuthPath, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WorkBuddyCredentialStore, h as WORKBUDDY_AUTH_FILE_ENV, i as processStartTimeMs, j as parseCreditMultiplier, k as classifyUpstreamError, l as FALLBACK_WORKBUDDY_MODELS, m as WORKBUDDY_AUTH_FILENAME, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as defaultDesktopAuthCandidates, w as workbuddyAccountId, x as hasEncryptedCredentialFields, y as defaultDesktopAuthDirs, z as deriveAtRestKeyId } from "./host-heartbeat-DPbL9oi2.js";
1
+ import { A as WorkBuddyCredentialRejectedError, B as clearAtRestKeyCache, C as hasEncryptedCredentialFields, D as workbuddyAccountId, E as parseWorkBuddyAuth, F as parseReasoning, G as isEncryptedFieldWrapper, H as deriveAtRestKeyId, I as parseUpstreamModel, J as macosNestedAppBundles, K as isWorkbuddyBundle, L as prepareChatBody, M as classifyUpstreamError, N as isCredentialRejectedError, O as workbuddyOwnAuthPath, P as parseCreditMultiplier, R as regionOf, S as defaultDesktopAuthPath, T as legacyWorkbuddyOwnAuthPath, U as fetchAtRestKeyPayload, V as deriveAtRestKey, W as findWorkbuddyAppExecutable, X as readAtRestKey, Y as openEncryptedField, Z as workbuddyAppExecutableCandidates, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WORKBUDDY_AUTH_FILE_ENV, h as WORKBUDDY_AUTH_FILENAME, i as processStartTimeMs, j as WorkBuddyUpstreamClient, k as CREDENTIAL_REJECTED_CODE, l as FALLBACK_WORKBUDDY_MODELS, m as ENCRYPTED_CREDENTIAL_CODE, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, q as macosBundleExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as WorkBuddyEncryptedCredentialError, w as isEncryptedCredentialError, x as defaultDesktopAuthDirs, y as authFileName, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-BJdDmMSn.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { randomBytes, timingSafeEqual } from "node:crypto";
4
4
  import { createProvider } from "@earendil-works/pi-ai";
@@ -1270,4 +1270,4 @@ function apply(ctx, config) {
1270
1270
  });
1271
1271
  }
1272
1272
  //#endregion
1273
- export { CREDENTIAL_REJECTED_CODE, Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyUpstreamClient, apply, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, inject, isCredentialRejectedError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, name, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
1273
+ export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-workbuddy",
3
3
  "displayName": "DSH Connect WorkBuddy",
4
4
  "description": "Connect locally signed-in WorkBuddy models to DeepSeek Harness with a read-only credits overview and model management.",
5
- "version": "2.0.10",
5
+ "version": "2.0.11",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",