dsh-connect-trae 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.1 (2026-09-14)
4
+
5
+ > **版本号说明**:`v1.4.1` ~ `v1.4.4` 四个 git tag 曾推送到远端,但对应代码已整体回档到 `1.4.0`,
6
+ > 且这批版本**从未发布到 npm**。本次发布前已删除这四个废弃 tag(备份见
7
+ > `backup/abandoned-v1.4.1-v1.4.4` 分支及 `abandoned-tags-v1.4.1-4.bundle`),
8
+ > 使 git tag 与 npm 版本号从 `1.4.1` 起重新对齐、连续编号。
9
+
10
+ ### Fixes
11
+
12
+ - 修复 CLI 登录(`traecli`)不被识别、插件恒显「未登录」的问题(issue #5,WSL2 用户报告)。插件此前只认桌面版 Electron 的 `globalStorage/storage.json`(加密值 `iCubeAuthInfo://icube.cloudide`),而 `traecli` 把登录信息写在自己的家目录里、内容是**未加密的裸 JWT**(实测 macOS 上是 `~/.trae-cn/trae-jwt-token`);纯 CLI 环境(WSL2 常见)根本没有 `storage.json`,因此永远解析不出账号。现在:
13
+ - 凭据来源扩展为 `desktop` / `cli` 两类,`TraeStorageCandidate` 新增 `source` 字段;新增 `parseTraeCliToken()` 直接解析裸 JWT(取 `data.user_id` 与 `exp`),不走 AES 解密链路。CLI token 不含 host 声明,统一补 CN 主机 `https://api.trae.cn`,避免空字符串变成不可用的 base URL。
14
+ - CLI 候选路径在 macOS / Windows / Linux 三平台都会探测(`~/.trae-cn/`、`~/.trae/`)。
15
+ - Linux 桌面版目录名改为**多候选并列探测**(`trae-cn` 与 macOS 拼写 `Trae CN` 都试)。此前只认从 macOS 抄来的 `Trae CN`,而 Linux 上 Electron 应用通常用小写无空格目录名;该拼写从未在 Linux 真机验证过。多探测保证猜错也不会漏掉真实安装。
16
+ - 把 `readDesktopAll()` 里的静默 `catch { continue }` 改为记录失败原因,新增 `TraeCredentialStore.diagnose()`:返回探测过的每个路径及其失败类型(`missing` / `unreadable` / `invalid`)。未登录时卡片新增可折叠的「已检查的路径」列表。此前无论路径不存在、key 缺失还是加密头不支持,用户都只看到「未登录」三个字,无从自助定位——`docs/WINDOWS_TOKEN_PROBE.md` 整篇文档的存在本身就是这个可观测性缺口的补丁。诊断内容只含路径与固定原因文案(错误消息不回显输入),不携带任何 token 材料,并有专门测试守住这一点。
17
+
18
+ ## 1.4.0 (2026-09-10)
19
+
20
+ ### Changes
21
+
22
+ - 对齐 DSH 内核 `0.1.2-rc.1` → `0.1.5-rc.2`(桌面 `dsh-plugin-desktop` 2.0.9 所捆绑通道;`0.1.5-rc.1` 为 npm `latest`,`rc.2` 在 `next`):
23
+ - `ResolvedPiAiProviderProfile` 在 0.1.5 新增**必填**字段 `modelErrors`:`PiAiAdapter` 现在按模型查此表,命中即以 `INVALID_CONFIG` 拒绝请求;`piProvider` 同时由必填改为可选。本插件是手工构造 profile(不走内核目录解析,故该表不会被子系统填充),补 `modelErrors: new Map()` 表达「所服务的模型全部可用」这一事实。不补则本插件在 0.1.5 宿主上**无法通过类型检查**(`TS2741`)。
24
+ - devDependencies 升级至 `@deepseek-ai/dsh-*@0.1.5-rc.2`,`@earendil-works/pi-ai` 由 `0.84.2` 升至 `^0.85.1`:`dsh-llm-pi-ai@0.1.5-rc.2` 要求 `pi-ai@^0.85.1`,版本不一致会在 `node_modules` 里留下两份互不兼容的 `pi-ai`,导致 `Provider<Api>` 结构不匹配的编译错误。
25
+ - `peerDependencies` 的 `@deepseek-ai/dsh-*` 由 `>=0.1.2-0` 抬到 `>=0.1.5-0`(`modelErrors` 为编译期硬依赖,0.1.2 宿主无法满足),`@earendil-works/pi-ai` 抬到 `>=0.85.1`。仍按范围声明、不锁死补丁版本,与内核 `next` 通道继续前进保持一致。
26
+ - 经核实本次不受文档列出的其余破坏项影响:未使用 `dsh-session` / `dsh-session-persistence` / `dsh-persona` / `dsh-system-prompt` / `dsh-message-feedback` / `dsh-subagent`;未读取 `DSH_SESSION_JSONL`、未调用已移除的 `locate()` / `readRaw()`;`AttachmentStore` 仅作类型引用,`IconChevronDownOutline14` 与客户端 `dsh.client.inject` 六个包在 0.1.5 均仍存在。会话格式 v3 与 persona 段改名对本插件不适用。
27
+
28
+ ### Fixes
29
+
30
+ - 修复内置回退模型表被实时线路映射过滤、导致真实安装上模型几乎全部消失的问题:`FALLBACK_TRAE_MODELS` 是本插件自带的静态兜底表,其中每个 id 都**从未**由 Trae Remote 目录公布,因此永远不会出现在 `callableKeys` 里。此前 `configuredModels` / `derive` 把这张表也交给 `dropDeadModels` 过滤,只要实时目录只返回一部分模型(账号权限、版本或网络给出的子集),过滤就会把「这次没被提到的」回退模型一并删掉——本机实测在无凭据环境下只剩 `glm-5.2` / `kimi-k2.6` 两个模型可用。现在回退表不再参与线路过滤,并以 `wireResolved` 标志取代「`callableKeys` 非空」作为「线路目录已解析」的判据,避免空目录被误读为「没有模型可用」。新增回归测试覆盖该路径;该缺陷在本仓库 1.3.0 上本来就会失败(与内核升级无关),修复后 `pnpm run test` 首次达到 157/157 全绿(此前 154/156)。
31
+
3
32
  ## 1.3.0 (2026-09-08)
4
33
 
5
34
  ### Changes
package/lib/client.js CHANGED
@@ -114,6 +114,13 @@ window.__ModuleLoader__.load({
114
114
  .dsm-trae-usage-cheer{display:inline-flex;align-items:center;gap:4px;flex:none;text-decoration:underline;text-underline-offset:2px;color:var(--dsw-alias-label-tertiary,#999);font-size:13px;line-height:1.5;transition:color .16s}
115
115
  .dsm-trae-usage-cheer-star{font-size:12px;line-height:1;display:inline-flex}
116
116
  .dsm-trae-usage-cheer:hover{color:var(--dsw-alias-label-primary,#e6e6e6)}
117
+ .dsm-trae-searched{margin-top:8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#999)}
118
+ .dsm-trae-searched>summary{cursor:pointer;color:var(--dsw-alias-label-secondary,#b8b8b8);user-select:none}
119
+ .dsm-trae-searched-hint{margin:6px 0 4px;color:var(--dsw-alias-label-tertiary,#999)}
120
+ .dsm-trae-searched-list{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px}
121
+ .dsm-trae-searched-list li{display:flex;flex-direction:column;gap:1px;min-width:0}
122
+ .dsm-trae-searched-list code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,#b8b8b8);word-break:break-all}
123
+ .dsm-trae-searched-reason{color:var(--dsw-alias-label-tertiary,#999);font-size:11px}
117
124
  .dsm-trae-usage-cheer:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:2px}
118
125
  `;
119
126
  //#endregion
@@ -618,10 +625,35 @@ window.__ModuleLoader__.load({
618
625
  ]
619
626
  })
620
627
  ] }) : null,
621
- status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
628
+ status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
622
629
  className: "dsm-trae-usage-text",
623
630
  children: status.message ?? t("row.signedOutHint")
624
- }) : null,
631
+ }), status.searched && status.searched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
632
+ className: "dsm-trae-searched",
633
+ children: [
634
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [
635
+ t("row.searchedTitle"),
636
+ " (",
637
+ status.searched.length,
638
+ ")"
639
+ ] }),
640
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
641
+ className: "dsm-trae-searched-hint",
642
+ children: t("row.searchedHint")
643
+ }),
644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
645
+ className: "dsm-trae-searched-list",
646
+ children: status.searched.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: item.path }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
647
+ className: "dsm-trae-searched-reason",
648
+ children: [
649
+ t(item.source === "cli" ? "row.sourceCli" : "row.sourceDesktop"),
650
+ " · ",
651
+ t(item.reason === "missing" ? "row.reasonMissing" : item.reason === "unreadable" ? "row.reasonUnreadable" : "row.reasonInvalid")
652
+ ]
653
+ })] }, `${item.source}:${item.path}`))
654
+ })
655
+ ]
656
+ }) : null] }) : null,
625
657
  status.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
626
658
  className: "dsm-trae-usage-error",
627
659
  children: status.message
@@ -645,6 +677,14 @@ window.__ModuleLoader__.load({
645
677
  "row.collapse": "Collapse",
646
678
  "row.signedOut": "Not signed in",
647
679
  "row.signedOutHint": "Sign in once in the Trae desktop app; this plugin follows that sign-in automatically.",
680
+ "row.searchedTitle": "Paths checked",
681
+ "row.searchedHint": "No sign-in was found. These locations were probed; if your Trae stores its login elsewhere, please report the path.",
682
+ "row.searchedMore": "Show {count} more",
683
+ "row.reasonMissing": "not found",
684
+ "row.reasonUnreadable": "unreadable",
685
+ "row.reasonInvalid": "unreadable format",
686
+ "row.sourceDesktop": "desktop app",
687
+ "row.sourceCli": "Trae CLI",
648
688
  "row.signedIn": "Signed in: {accountName}",
649
689
  "row.tokenExpiry": "Access token expires {expiresAt} (auto-renewal)",
650
690
  "row.requestFailed": "Request failed",
@@ -692,6 +732,14 @@ window.__ModuleLoader__.load({
692
732
  "row.collapse": "收起",
693
733
  "row.signedOut": "未登录",
694
734
  "row.signedOutHint": "在 Trae 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
735
+ "row.searchedTitle": "已检查的路径",
736
+ "row.searchedHint": "未找到登录信息。以下是插件探测过的位置;如果你的 Trae 把登录信息存在别处,请把该路径反馈给我们。",
737
+ "row.searchedMore": "展开其余 {count} 条",
738
+ "row.reasonMissing": "文件不存在",
739
+ "row.reasonUnreadable": "无法读取",
740
+ "row.reasonInvalid": "格式无法识别",
741
+ "row.sourceDesktop": "桌面版",
742
+ "row.sourceCli": "Trae CLI",
695
743
  "row.signedIn": "已登录:{accountName}",
696
744
  "row.tokenExpiry": "访问令牌 {expiresAt} 过期(自动续期)",
697
745
  "row.requestFailed": "请求失败",
package/lib/index.d.ts CHANGED
@@ -187,9 +187,16 @@ declare function createTraeAdapter(options: TraeAdapterOptions): TraeAdapter;
187
187
  //#endregion
188
188
  //#region src/paths.d.ts
189
189
  type TraeEdition = 'cn' | 'sg' | 'solo' | 'solo-sg';
190
+ /**
191
+ * Where a local sign-in was persisted. `desktop` candidates hold an Electron
192
+ * `globalStorage/storage.json` whose auth value is encrypted; `cli` candidates
193
+ * hold a bare JWT written by the Trae CLI, which needs no decryption.
194
+ */
195
+ type TraeCredentialSource = 'desktop' | 'cli';
190
196
  interface TraeStorageCandidate {
191
197
  edition: TraeEdition;
192
198
  path: string;
199
+ source: TraeCredentialSource;
193
200
  }
194
201
  declare function traeStorageCandidates(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): TraeStorageCandidate[];
195
202
  //#endregion
@@ -203,7 +210,7 @@ interface TraeCredential {
203
210
  expiresAtMs: number;
204
211
  refreshExpiresAtMs?: number;
205
212
  edition: TraeEdition;
206
- source: 'desktop' | 'dsh';
213
+ source: 'desktop' | 'dsh' | 'cli';
207
214
  }
208
215
  interface TraeRefreshOutcome {
209
216
  accessToken: string;
@@ -224,10 +231,21 @@ interface TraeAccountChoice {
224
231
  id: string;
225
232
  accountName: string;
226
233
  edition: TraeEdition;
227
- source: 'desktop' | 'dsh';
234
+ source: 'desktop' | 'dsh' | 'cli';
228
235
  tokenExpiresAtMs: number;
229
236
  selected: boolean;
230
237
  }
238
+ /**
239
+ * Why one candidate path did not yield an account. These are safe to surface:
240
+ * they carry paths and error text, never token material.
241
+ */
242
+ interface TraeCandidateFailure {
243
+ path: string;
244
+ edition: TraeEdition;
245
+ source: TraeCredentialSource;
246
+ reason: 'missing' | 'unreadable' | 'invalid';
247
+ message?: string;
248
+ }
231
249
  declare function traeOwnAuthPath(): string;
232
250
  declare function normalizeTraeCredential(raw: unknown, edition: TraeEdition, source: TraeCredential['source']): TraeCredential | undefined;
233
251
  declare class TraeCredentialStore {
@@ -256,11 +274,23 @@ declare class TraeCredentialStore {
256
274
  state: 'signed-in' | 'signed-out';
257
275
  edition?: TraeEdition;
258
276
  expiresAtMs?: number;
259
- source?: 'desktop' | 'dsh';
277
+ source?: TraeCredential['source'];
260
278
  }>;
261
279
  desktopFilePresent(): Promise<boolean>;
262
280
  logout(): Promise<void>;
263
281
  private readAll;
282
+ /**
283
+ * Which paths were tried and why each one failed. Read-only and token-free:
284
+ * it exists so a signed-out card can explain itself instead of showing a bare
285
+ * "not signed in", which is undiagnosable on a machine whose layout differs
286
+ * from the ones the plugin was written against.
287
+ */
288
+ diagnose(): Promise<{
289
+ tried: TraeStorageCandidate[];
290
+ failures: TraeCandidateFailure[];
291
+ }>;
292
+ /** Parse one candidate file's text into a credential, or throw. */
293
+ private credentialFrom;
264
294
  private readDesktopAll;
265
295
  private readOwn;
266
296
  private refreshNow;
@@ -976,15 +1006,24 @@ interface TraeWebAccount {
976
1006
  id: string;
977
1007
  accountName: string;
978
1008
  edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
979
- source: 'desktop' | 'dsh';
1009
+ source: 'desktop' | 'dsh' | 'cli';
980
1010
  tokenExpiresAtMs: number;
981
1011
  selected: boolean;
982
1012
  }
1013
+ /** One probed candidate path and why it did not yield an account. */
1014
+ interface TraeWebSearchPath {
1015
+ path: string;
1016
+ edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
1017
+ source: 'desktop' | 'cli';
1018
+ reason: 'missing' | 'unreadable' | 'invalid';
1019
+ message?: string;
1020
+ }
983
1021
  /** The JSON document the plugin card renders. */
984
1022
  type TraeWebUsage = {
985
1023
  status: 'signed-out';
986
1024
  accounts: readonly TraeWebAccount[];
987
1025
  message?: string;
1026
+ searched?: readonly TraeWebSearchPath[];
988
1027
  } | {
989
1028
  status: 'signed-in';
990
1029
  accountId: string;
package/lib/index.js CHANGED
@@ -262,6 +262,7 @@ function createTraeAdapter(options) {
262
262
  streamIdleTimeoutMs: TRAE_STREAM_IDLE_TIMEOUT_MS,
263
263
  retryPolicy: resolveRetryPolicy(void 0, "dsh-connect-trae retryPolicy"),
264
264
  configuredMaxTokens: /* @__PURE__ */ new Map(),
265
+ modelErrors: /* @__PURE__ */ new Map(),
265
266
  ...REQUEST_IMAGE_BUDGETS,
266
267
  piProvider: provider
267
268
  };
@@ -598,6 +599,46 @@ function parseTraeStorageDocument(text) {
598
599
  if (typeof value !== "string") throw new Error(`Trae storage document has no ${TRAE_AUTH_STORAGE_KEY}`);
599
600
  return parseTraeAuthValue(value);
600
601
  }
602
+ function decodeBase64UrlJson(segment) {
603
+ try {
604
+ const padded = segment.replace(/-/g, "+").replace(/_/g, "/");
605
+ const decoded = Buffer.from(padded, "base64").toString("utf8");
606
+ const parsed = JSON.parse(decoded);
607
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
608
+ } catch {
609
+ return;
610
+ }
611
+ }
612
+ /**
613
+ * Parse the CLI token file. Accepts either the bare JWT itself or a JSON
614
+ * envelope containing one, since the on-disk shape is only verified on macOS
615
+ * and a future CLI revision may wrap it.
616
+ */
617
+ function parseTraeCliToken(text) {
618
+ const trimmed = text.trim();
619
+ if (trimmed === "") throw new Error("Trae CLI token file is empty");
620
+ let token = trimmed;
621
+ if (trimmed.startsWith("{")) {
622
+ const envelope = JSON.parse(trimmed);
623
+ const candidate = envelope["token"] ?? envelope["accessToken"] ?? envelope["jwt"];
624
+ if (typeof candidate !== "string" || candidate.trim() === "") throw new Error("Trae CLI token document has no token field");
625
+ token = candidate.trim();
626
+ }
627
+ const segments = token.split(".");
628
+ if (segments.length !== 3 || segments.some((segment) => segment === "")) throw new Error("Trae CLI token is not a three-part JWT");
629
+ const payload = decodeBase64UrlJson(segments[1]);
630
+ if (payload === void 0) throw new Error("Trae CLI token payload is not decodable JSON");
631
+ const data = typeof payload["data"] === "object" && payload["data"] !== null && !Array.isArray(payload["data"]) ? payload["data"] : void 0;
632
+ const userId = typeof data?.["user_id"] === "string" ? data["user_id"] : void 0;
633
+ if (userId === void 0 || userId === "") throw new Error("Trae CLI token has no data.user_id claim");
634
+ const exp = payload["exp"];
635
+ const expiresAtMs = typeof exp === "number" && Number.isFinite(exp) && exp > 0 ? exp * 1e3 : void 0;
636
+ return {
637
+ accessToken: token,
638
+ userId,
639
+ ...expiresAtMs === void 0 ? {} : { expiresAtMs }
640
+ };
641
+ }
601
642
  //#endregion
602
643
  //#region src/paths.ts
603
644
  const APP_NAMES = {
@@ -606,6 +647,33 @@ const APP_NAMES = {
606
647
  solo: "TRAE SOLO CN",
607
648
  "solo-sg": "TRAE SOLO"
608
649
  };
650
+ /**
651
+ * Directory names the Trae CLI uses for its own home. These are deliberately
652
+ * separate from the Electron app-support names above: the CLI keeps a dotfile
653
+ * home (observed on macOS as `~/.trae-cn`) rather than an `Application Support`
654
+ * entry, so a machine with only the CLI installed has no `storage.json` at all.
655
+ */
656
+ const CLI_HOME_NAMES = [".trae-cn", ".trae"];
657
+ /** Basename of the CLI's persisted bare JWT. */
658
+ const TRAE_CLI_TOKEN_FILENAME = "trae-jwt-token";
659
+ /**
660
+ * Linux desktop config directory names. Electron apps on Linux normally use a
661
+ * lowercase, space-free name rather than the macOS `Trae CN` spelling, so both
662
+ * are probed: guessing only the macOS spelling would silently miss a real
663
+ * install, and guessing only the Linux spelling would break every existing
664
+ * user. The actual name is unverified on a real Linux host.
665
+ */
666
+ const LINUX_APP_NAMES = {
667
+ cn: [
668
+ "trae-cn",
669
+ "Trae CN",
670
+ "trae",
671
+ "Trae"
672
+ ],
673
+ sg: ["trae", "Trae"],
674
+ solo: ["trae-solo-cn", "TRAE SOLO CN"],
675
+ "solo-sg": ["trae-solo", "TRAE SOLO"]
676
+ };
609
677
  function traeStorageCandidates(platform = process.platform, home = homedir(), env = process.env) {
610
678
  const result = [];
611
679
  for (const edition of [
@@ -616,19 +684,59 @@ function traeStorageCandidates(platform = process.platform, home = homedir(), en
616
684
  ]) {
617
685
  const app = APP_NAMES[edition];
618
686
  let roots;
619
- if (platform === "darwin") roots = [join(home, "Library", "Application Support")];
620
- else if (platform === "win32") roots = [env.APPDATA, join(home, "AppData", "Roaming")].filter((value, index, all) => typeof value === "string" && value !== "" && all.indexOf(value) === index);
621
- else if (platform === "linux") roots = [env.XDG_CONFIG_HOME || join(home, ".config")];
622
- else roots = [];
623
- for (const root of roots) result.push({
687
+ let appNames;
688
+ if (platform === "darwin") {
689
+ roots = [join(home, "Library", "Application Support")];
690
+ appNames = [app];
691
+ } else if (platform === "win32") {
692
+ roots = [env.APPDATA, join(home, "AppData", "Roaming")].filter((value, index, all) => typeof value === "string" && value !== "" && all.indexOf(value) === index);
693
+ appNames = [app];
694
+ } else if (platform === "linux") {
695
+ roots = [env.XDG_CONFIG_HOME || join(home, ".config")];
696
+ appNames = LINUX_APP_NAMES[edition];
697
+ } else {
698
+ roots = [];
699
+ appNames = [app];
700
+ }
701
+ for (const root of roots) for (const appName of appNames) result.push({
624
702
  edition,
625
- path: join(root, app, "User", "globalStorage", "storage.json")
703
+ path: join(root, appName, "User", "globalStorage", "storage.json"),
704
+ source: "desktop"
705
+ });
706
+ }
707
+ return [...result, ...traeCliCandidates(platform, home, env)];
708
+ }
709
+ /**
710
+ * Candidate CLI token paths. Only CN/SOLO editions are targeted here for the
711
+ * same reason the store ignores SG desktop installs, and `.trae-cn` is mapped
712
+ * to `cn` while `.trae` is the CLI's international home and is therefore
713
+ * skipped by the store's edition filter.
714
+ *
715
+ * The CLI home is a dotfile directory directly under `$HOME` on every platform
716
+ * observed so far, but this is only verified on macOS; the Windows and Linux
717
+ * spellings are probed speculatively and a miss is harmless because every
718
+ * candidate is tried in order.
719
+ */
720
+ function traeCliCandidates(platform = process.platform, home = homedir(), env = process.env) {
721
+ const roots = [];
722
+ if (platform === "win32") {
723
+ for (const value of [env.USERPROFILE, home]) if (typeof value === "string" && value !== "" && !roots.includes(value)) roots.push(value);
724
+ } else roots.push(home);
725
+ const result = [];
726
+ for (const root of roots) for (const name of CLI_HOME_NAMES) {
727
+ const edition = name === ".trae-cn" ? "cn" : "sg";
728
+ result.push({
729
+ edition,
730
+ path: join(root, name, TRAE_CLI_TOKEN_FILENAME),
731
+ source: "cli"
626
732
  });
627
733
  }
628
734
  return result;
629
735
  }
630
736
  //#endregion
631
737
  //#region src/auth.ts
738
+ /** Host used for CLI tokens, which carry no host claim of their own. */
739
+ const CLI_DEFAULT_HOST = "https://api.trae.cn";
632
740
  const OWN_VERSION = 1;
633
741
  const TRAE_AUTH_FILENAME = ".trae-auth.json";
634
742
  function traeOwnAuthPath() {
@@ -717,10 +825,18 @@ var TraeCredentialStore = class {
717
825
  this.inflight = void 0;
718
826
  }
719
827
  candidates() {
720
- if (this.storagePathOverride !== void 0) return [{
721
- edition: this.edition === "auto" ? "cn" : this.edition,
722
- path: this.storagePathOverride
723
- }];
828
+ if (this.storagePathOverride !== void 0) {
829
+ const edition = this.edition === "auto" ? "cn" : this.edition;
830
+ return [{
831
+ edition,
832
+ path: this.storagePathOverride,
833
+ source: "desktop"
834
+ }, {
835
+ edition,
836
+ path: this.storagePathOverride,
837
+ source: "cli"
838
+ }];
839
+ }
724
840
  const all = traeStorageCandidates();
725
841
  return this.edition === "auto" ? all.filter((candidate) => candidate.edition === "cn" || candidate.edition === "solo") : all.filter((candidate) => candidate.edition === this.edition && (candidate.edition === "cn" || candidate.edition === "solo"));
726
842
  }
@@ -784,21 +900,87 @@ var TraeCredentialStore = class {
784
900
  await rm(`${this.ownPath}.lock`, { force: true });
785
901
  }
786
902
  async readAll() {
787
- const desktop = await this.readDesktopAll();
903
+ const { credentials: desktop } = await this.readDesktopAll();
788
904
  const own = await this.readOwn();
789
905
  const cnOwn = own?.edition === "cn" || own?.edition === "solo" ? own : void 0;
790
906
  if (cnOwn === void 0 || desktop.some((credential) => traeAccountId(credential) === traeAccountId(cnOwn))) return desktop;
791
907
  return [...desktop, cnOwn];
792
908
  }
909
+ /**
910
+ * Which paths were tried and why each one failed. Read-only and token-free:
911
+ * it exists so a signed-out card can explain itself instead of showing a bare
912
+ * "not signed in", which is undiagnosable on a machine whose layout differs
913
+ * from the ones the plugin was written against.
914
+ */
915
+ async diagnose() {
916
+ const tried = this.candidates();
917
+ const failures = [];
918
+ for (const candidate of tried) {
919
+ const raw = await readFile(candidate.path, "utf8").then((text) => ({ text }), (error) => ({ error }));
920
+ if ("error" in raw) {
921
+ const code = typeof raw.error === "object" && raw.error !== null && "code" in raw.error ? raw.error.code : void 0;
922
+ failures.push({
923
+ path: candidate.path,
924
+ edition: candidate.edition,
925
+ source: candidate.source,
926
+ reason: code === "ENOENT" ? "missing" : "unreadable",
927
+ ...code === "ENOENT" ? {} : { message: String(raw.error) }
928
+ });
929
+ continue;
930
+ }
931
+ try {
932
+ this.credentialFrom(candidate, raw.text);
933
+ } catch (error) {
934
+ failures.push({
935
+ path: candidate.path,
936
+ edition: candidate.edition,
937
+ source: candidate.source,
938
+ reason: "invalid",
939
+ message: error instanceof Error ? error.message : String(error)
940
+ });
941
+ }
942
+ }
943
+ return {
944
+ tried,
945
+ failures
946
+ };
947
+ }
948
+ /** Parse one candidate file's text into a credential, or throw. */
949
+ credentialFrom(candidate, text) {
950
+ let credential;
951
+ if (candidate.source === "cli") {
952
+ const claims = parseTraeCliToken(text);
953
+ credential = normalizeTraeCredential({
954
+ token: claims.accessToken,
955
+ userId: claims.userId,
956
+ host: CLI_DEFAULT_HOST,
957
+ expiredAt: claims.expiresAtMs
958
+ }, candidate.edition === "sg" ? "cn" : candidate.edition, "cli");
959
+ } else credential = normalizeTraeCredential(parseTraeStorageDocument(text), candidate.edition, "desktop");
960
+ if (credential === void 0) throw new Error(`${candidate.source} candidate could not be normalized into a credential`);
961
+ return credential;
962
+ }
793
963
  async readDesktopAll() {
794
964
  const credentials = [];
965
+ const failures = [];
795
966
  for (const candidate of this.candidates()) try {
796
- const credential = normalizeTraeCredential(parseTraeStorageDocument(await readFile(candidate.path, "utf8")), candidate.edition, "desktop");
797
- if (credential !== void 0 && !credentials.some((existing) => traeAccountId(existing) === traeAccountId(credential))) credentials.push(credential);
798
- } catch {
967
+ const credential = this.credentialFrom(candidate, await readFile(candidate.path, "utf8"));
968
+ if (!credentials.some((existing) => traeAccountId(existing) === traeAccountId(credential))) credentials.push(credential);
969
+ } catch (error) {
970
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
971
+ failures.push({
972
+ path: candidate.path,
973
+ edition: candidate.edition,
974
+ source: candidate.source,
975
+ reason: code === "ENOENT" ? "missing" : code === void 0 ? "invalid" : "unreadable",
976
+ ...code === "ENOENT" || code === void 0 && !(error instanceof Error) ? {} : { message: error instanceof Error ? error.message : String(error) }
977
+ });
799
978
  continue;
800
979
  }
801
- return credentials;
980
+ return {
981
+ credentials,
982
+ failures
983
+ };
802
984
  }
803
985
  async readOwn() {
804
986
  try {
@@ -2512,10 +2694,20 @@ function toCredits(snapshot) {
2512
2694
  */
2513
2695
  async function traeWebUsage(deps) {
2514
2696
  const accounts = await deps.store.accounts();
2515
- if ((await deps.store.status()).state !== "signed-in") return {
2516
- status: "signed-out",
2517
- accounts
2518
- };
2697
+ if ((await deps.store.status()).state !== "signed-in") {
2698
+ const { failures } = await deps.store.diagnose();
2699
+ return {
2700
+ status: "signed-out",
2701
+ accounts,
2702
+ searched: failures.map((failure) => ({
2703
+ path: failure.path,
2704
+ edition: failure.edition,
2705
+ source: failure.source,
2706
+ reason: failure.reason,
2707
+ ...failure.message === void 0 ? {} : { message: safeMessage(failure.message) }
2708
+ }))
2709
+ };
2710
+ }
2519
2711
  let credential;
2520
2712
  try {
2521
2713
  credential = await deps.store.resolve();
@@ -2783,17 +2975,19 @@ function apply(ctx, config) {
2783
2975
  const enabledSet = (value) => new Set(value.enabledModelIds ?? []);
2784
2976
  const imageSet = (value) => new Set(value.imageModelIds ?? []);
2785
2977
  const callableKeys = /* @__PURE__ */ new Set();
2978
+ let wireResolved = false;
2786
2979
  const dropDeadModels = (rows) => {
2787
- if (callableKeys.size === 0) return rows;
2980
+ if (!wireResolved) return rows;
2788
2981
  return rows.filter((model) => callableKeys.has(model.id.trim().toLowerCase()) || callableKeys.has(model.name.trim().toLowerCase()));
2789
2982
  };
2983
+ const fallbackModels = (value) => applyImageSelection(FALLBACK_TRAE_MODELS, imageSet(value));
2790
2984
  const derive = (value, raw) => {
2791
2985
  const selectedImages = imageSet(value);
2792
2986
  const derived = deriveCatalog(applyImageSelection(sanitizeCatalog(dropDeadModels(raw)), selectedImages), enabledSet(value), value.contextBudgets ?? {});
2793
- return derived.length > 0 ? derived : applyImageSelection(dropDeadModels(FALLBACK_TRAE_MODELS), selectedImages);
2987
+ return derived.length > 0 ? derived : fallbackModels(value);
2794
2988
  };
2795
- const configuredModels = (value) => value.lastCatalog?.length ? derive(value, value.lastCatalog) : value.models?.length ? derive(value, value.models) : applyImageSelection(dropDeadModels(FALLBACK_TRAE_MODELS), imageSet(value));
2796
- const displayModels = (value) => value.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(value.lastCatalog)) : value.models?.length ? dropDeadModels(sanitizeCatalog(value.models)) : dropDeadModels(FALLBACK_TRAE_MODELS);
2989
+ const configuredModels = (value) => value.lastCatalog?.length ? derive(value, value.lastCatalog) : value.models?.length ? derive(value, value.models) : fallbackModels(value);
2990
+ const displayModels = (value) => value.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(value.lastCatalog)) : value.models?.length ? dropDeadModels(sanitizeCatalog(value.models)) : FALLBACK_TRAE_MODELS;
2797
2991
  const store = new TraeCredentialStore({
2798
2992
  ...config.authFile === void 0 ? {} : { storagePath: config.authFile },
2799
2993
  edition: config.edition ?? "auto",
@@ -2801,9 +2995,10 @@ function apply(ctx, config) {
2801
2995
  refresh: (credential) => refreshTraeCredential(credential)
2802
2996
  });
2803
2997
  const identity = async () => {
2804
- const candidates = config.authFile === void 0 ? traeStorageCandidates().filter((item) => (item.edition === "cn" || item.edition === "solo") && (config.edition === void 0 || config.edition === "auto" || item.edition === config.edition)) : [{
2998
+ const candidates = config.authFile === void 0 ? traeStorageCandidates().filter((item) => item.source === "desktop" && (item.edition === "cn" || item.edition === "solo") && (config.edition === void 0 || config.edition === "auto" || item.edition === config.edition)) : [{
2805
2999
  edition: config.edition === void 0 || config.edition === "auto" ? "solo" : config.edition,
2806
- path: config.authFile
3000
+ path: config.authFile,
3001
+ source: "desktop"
2807
3002
  }];
2808
3003
  if (candidates.length === 0) throw new Error("Trae storage was not found");
2809
3004
  return pickTraeStorageIdentity(candidates);
@@ -2869,6 +3064,7 @@ function apply(ctx, config) {
2869
3064
  callableKeys.add(model.id.trim().toLowerCase());
2870
3065
  callableKeys.add(model.name.trim().toLowerCase());
2871
3066
  }
3067
+ wireResolved = merged.length > 0;
2872
3068
  wireById.clear();
2873
3069
  wireByName.clear();
2874
3070
  for (const model of merged) if (model.wireConfigName !== void 0) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-trae",
3
3
  "displayName": "DSH Connect Trae",
4
4
  "description": "Connect locally signed-in Trae models to DeepSeek Harness, plus a read-only usage/credits summary.",
5
- "version": "1.3.0",
5
+ "version": "1.4.1",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",
@@ -73,35 +73,35 @@
73
73
  },
74
74
  "peerDependencies": {
75
75
  "@deepseek-ai/cordis": "^4.0.1-rc.1",
76
- "@deepseek-ai/dsh-atomic-write": ">=0.1.2-0 <0.2.0-0",
77
- "@deepseek-ai/dsh-attachment": ">=0.1.2-0 <0.2.0-0",
78
- "@deepseek-ai/dsh-home-paths": ">=0.1.2-0 <0.2.0-0",
79
- "@deepseek-ai/dsh-host-webserver": ">=0.1.2-0 <0.2.0-0",
80
- "@deepseek-ai/dsh-llm": ">=0.1.2-0 <0.2.0-0",
81
- "@deepseek-ai/dsh-llm-pi-ai": ">=0.1.2-0 <0.2.0-0",
82
- "@deepseek-ai/dsh-settings": ">=0.1.2-0 <0.2.0-0",
83
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.2-0 <0.2.0-0",
76
+ "@deepseek-ai/dsh-atomic-write": ">=0.1.5-0 <0.2.0-0",
77
+ "@deepseek-ai/dsh-attachment": ">=0.1.5-0 <0.2.0-0",
78
+ "@deepseek-ai/dsh-home-paths": ">=0.1.5-0 <0.2.0-0",
79
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.5-0 <0.2.0-0",
80
+ "@deepseek-ai/dsh-llm": ">=0.1.5-0 <0.2.0-0",
81
+ "@deepseek-ai/dsh-llm-pi-ai": ">=0.1.5-0 <0.2.0-0",
82
+ "@deepseek-ai/dsh-settings": ">=0.1.5-0 <0.2.0-0",
83
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.5-0 <0.2.0-0",
84
84
  "@deepseek-ai/schemastery": "^3.18.1-rc.1",
85
- "@earendil-works/pi-ai": ">=0.82.1",
85
+ "@earendil-works/pi-ai": ">=0.85.1",
86
86
  "react": "^18.2.0"
87
87
  },
88
88
  "devDependencies": {
89
89
  "@deepseek-ai/cordis": "^4.0.2",
90
- "@deepseek-ai/dsh-atomic-write": "^0.1.2-rc.1",
91
- "@deepseek-ai/dsh-attachment": "^0.1.2-rc.1",
92
- "@deepseek-ai/dsh-client-locale": "0.1.2-rc.1",
93
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.2-rc.1",
94
- "@deepseek-ai/dsh-client-ui-settings": "0.1.2-rc.1",
95
- "@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.2-rc.1",
96
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-rc.1",
97
- "@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1",
98
- "@deepseek-ai/dsh-home-paths": "^0.1.2-rc.1",
99
- "@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
100
- "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
101
- "@deepseek-ai/dsh-llm-pi-ai": "^0.1.2-rc.1",
102
- "@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
90
+ "@deepseek-ai/dsh-atomic-write": "0.1.5-rc.2",
91
+ "@deepseek-ai/dsh-attachment": "0.1.5-rc.2",
92
+ "@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
93
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.2",
94
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
95
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.5-rc.2",
96
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
97
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
98
+ "@deepseek-ai/dsh-home-paths": "0.1.5-rc.2",
99
+ "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
100
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.2",
101
+ "@deepseek-ai/dsh-llm-pi-ai": "0.1.5-rc.2",
102
+ "@deepseek-ai/dsh-settings": "0.1.5-rc.2",
103
103
  "@deepseek-ai/schemastery": "3.18.2",
104
- "@earendil-works/pi-ai": "0.84.2",
104
+ "@earendil-works/pi-ai": "^0.85.1",
105
105
  "@types/node": "^22.20.0",
106
106
  "@types/react": "~18.3.31",
107
107
  "react": "^18.3.1",