dsh-connect-trae 1.4.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,20 @@
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
+
3
18
  ## 1.4.0 (2026-09-10)
4
19
 
5
20
  ### 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
@@ -599,6 +599,46 @@ function parseTraeStorageDocument(text) {
599
599
  if (typeof value !== "string") throw new Error(`Trae storage document has no ${TRAE_AUTH_STORAGE_KEY}`);
600
600
  return parseTraeAuthValue(value);
601
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
+ }
602
642
  //#endregion
603
643
  //#region src/paths.ts
604
644
  const APP_NAMES = {
@@ -607,6 +647,33 @@ const APP_NAMES = {
607
647
  solo: "TRAE SOLO CN",
608
648
  "solo-sg": "TRAE SOLO"
609
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
+ };
610
677
  function traeStorageCandidates(platform = process.platform, home = homedir(), env = process.env) {
611
678
  const result = [];
612
679
  for (const edition of [
@@ -617,19 +684,59 @@ function traeStorageCandidates(platform = process.platform, home = homedir(), en
617
684
  ]) {
618
685
  const app = APP_NAMES[edition];
619
686
  let roots;
620
- if (platform === "darwin") roots = [join(home, "Library", "Application Support")];
621
- else if (platform === "win32") roots = [env.APPDATA, join(home, "AppData", "Roaming")].filter((value, index, all) => typeof value === "string" && value !== "" && all.indexOf(value) === index);
622
- else if (platform === "linux") roots = [env.XDG_CONFIG_HOME || join(home, ".config")];
623
- else roots = [];
624
- 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({
625
702
  edition,
626
- 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"
627
732
  });
628
733
  }
629
734
  return result;
630
735
  }
631
736
  //#endregion
632
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";
633
740
  const OWN_VERSION = 1;
634
741
  const TRAE_AUTH_FILENAME = ".trae-auth.json";
635
742
  function traeOwnAuthPath() {
@@ -718,10 +825,18 @@ var TraeCredentialStore = class {
718
825
  this.inflight = void 0;
719
826
  }
720
827
  candidates() {
721
- if (this.storagePathOverride !== void 0) return [{
722
- edition: this.edition === "auto" ? "cn" : this.edition,
723
- path: this.storagePathOverride
724
- }];
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
+ }
725
840
  const all = traeStorageCandidates();
726
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"));
727
842
  }
@@ -785,21 +900,87 @@ var TraeCredentialStore = class {
785
900
  await rm(`${this.ownPath}.lock`, { force: true });
786
901
  }
787
902
  async readAll() {
788
- const desktop = await this.readDesktopAll();
903
+ const { credentials: desktop } = await this.readDesktopAll();
789
904
  const own = await this.readOwn();
790
905
  const cnOwn = own?.edition === "cn" || own?.edition === "solo" ? own : void 0;
791
906
  if (cnOwn === void 0 || desktop.some((credential) => traeAccountId(credential) === traeAccountId(cnOwn))) return desktop;
792
907
  return [...desktop, cnOwn];
793
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
+ }
794
963
  async readDesktopAll() {
795
964
  const credentials = [];
965
+ const failures = [];
796
966
  for (const candidate of this.candidates()) try {
797
- const credential = normalizeTraeCredential(parseTraeStorageDocument(await readFile(candidate.path, "utf8")), candidate.edition, "desktop");
798
- if (credential !== void 0 && !credentials.some((existing) => traeAccountId(existing) === traeAccountId(credential))) credentials.push(credential);
799
- } 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
+ });
800
978
  continue;
801
979
  }
802
- return credentials;
980
+ return {
981
+ credentials,
982
+ failures
983
+ };
803
984
  }
804
985
  async readOwn() {
805
986
  try {
@@ -2513,10 +2694,20 @@ function toCredits(snapshot) {
2513
2694
  */
2514
2695
  async function traeWebUsage(deps) {
2515
2696
  const accounts = await deps.store.accounts();
2516
- if ((await deps.store.status()).state !== "signed-in") return {
2517
- status: "signed-out",
2518
- accounts
2519
- };
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
+ }
2520
2711
  let credential;
2521
2712
  try {
2522
2713
  credential = await deps.store.resolve();
@@ -2804,9 +2995,10 @@ function apply(ctx, config) {
2804
2995
  refresh: (credential) => refreshTraeCredential(credential)
2805
2996
  });
2806
2997
  const identity = async () => {
2807
- 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)) : [{
2808
2999
  edition: config.edition === void 0 || config.edition === "auto" ? "solo" : config.edition,
2809
- path: config.authFile
3000
+ path: config.authFile,
3001
+ source: "desktop"
2810
3002
  }];
2811
3003
  if (candidates.length === 0) throw new Error("Trae storage was not found");
2812
3004
  return pickTraeStorageIdentity(candidates);
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.4.0",
5
+ "version": "1.4.1",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",