dsh-connect-trae 3.9.2 → 3.10.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/lib/index.js CHANGED
@@ -6,15 +6,15 @@ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import { dirname, join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import { readFile, rm, stat } from "node:fs/promises";
10
- import { createDecipheriv, createHash, randomBytes, randomUUID, sign, timingSafeEqual } from "node:crypto";
9
+ import { copyFile, readFile, rm, stat } from "node:fs/promises";
10
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, sign, timingSafeEqual } from "node:crypto";
11
11
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
12
12
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
13
13
  import { cpus, homedir, hostname, release, userInfo } from "node:os";
14
- import { createServer } from "node:http";
15
- import { Readable } from "node:stream";
16
14
  import { execFile } from "node:child_process";
17
15
  import { promisify } from "node:util";
16
+ import { createServer } from "node:http";
17
+ import { Readable } from "node:stream";
18
18
  //#region src/catalog.ts
19
19
  /**
20
20
  * Bootstrap catalog: identity only where current Trae metadata has not been
@@ -966,11 +966,6 @@ function regionOfCredential(credential) {
966
966
  }
967
967
  //#endregion
968
968
  //#region src/status-paths.ts
969
- /**
970
- * Node-free constants and types shared by the Host and browser halves.
971
- * Mirrors the `dsh-workbuddy-connect` status-route pattern so the plugin card
972
- * stays consistent with that project's external presentation.
973
- */
974
969
  /** Plugin-owned usage endpoint consumed by its browser half. */
975
970
  const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
976
971
  /** Plugin-owned live model refresh endpoint. */
@@ -1038,7 +1033,7 @@ function poolActionOf(url) {
1038
1033
  const at = url.indexOf("?");
1039
1034
  if (at === -1) return void 0;
1040
1035
  const value = new URLSearchParams(url.slice(at + 1)).get(TRAE_POOL_ACTION_PARAM);
1041
- return value === "checkin" || value === "test" || value === "remove" ? value : void 0;
1036
+ return value === "checkin" || value === "test" || value === "remove" || value === "delete" ? value : void 0;
1042
1037
  }
1043
1038
  /**
1044
1039
  * Read the ONE account a per-row pool request addresses, from the QUERY string.
@@ -1579,6 +1574,54 @@ function decryptTraeStorageValue(encoded) {
1579
1574
  if (!expected.equals(actual)) throw new Error("Trae auth integrity check failed");
1580
1575
  return plaintext.toString("utf8");
1581
1576
  }
1577
+ /**
1578
+ * Re-encrypts one `iCubeAuthInfo` value the way Trae itself stores it.
1579
+ *
1580
+ * Needed only to rewrite the app's own `storage.json` after removing an account:
1581
+ * dropping a key from the plaintext JSON is useless unless the result is sealed
1582
+ * again with the same scheme, because Trae will fail to decrypt anything else.
1583
+ * The format is [6-byte header][32-byte random][AES-128-CBC ciphertext], where
1584
+ * the key/IV come from `sha512(sha512(random) || salt)` and the ciphertext holds
1585
+ * `sha512(body) || body`. A fresh random is drawn per call, so encrypting the
1586
+ * same plaintext twice yields different bytes — exactly like the app.
1587
+ *
1588
+ * `aes-private` (SALT_C ^ SALT_D) is supported because a value can be read in
1589
+ * that shape; the default matches every value observed in shipped storage files.
1590
+ */
1591
+ function encryptTraeStorageValue(plaintext, type = "aes") {
1592
+ const body = Buffer.from(plaintext, "utf8");
1593
+ const digest = createHash("sha512").update(body).digest();
1594
+ const random = randomBytes(32);
1595
+ const salt = type === "aes-private" ? xor(SALT_C, SALT_D) : xor(SALT_A, SALT_B);
1596
+ const first = createHash("sha512").update(random).digest();
1597
+ const derived = createHash("sha512").update(Buffer.concat([first, salt])).digest();
1598
+ const cipher = createCipheriv("aes-128-cbc", derived.subarray(0, 16), derived.subarray(16, 32));
1599
+ const encrypted = Buffer.concat([cipher.update(Buffer.concat([digest, body])), cipher.final()]);
1600
+ const header = type === "aes-private" ? Buffer.from([
1601
+ 18,
1602
+ 57,
1603
+ 32,
1604
+ 32,
1605
+ 2,
1606
+ 3
1607
+ ]) : Buffer.from([
1608
+ 116,
1609
+ 99,
1610
+ 5,
1611
+ 16,
1612
+ 0,
1613
+ 0
1614
+ ]);
1615
+ return Buffer.concat([
1616
+ header,
1617
+ random,
1618
+ encrypted
1619
+ ]).toString("base64");
1620
+ }
1621
+ /** The encryption family of an encoded value, for a byte-faithful rewrite. */
1622
+ function traeStorageValueType(encoded) {
1623
+ return encryptionType(Buffer.from(encoded, "base64").subarray(0, 6));
1624
+ }
1582
1625
  function parseTraeAuthValue(value) {
1583
1626
  const trimmed = value.trim();
1584
1627
  if (trimmed === "") throw new Error("Trae auth value is empty");
@@ -1634,6 +1677,7 @@ function parseTraeCliToken(text) {
1634
1677
  }
1635
1678
  //#endregion
1636
1679
  //#region src/paths.ts
1680
+ /** The macOS app-support directory name each edition writes, per edition. */
1637
1681
  const APP_NAMES = {
1638
1682
  cn: "Trae CN",
1639
1683
  sg: "Trae",
@@ -1764,6 +1808,80 @@ function traeCliCandidates(platform = process.platform, home = homedir(), env =
1764
1808
  return result;
1765
1809
  }
1766
1810
  //#endregion
1811
+ //#region src/trae-process.ts
1812
+ const run = promisify(execFile);
1813
+ /**
1814
+ * Why this exists at all: Trae keeps `storage.json` in memory and flushes it over
1815
+ * the file. Rewriting that file while the app runs is silently undone on its next
1816
+ * write — the account reappears and the user is told the deletion failed twice
1817
+ * for two different reasons. So the delete refuses while the matching app is up
1818
+ * and says which app to quit.
1819
+ *
1820
+ * This is a helper, never a gate on correctness: if the probe itself fails
1821
+ * (`ps` missing, sandbox denial) the answer is "nothing is running", and the
1822
+ * write proceeds. A false negative costs a retry; a false positive would block a
1823
+ * deletion that would have worked.
1824
+ */
1825
+ const probeRunningTraeApps = async () => {
1826
+ try {
1827
+ if (process.platform === "darwin") {
1828
+ const { stdout } = await run("ps", ["-Ao", "command"], { maxBuffer: 16777216 });
1829
+ return traeEditionsInCommands(stdout, "Application Support");
1830
+ }
1831
+ if (process.platform === "linux") {
1832
+ const { stdout } = await run("ps", ["-Ao", "args"], { maxBuffer: 16777216 });
1833
+ return traeEditionsInCommands(stdout, ".config");
1834
+ }
1835
+ if (process.platform === "win32") {
1836
+ const { stdout } = await run("tasklist", [
1837
+ "/FO",
1838
+ "CSV",
1839
+ "/NH"
1840
+ ], { maxBuffer: 16777216 });
1841
+ return /trae/i.test(stdout) ? /* @__PURE__ */ new Set([
1842
+ "cn",
1843
+ "sg",
1844
+ "solo",
1845
+ "solo-sg"
1846
+ ]) : /* @__PURE__ */ new Set();
1847
+ }
1848
+ } catch {}
1849
+ return /* @__PURE__ */ new Set();
1850
+ };
1851
+ /**
1852
+ * Editions whose app-support directory appears in a process listing.
1853
+ *
1854
+ * The names are prefixes of each other (`Trae` vs `Trae CN`), so the match is
1855
+ * made longest-first at each occurrence and consumes the name it claimed. A
1856
+ * plain `includes('Trae')` would report the international app running whenever
1857
+ * only `Trae CN` was, and the delete would then refuse for the wrong edition.
1858
+ */
1859
+ function traeEditionsInCommands(commands, supportDir) {
1860
+ const running = /* @__PURE__ */ new Set();
1861
+ const editions = [
1862
+ "cn",
1863
+ "sg",
1864
+ "solo",
1865
+ "solo-sg"
1866
+ ].map((edition) => ({
1867
+ edition,
1868
+ name: APP_NAMES[edition]
1869
+ })).sort((a, b) => b.name.length - a.name.length);
1870
+ const marker = `${supportDir}/`;
1871
+ let cursor = commands.indexOf(marker);
1872
+ while (cursor !== -1) {
1873
+ let consumed = 0;
1874
+ for (const { edition, name } of editions) {
1875
+ if (!commands.startsWith(name, cursor + marker.length)) continue;
1876
+ running.add(edition);
1877
+ consumed = name.length;
1878
+ break;
1879
+ }
1880
+ cursor = commands.indexOf(marker, cursor + marker.length + consumed);
1881
+ }
1882
+ return running;
1883
+ }
1884
+ //#endregion
1767
1885
  //#region src/auth.ts
1768
1886
  /** Host used for CLI tokens, which carry no host claim of their own. */
1769
1887
  const CLI_DEFAULT_HOST = "https://api.trae.cn";
@@ -1812,6 +1930,20 @@ function traeOwnAuthPath(region) {
1812
1930
  function legacyTraeOwnAuthPath() {
1813
1931
  return join(resolveDshHome(), TRAE_AUTH_FILENAME);
1814
1932
  }
1933
+ /**
1934
+ * Where the "this copy was deleted on purpose" tombstones live.
1935
+ *
1936
+ * The store mirrors every local Trae login into its own copy on each refresh
1937
+ * (see `mirrorLocalLogins`). Without a tombstone the mirror would recreate a
1938
+ * copy the user just deleted on the very next poll — "delete" would look broken
1939
+ * again, which is the exact complaint this mechanism exists to fix. A tombstone
1940
+ * therefore suppresses the mirror for one account id, and is dropped as soon as
1941
+ * that id is no longer signed in to the app: a later sign-in is a NEW login and
1942
+ * is mirrored again.
1943
+ */
1944
+ function traeDeletedAccountsPath(region) {
1945
+ return join(resolveDshHome(), region === void 0 ? ".trae-deleted.json" : `${TRAE_OWN_PREFIX}.deleted.${region}.json`);
1946
+ }
1815
1947
  function optionalString(value) {
1816
1948
  return typeof value === "string" && value !== "" ? value : void 0;
1817
1949
  }
@@ -1951,6 +2083,42 @@ async function writeOwn(path, credentials) {
1951
2083
  dirMode: 448
1952
2084
  });
1953
2085
  }
2086
+ /** The key Trae stores the per-user region map under, beside the login itself. */
2087
+ const TRAE_USERTAG_STORAGE_KEY = "iCubeAuthInfo://usertag";
2088
+ /**
2089
+ * Rewrites one Trae `storage.json` so it no longer carries the given login.
2090
+ *
2091
+ * Everything else in the document is preserved byte-for-byte at the JSON level:
2092
+ * only the auth value is dropped and the usertag map loses this one user. Both
2093
+ * the backup and the atomic write exist because this is the one operation in the
2094
+ * plugin that edits a file belonging to another application.
2095
+ */
2096
+ async function eraseDesktopLogin(path, document, claims, encoded) {
2097
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2098
+ try {
2099
+ await copyFile(path, `${path}.dsh-backup-${stamp}`);
2100
+ } catch {
2101
+ return false;
2102
+ }
2103
+ const next = { ...document };
2104
+ delete next[TRAE_AUTH_STORAGE_KEY];
2105
+ const userTag = document[TRAE_USERTAG_STORAGE_KEY];
2106
+ const userId = typeof claims["userId"] === "string" || typeof claims["userId"] === "number" ? String(claims["userId"]) : void 0;
2107
+ if (userId !== void 0 && typeof userTag === "string") try {
2108
+ const decoded = parseTraeAuthValue(userTag);
2109
+ if (typeof decoded === "object" && decoded !== null && !Array.isArray(decoded)) {
2110
+ const tags = { ...decoded };
2111
+ delete tags[userId];
2112
+ next[TRAE_USERTAG_STORAGE_KEY] = encryptTraeStorageValue(JSON.stringify(tags), traeStorageValueType(userTag));
2113
+ }
2114
+ } catch {}
2115
+ try {
2116
+ await writeFileAtomic(path, `${JSON.stringify(next, null, 2)}\n`, { mode: 384 });
2117
+ return !(await readFile(path, "utf8")).includes(encoded);
2118
+ } catch {
2119
+ return false;
2120
+ }
2121
+ }
1954
2122
  var TraeCredentialStore = class {
1955
2123
  storagePathOverride;
1956
2124
  edition;
@@ -1963,6 +2131,7 @@ var TraeCredentialStore = class {
1963
2131
  legacyOwnPathExplicit;
1964
2132
  refresh;
1965
2133
  refreshMarginMs;
2134
+ runningApps;
1966
2135
  inflight;
1967
2136
  /**
1968
2137
  * In-flight refreshes of individual accounts, one per account id.
@@ -2007,6 +2176,7 @@ var TraeCredentialStore = class {
2007
2176
  this.legacyOwnPathExplicit = options.legacyOwnPath;
2008
2177
  this.refresh = options.refresh;
2009
2178
  this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
2179
+ this.runningApps = options.runningApps ?? probeRunningTraeApps;
2010
2180
  }
2011
2181
  /** Whether a credential's own claim belongs to this store's region. */
2012
2182
  matchesRegion(credential) {
@@ -2040,6 +2210,93 @@ var TraeCredentialStore = class {
2040
2210
  traeOwnAuthPath("ai")
2041
2211
  ];
2042
2212
  }
2213
+ /**
2214
+ * Where the "deleted copy" tombstones live for this store.
2215
+ *
2216
+ * Derived from the store's own copy path, so a test that pins `ownPath` also
2217
+ * pins the tombstones and can never touch the real `~/.dsh` file.
2218
+ */
2219
+ deletedPath() {
2220
+ if (this.ownPathExplicit !== void 0) return `${this.ownPathExplicit}.deleted`;
2221
+ if (this.legacyOwnPathExplicit !== void 0) return `${this.legacyOwnPathExplicit}.deleted`;
2222
+ return traeDeletedAccountsPath(this.region);
2223
+ }
2224
+ /**
2225
+ * The account ids whose copy the user deleted on purpose.
2226
+ *
2227
+ * Read defensively: a missing, unreadable, or hand-edited file means "no
2228
+ * tombstones", never a failing store — losing the list only re-mirrors an
2229
+ * account, it never locks the user out.
2230
+ */
2231
+ async readTombstones() {
2232
+ const text = await readFile(this.deletedPath(), "utf8").catch(() => "");
2233
+ if (text.trim() === "") return /* @__PURE__ */ new Set();
2234
+ try {
2235
+ const parsed = JSON.parse(text);
2236
+ const ids = Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.ids) ? parsed.ids : [];
2237
+ return new Set(ids.filter((id) => typeof id === "string" && TRAE_ACCOUNT_ID_RE.test(id)));
2238
+ } catch {
2239
+ return /* @__PURE__ */ new Set();
2240
+ }
2241
+ }
2242
+ async writeTombstones(ids) {
2243
+ const path = this.deletedPath();
2244
+ await withFileLock(path, async () => {
2245
+ await writeFileAtomic(path, `${JSON.stringify({
2246
+ version: 1,
2247
+ ids: [...ids]
2248
+ }, null, 2)}\n`, {
2249
+ mode: 384,
2250
+ dirMode: 448
2251
+ });
2252
+ });
2253
+ }
2254
+ /**
2255
+ * Copy every local Trae sign-in into this store's own file, once.
2256
+ *
2257
+ * The plugin presents COPIES, not the app's live login: the app has a single
2258
+ * sign-in slot that the next Trae login overwrites, so an account could only
2259
+ * ever be billed until the user switched apps. Keeping a copy per account is
2260
+ * what makes a multi-account pool survive those switches.
2261
+ *
2262
+ * This runs on every `accounts()` read — the card's refresh, the pool's member
2263
+ * read, the "detect accounts" press — and is additive:
2264
+ * - an account that already has a copy is left exactly as it is (the copy
2265
+ * may hold a token this plugin refreshed, which is fresher than the app's);
2266
+ * - an account the user DELETED stays deleted while it is still signed in to
2267
+ * the app, via a tombstone that is dropped once the app no longer holds it
2268
+ * (so a later sign-in is mirrored again).
2269
+ */
2270
+ async mirrorLocalLogins() {
2271
+ const { credentials: discovered } = await this.readDesktopAll();
2272
+ const own = await this.readOwns();
2273
+ const ownIds = new Set(own.map((credential) => traeAccountId(credential)));
2274
+ const tombstones = await this.readTombstones();
2275
+ const liveIds = new Set(discovered.map((credential) => traeAccountId(credential)));
2276
+ let tombstonesChanged = false;
2277
+ for (const id of [...tombstones]) {
2278
+ if (liveIds.has(id)) continue;
2279
+ tombstones.delete(id);
2280
+ tombstonesChanged = true;
2281
+ }
2282
+ const missing = discovered.filter((credential) => this.matchesRegion(credential) && !ownIds.has(traeAccountId(credential)) && !tombstones.has(traeAccountId(credential)));
2283
+ if (tombstonesChanged) await this.writeTombstones(tombstones);
2284
+ if (missing.length === 0) return;
2285
+ const ownPath = this.ownAuthPath();
2286
+ await withFileLock(ownPath, async () => {
2287
+ const existing = await readFile(ownPath, "utf8").then(parseOwn, () => []);
2288
+ const byId = new Map(existing.map((credential) => [traeAccountId(credential), credential]));
2289
+ for (const credential of missing) {
2290
+ const id = traeAccountId(credential);
2291
+ if (byId.has(id)) continue;
2292
+ byId.set(id, {
2293
+ ...credential,
2294
+ source: "dsh"
2295
+ });
2296
+ }
2297
+ await writeOwn(ownPath, [...byId.values()]);
2298
+ });
2299
+ }
2043
2300
  setSource(storagePath, edition = "auto", accountId) {
2044
2301
  this.storagePathOverride = storagePath;
2045
2302
  this.edition = edition;
@@ -2137,8 +2394,15 @@ var TraeCredentialStore = class {
2137
2394
  return eligible.length > 0 ? eligible : discovered;
2138
2395
  }
2139
2396
  async accounts() {
2397
+ await this.mirrorLocalLogins();
2140
2398
  const discovered = await this.readAll();
2141
- const offered = this.offered(discovered);
2399
+ const copyIds = /* @__PURE__ */ new Set();
2400
+ for (const credential of await this.readOwns()) {
2401
+ if (!this.matchesRegion(credential)) continue;
2402
+ copyIds.add(traeAccountId(credential));
2403
+ }
2404
+ const visible = discovered.filter((credential) => copyIds.has(traeAccountId(credential)));
2405
+ const offered = this.offered(visible);
2142
2406
  const inUse = this.inUse(discovered, offered);
2143
2407
  /**
2144
2408
  * Never hide the account we are actually billing.
@@ -2159,7 +2423,7 @@ var TraeCredentialStore = class {
2159
2423
  edition: credential.edition,
2160
2424
  enterprise: isEnterpriseAccount(credential),
2161
2425
  region: regionOfCredential(credential),
2162
- source: credential.source,
2426
+ source: "dsh",
2163
2427
  tokenExpiresAtMs: credential.expiresAtMs,
2164
2428
  ...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: credential.refreshExpiresAtMs },
2165
2429
  selected: inUseId !== void 0 && traeAccountId(credential) === inUseId
@@ -2269,6 +2533,21 @@ var TraeCredentialStore = class {
2269
2533
  return (await this.readAll()).find((credential) => traeAccountId(credential) === accountId);
2270
2534
  }
2271
2535
  /**
2536
+ * The PLUGIN'S OWN COPY of one account — never the Trae app's credential.
2537
+ *
2538
+ * {@link credentialOf} answers from the merged view (copies plus the app's
2539
+ * logins) and prefers whichever is fresher. That is right for placing a
2540
+ * request, but wrong for HANDING CREDENTIALS OUT: an export must carry the
2541
+ * copies this plugin owns and refreshes, not whatever the app happens to have
2542
+ * on disk. The two are normally identical — the copy is mirrored from the app
2543
+ * — but only the copy is ours, and an account with no copy is not ours to
2544
+ * export at all (undefined, no fallback to the app's login).
2545
+ */
2546
+ async copyOf(accountId) {
2547
+ if (accountId === "") return void 0;
2548
+ return (await this.readOwns()).find((credential) => traeAccountId(credential) === accountId);
2549
+ }
2550
+ /**
2272
2551
  * A SPECIFIC account's credential, refreshed when its access token is due.
2273
2552
  *
2274
2553
  * {@link credentialOf} is a pure lookup and hands back an expired token as-is,
@@ -2385,6 +2664,133 @@ var TraeCredentialStore = class {
2385
2664
  }
2386
2665
  }
2387
2666
  /**
2667
+ * Remove ONE account from the plugin-owned copy (per-region file, legacy
2668
+ * single file), where this store can rewrite the shape it wrote.
2669
+ *
2670
+ * The desktop storage files are deliberately untouched: their credential is
2671
+ * a Trae app login that only Trae can rewrite, so a plugin removing it would
2672
+ * corrupt an application state it does not own. `deleteAccount` therefore
2673
+ * only ever commits what this plugin persisted — the refreshed copy the
2674
+ * store owns — and reports what it did.
2675
+ *
2676
+ * `stillSignedIn` is true when the SAME account id still resolves to a
2677
+ * desktop storage copy, i.e. the account will keep appearing (from Trae's own
2678
+ * login) even after the plugin's copy is gone. The card then shows the user
2679
+ * where the account really lives instead of implying a deletion that did not
2680
+ * take: the account is still signed in Trae, and Trae is the only writer that
2681
+ * can remove it.
2682
+ *
2683
+ * @returns `{ deleted: boolean; stillSignedIn: boolean }` — `deleted` is
2684
+ * whether a plugin-owned copy of this account was actually removed;
2685
+ * `stillSignedIn` reports a TRAE-owned login that survives it.
2686
+ */
2687
+ /**
2688
+ * Erases this account's login from Trae's OWN storage files.
2689
+ *
2690
+ * Deleting only the plugin's copy of an account the app itself is signed in to
2691
+ * changes nothing the user can see: the app keeps reporting the account, the
2692
+ * row comes back on the next poll, and "delete" looks broken. The only real
2693
+ * deletion therefore has to rewrite the app's `storage.json`, which means
2694
+ * (a) re-encrypting the values that stay, with Trae's own scheme, and (b)
2695
+ * refusing while that app runs — its in-memory state would be flushed back
2696
+ * over the file and undo the edit.
2697
+ *
2698
+ * A backup of every rewritten file is left beside it (`.dsh-backup-<stamp>`),
2699
+ * because signing a user's editor out is not something to do without a way back.
2700
+ */
2701
+ async deleteDesktopLogins(accountId) {
2702
+ const signedOut = [];
2703
+ const blocked = [];
2704
+ const seen = /* @__PURE__ */ new Set();
2705
+ for (const candidate of this.candidates()) {
2706
+ if (candidate.source !== "desktop" || seen.has(candidate.path)) continue;
2707
+ seen.add(candidate.path);
2708
+ const document = await readFile(candidate.path, "utf8").then((text) => JSON.parse(text)).catch(() => void 0);
2709
+ if (document === void 0) continue;
2710
+ const encoded = document[TRAE_AUTH_STORAGE_KEY];
2711
+ if (typeof encoded !== "string") continue;
2712
+ let claims;
2713
+ try {
2714
+ const parsed = parseTraeAuthValue(encoded);
2715
+ if (typeof parsed !== "object" || parsed === null) continue;
2716
+ claims = parsed;
2717
+ } catch {
2718
+ continue;
2719
+ }
2720
+ const credential = normalizeTraeCredential(claims, candidate.edition, "desktop");
2721
+ if (credential === void 0 || traeAccountId(credential) !== accountId) continue;
2722
+ if ((await this.runningApps().catch(() => /* @__PURE__ */ new Set())).has(candidate.edition)) {
2723
+ blocked.push(candidate.edition);
2724
+ continue;
2725
+ }
2726
+ if (await eraseDesktopLogin(candidate.path, document, claims, encoded)) signedOut.push(candidate.edition);
2727
+ else blocked.push(candidate.edition);
2728
+ }
2729
+ return {
2730
+ signedOut: [...new Set(signedOut)],
2731
+ blocked: [...new Set(blocked)]
2732
+ };
2733
+ }
2734
+ /**
2735
+ * Delete this store's COPY of one account.
2736
+ *
2737
+ * What the plugin shows and bills is the copy, so that is the first thing
2738
+ * "delete" removes. The copy alone is not enough: the same login usually also
2739
+ * lives in the app's own `storage.json`, and the app keeps reporting it, so
2740
+ * the delete then erases that login too (see `deleteDesktopLogins`, which
2741
+ * refuses while the app runs and leaves a backup beside every file it
2742
+ * rewrites).
2743
+ *
2744
+ * The catch is the mirror: `accounts()` recreates a copy from any local login
2745
+ * that lacks one, so deleting the copy of an account still signed in to Trae
2746
+ * would resurrect it on the next poll. A TOMBSTONE records the deletion for
2747
+ * exactly that id; the mirror skips it while the app still holds the login and
2748
+ * drops the tombstone once it does not, so signing in again later mirrors it
2749
+ * again. That is why `stillSignedIn` is reported: it explains that the account
2750
+ * still lives in Trae even though the plugin no longer shows it.
2751
+ *
2752
+ * @returns `{ deleted, stillSignedIn, hadCopy, signedOutApps, blockedApps }` —
2753
+ * `deleted` is "this machine no longer carries the account anywhere" (true
2754
+ * even when there was never a copy, so a ghost member can be cleared);
2755
+ * `stillSignedIn` answers "is it still a live Trae login here?"; `hadCopy` is
2756
+ * "did a file actually change?"; `signedOutApps` names the apps whose login
2757
+ * was erased; `blockedApps` names the apps that were running, in which case
2758
+ * nothing was erased and `deleted` is false.
2759
+ */
2760
+ async deleteAccount(accountId) {
2761
+ let mutated = false;
2762
+ for (const path of this.ownCandidates()) {
2763
+ const dropped = await withFileLock(path, async () => {
2764
+ const existing = await readFile(path, "utf8").then(parseOwn, () => null);
2765
+ if (existing === null || !existing.some((credential) => traeAccountId(credential) === accountId)) return false;
2766
+ const next = existing.filter((credential) => traeAccountId(credential) !== accountId);
2767
+ await writeOwn(path, next);
2768
+ return true;
2769
+ }).catch(() => false);
2770
+ mutated = mutated || dropped;
2771
+ }
2772
+ const tombstones = await this.readTombstones();
2773
+ if (!tombstones.has(accountId)) {
2774
+ tombstones.add(accountId);
2775
+ await this.writeTombstones(tombstones).catch(() => void 0);
2776
+ }
2777
+ if (this.accountId === accountId) {
2778
+ this.accountId = void 0;
2779
+ this.inflight = void 0;
2780
+ }
2781
+ const { signedOut, blocked } = await this.deleteDesktopLogins(accountId);
2782
+ const stillInOwn = (await this.readOwns()).some((credential) => traeAccountId(credential) === accountId);
2783
+ const { credentials: discovered } = await this.readDesktopAll();
2784
+ const stillSignedIn = discovered.some((credential) => traeAccountId(credential) === accountId);
2785
+ return {
2786
+ deleted: !stillInOwn && !stillSignedIn,
2787
+ stillSignedIn,
2788
+ hadCopy: mutated || signedOut.length > 0,
2789
+ signedOutApps: signedOut,
2790
+ blockedApps: blocked
2791
+ };
2792
+ }
2793
+ /**
2388
2794
  * Every local credential of this store's region, deduplicated by account id.
2389
2795
  * A region-scoped store sees only its own region's credentials: the other
2390
2796
  * region's accounts are invisible to selection, refresh, and status alike,
@@ -4271,6 +4677,39 @@ async function bridgeTraeSoloStreamOrFail(response, model) {
4271
4677
  response: bridgeTraeSource(opened.source, model)
4272
4678
  };
4273
4679
  }
4680
+ /**
4681
+ * The startup wire map for one region's merged directory.
4682
+ *
4683
+ * EVERY row is registered, not only the ones whose wire config_name differs
4684
+ * from the display id. The runtime catalog is filtered by `enabledModelIds`, so
4685
+ * a pool that targets a model the user never enabled for chat would otherwise
4686
+ * reach the bridge with no catalog row AND no mapping — and the request would
4687
+ * carry the raw id under the DEFAULT function, which answers `4001 config_name
4688
+ * rejected` (Doubao-Seed-Code is served only by `solo_agent_remote`;
4689
+ * `solo_work_lite` rejects it). The identity mapping is what keeps the
4690
+ * discovered function alive for those ids.
4691
+ *
4692
+ * First row wins a display name: two directory ids can share one (`Seed-Code`),
4693
+ * and overwriting would answer with the later row's config_name. Ids are
4694
+ * unique, so `byId` needs no such guard.
4695
+ */
4696
+ function wireTargetsOf(models) {
4697
+ const byId = /* @__PURE__ */ new Map();
4698
+ const byName = /* @__PURE__ */ new Map();
4699
+ for (const model of models) {
4700
+ const target = {
4701
+ configName: model.wireConfigName ?? model.id,
4702
+ ...model.wireFunction === void 0 ? {} : { function: model.wireFunction }
4703
+ };
4704
+ byId.set(model.id, target);
4705
+ const nameKey = model.name.trim().toLowerCase();
4706
+ if (!byName.has(nameKey)) byName.set(nameKey, target);
4707
+ }
4708
+ return {
4709
+ byId,
4710
+ byName
4711
+ };
4712
+ }
4274
4713
  /** Native SOLO client wrapper used by the loopback OpenAI adapter. */
4275
4714
  var TraeSoloBridge = class {
4276
4715
  upstream;
@@ -4294,7 +4733,23 @@ var TraeSoloBridge = class {
4294
4733
  ...entry.wireFunction === void 0 ? {} : { function: entry.wireFunction }
4295
4734
  };
4296
4735
  const fromResolver = this.wireResolver?.(model) ?? this.wireResolver?.(entry?.name ?? "");
4297
- const target = fromCatalog ?? fromResolver;
4736
+ /**
4737
+ * The catalog row wins the config_name, but it may not know the function.
4738
+ *
4739
+ * A row persisted by an older build -- or one whose `wireFunction` the
4740
+ * settings schema dropped -- carries the name mapping only, and asking
4741
+ * without the function is a `4001 config_name rejected` for every model
4742
+ * that a non-default function serves (`Doubao-Seed-Code` lives on
4743
+ * `solo_agent_remote` alone). The resolver is built from the same
4744
+ * directory, so borrowing its function is safe exactly when both sources
4745
+ * agree on the wire config_name; a disagreement means the resolver found
4746
+ * a different row and its function would be wrong.
4747
+ */
4748
+ const sameWireConfig = fromCatalog !== void 0 && fromResolver !== void 0 && fromCatalog.configName.toLowerCase() === fromResolver.configName.toLowerCase();
4749
+ const target = fromCatalog !== void 0 && fromCatalog.function === void 0 && sameWireConfig && fromResolver !== void 0 ? {
4750
+ configName: fromCatalog.configName,
4751
+ ...fromResolver.function === void 0 ? {} : { function: fromResolver.function }
4752
+ } : fromCatalog ?? fromResolver;
4298
4753
  /**
4299
4754
  * Last line of defence: the discovered row's own id.
4300
4755
  *
@@ -6635,7 +7090,7 @@ async function traeWebPool(deps, panel) {
6635
7090
  const credits = entry?.credits;
6636
7091
  const checkin = checkinState[row.account.id];
6637
7092
  const excludedBy = excludedById.get(row.account.id);
6638
- const accountName = row.account.accountName !== "" ? row.account.accountName : memberNames[row.account.id] ?? measured?.accountName ?? "";
7093
+ const accountName = row.account.accountName !== "" ? row.account.accountName : memberNames[row.account.id] ?? measured?.accountName ?? row.account.id;
6639
7094
  return {
6640
7095
  accountId: row.account.id,
6641
7096
  accountName,
@@ -6679,9 +7134,12 @@ async function traeWebPool(deps, panel) {
6679
7134
  else if (ranked.every((row) => row.excludedBy !== void 0)) noCandidate = new Set(ranked.map((row) => row.excludedBy)).size === 1 ? ranked[0]?.excludedBy ?? "unusable" : "unusable";
6680
7135
  }
6681
7136
  const effectiveMemberAccountIds = effectiveMembersOf(rows.filter((row) => row.member).map((row) => row.account.id), new Set(members.map((member) => member.account.id)));
7137
+ const copies = await deps.copiesOf?.(panel).catch(() => void 0);
6682
7138
  return {
6683
7139
  enabled: preferences.enabled === true,
6684
7140
  checkinSupported: pool.checkin !== void 0 && regionOfPanel(panel) === "cn",
7141
+ ...deps.deleteAccountOf === void 0 ? {} : { deleteSupported: true },
7142
+ ...copies === void 0 ? {} : { copies },
6685
7143
  ...target.modelId === void 0 ? {} : { targetModelId: target.modelId },
6686
7144
  targetModelSource: target.source,
6687
7145
  probeInputTokens: Math.max(0, preferences.probeInputTokens ?? 0),
@@ -6878,7 +7336,7 @@ function registerTraeUsageRoute(ctx, deps) {
6878
7336
  const records = [];
6879
7337
  for (const account of await store.accounts()) {
6880
7338
  if (!wanted.has(account.id)) continue;
6881
- const credential = await store.credentialOf(account.id).catch(() => void 0);
7339
+ const credential = await store.copyOf(account.id).catch(() => void 0);
6882
7340
  if (credential === void 0) continue;
6883
7341
  records.push(traeTransferRecordOf(credential, {
6884
7342
  auth: {
@@ -7091,6 +7549,19 @@ function registerTraeUsageRoute(ctx, deps) {
7091
7549
  }
7092
7550
  return;
7093
7551
  }
7552
+ if (action === "delete") {
7553
+ if (accountId === void 0) return json(res, 400, { error: "delete needs an accountId" });
7554
+ if (deps.deleteAccountOf === void 0) return json(res, 503, { error: "account deletion unavailable" });
7555
+ try {
7556
+ json(res, 200, {
7557
+ accountId,
7558
+ ...await deps.deleteAccountOf(panel, accountId)
7559
+ });
7560
+ } catch (error) {
7561
+ json(res, 500, { error: safeMessage(error) });
7562
+ }
7563
+ return;
7564
+ }
7094
7565
  /**
7095
7566
  * Both remaining actions need a NON-EMPTY effective pool.
7096
7567
  *
@@ -8116,6 +8587,23 @@ function apply(ctx, config) {
8116
8587
  byId: /* @__PURE__ */ new Map(),
8117
8588
  byName: /* @__PURE__ */ new Map()
8118
8589
  };
8590
+ /**
8591
+ * Warm the wire map before anything spends quota.
8592
+ *
8593
+ * The bridge resolves a model's directory function from the wire map,
8594
+ * falling back to the catalog row. Right after a restart neither may be
8595
+ * populated yet — discovery only runs when the host refreshes its model list
8596
+ * — and a probe sent without the function is answered `4001` for every model
8597
+ * that a non-default function serves (`Doubao-Seed-Code` lives on
8598
+ * `solo_agent_remote`). One discovery here is cheaper than telling the user
8599
+ * that a working account is unavailable.
8600
+ */
8601
+ const warmWireMap = async (panel) => {
8602
+ if (wireState[panel].resolved) return;
8603
+ try {
8604
+ await stacks[panel].discoverModels();
8605
+ } catch {}
8606
+ };
8119
8607
  const dropDeadModels = (rows, panel) => {
8120
8608
  const wire = wireState[panel];
8121
8609
  if (!wire.resolved) return rows;
@@ -8423,16 +8911,9 @@ function apply(ctx, config) {
8423
8911
  wire.callableKeys.add(model.name.trim().toLowerCase());
8424
8912
  }
8425
8913
  wire.resolved = merged.length > 0;
8426
- wire.byId.clear();
8427
- wire.byName.clear();
8428
- for (const model of merged) if (model.wireConfigName !== void 0) {
8429
- const target = {
8430
- configName: model.wireConfigName,
8431
- ...model.wireFunction === void 0 ? {} : { function: model.wireFunction }
8432
- };
8433
- wire.byId.set(model.id, target);
8434
- wire.byName.set(model.name.trim().toLowerCase(), target);
8435
- }
8914
+ const wireTargets = wireTargetsOf(merged);
8915
+ wire.byId = wireTargets.byId;
8916
+ wire.byName = wireTargets.byName;
8436
8917
  latestDiscovery.length = 0;
8437
8918
  latestDiscovery.push(...merged);
8438
8919
  return merged;
@@ -8470,6 +8951,18 @@ function apply(ctx, config) {
8470
8951
  routeOf: (provider) => routeOfTraeProvider(provider) ?? "solo"
8471
8952
  };
8472
8953
  }
8954
+ /**
8955
+ * Mirror each region's Trae login into the plugin's OWN copy as soon as the
8956
+ * plugin loads.
8957
+ *
8958
+ * `accounts()` performs the mirror, so a card read creates the copy — but only
8959
+ * for the tab the user actually opens. Signing in to Trae and never opening
8960
+ * the panel would leave the copy missing until the next visit, which reads as
8961
+ * "the plugin never made a copy". This fire-and-forget pass makes the copies
8962
+ * exist from start-up. Failures are swallowed on purpose: the next card read
8963
+ * retries, and a read-only file system must not break the plugin load.
8964
+ */
8965
+ for (const panel of TRAE_PANELS) stacks[panel].store.accounts().catch(() => void 0);
8473
8966
  /** This panel's saved pool preferences, unwrapped and defaulted to OFF. */
8474
8967
  const poolPreferencesOf = (panel) => unwrapVolatileDeep(regionStateOf(current(), panel).pool) ?? {};
8475
8968
  /**
@@ -8767,6 +9260,7 @@ function apply(ctx, config) {
8767
9260
  };
8768
9261
  /** One account's row, through the same bridge and probe the batch uses. */
8769
9262
  const testWith = async (target, modelId, probeInputTokens) => {
9263
+ await warmWireMap(panel);
8770
9264
  const [row] = await testAllAccounts([target], modelId, (chatFor, credential, id) => {
8771
9265
  if (credential === void 0) return Promise.resolve({
8772
9266
  id,
@@ -8817,6 +9311,7 @@ function apply(ctx, config) {
8817
9311
  return state;
8818
9312
  },
8819
9313
  test: async (stored, modelId, onRow) => {
9314
+ await warmWireMap(stored);
8820
9315
  const probeInputTokens = probeInputTokensOf(stored);
8821
9316
  const rows = await testAllAccounts(await poolTargetsOf(stored), modelId, (chatFor, credential, id) => {
8822
9317
  if (credential === void 0) return Promise.resolve({
@@ -8897,9 +9392,17 @@ function apply(ctx, config) {
8897
9392
  enabledModelIds: (panel) => regionStateOf(current(), panel).enabledModelIds ?? [],
8898
9393
  regionEnabled: (panel) => regionEnabled(current(), panel),
8899
9394
  discoverModels: (panel, signal) => stacks[panel].discoverModels(signal),
8900
- probeModels: (panel, ids) => stacks[panel].probeModels(ids),
9395
+ probeModels: async (panel, ids) => {
9396
+ await warmWireMap(panel);
9397
+ return stacks[panel].probeModels(ids);
9398
+ },
8901
9399
  rawDiagnostic: (panel) => stacks[panel].rawDiagnostic(),
8902
9400
  creditsOfAccount: (panel, accountId) => stacks[panel].creditsOfAccount(accountId),
9401
+ deleteAccountOf: (panel, accountId) => stacks[panel].store.deleteAccount(accountId),
9402
+ copiesOf: async (panel) => ({
9403
+ path: stacks[panel].store.ownAuthPath(),
9404
+ count: (await stacks[panel].store.accounts()).length
9405
+ }),
8903
9406
  accountStateOf: (panel, accountId) => stacks[panel].accountStateOf(accountId),
8904
9407
  claimCheckinOf: (panel, accountId) => stacks[panel].claimCheckinOf(accountId),
8905
9408
  liveLogin: (panel) => liveLoginOf(panel),
@@ -9158,4 +9661,4 @@ function apply(ctx, config) {
9158
9661
  });
9159
9662
  }
9160
9663
  //#endregion
9161
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
9664
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, wireTargetsOf, withContextAdaptation, withTraeRegion };