dsh-workbuddy-connect 0.6.2 → 0.6.4

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.
@@ -5,7 +5,7 @@ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
5
5
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
6
6
  import { createDecipheriv, createHash, randomBytes } from "node:crypto";
7
7
  import { execFile, execFileSync } from "node:child_process";
8
- import { accessSync, constants, realpathSync, statSync } from "node:fs";
8
+ import { accessSync, constants, readFileSync, realpathSync, statSync } from "node:fs";
9
9
  //#region src/app-version.ts
10
10
  /**
11
11
  * The international desktop app's version, used as the `/v3/config` UA.
@@ -18,9 +18,10 @@ import { accessSync, constants, realpathSync, statSync } from "node:fs";
18
18
  * removed — returns the 21-model App document. The UA is therefore built from
19
19
  * the form verified in code, not from the earlier prose.
20
20
  *
21
- * The version is only ever a UA component: a missing App, an unreadable
22
- * plist, or a bad cached value degrades to the last saved value and finally to
23
- * a compiled-in constant, and never blocks credential use or the provider.
21
+ * The version feeds the international catalog UA and the chat identity's
22
+ * `X-IDE-Version`: a missing App, an unreadable plist, or a bad cached value
23
+ * degrades to the last saved value and finally to a compiled-in constant, and
24
+ * never blocks credential use or the provider.
24
25
  *
25
26
  * @module dsh-workbuddy-connect/app-version
26
27
  */
@@ -628,6 +629,7 @@ const SESSION_DEAD_MARKERS = ["Offline user session not found", "12153"];
628
629
  /** Classify an upstream failure from its HTTP status and body excerpt. */
629
630
  function classifyUpstreamError(status, body) {
630
631
  if (status === 402) return "hard_credit";
632
+ if (status === 401) return "session_dead";
631
633
  const lower = body.toLowerCase();
632
634
  for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
633
635
  for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
@@ -637,6 +639,26 @@ function classifyUpstreamError(status, body) {
637
639
  if (status >= 400) return "client";
638
640
  return "client";
639
641
  }
642
+ /**
643
+ * Extract a user-facing error message from an upstream JSON response body.
644
+ * Prefers WorkBuddy's localized `displayMsg.zh` / `displayMsg.en`, falling back to `msg`.
645
+ */
646
+ function extractDisplayErrorMessage(body) {
647
+ const trimmed = body.trim();
648
+ if (!trimmed.startsWith("{")) return void 0;
649
+ try {
650
+ const parsed = JSON.parse(trimmed);
651
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
652
+ const obj = parsed;
653
+ const displayMsg = obj["displayMsg"];
654
+ if (typeof displayMsg === "object" && displayMsg !== null && !Array.isArray(displayMsg)) {
655
+ const localized = displayMsg;
656
+ if (typeof localized["zh"] === "string" && localized["zh"].trim() !== "") return localized["zh"].trim();
657
+ if (typeof localized["en"] === "string" && localized["en"].trim() !== "") return localized["en"].trim();
658
+ }
659
+ if (typeof obj["msg"] === "string" && obj["msg"].trim() !== "") return obj["msg"].trim();
660
+ } catch {}
661
+ }
640
662
  /** Region for a login domain; an empty domain means CN (matching upstream tooling). */
641
663
  function regionOf(domain) {
642
664
  const lowered = domain.trim().toLowerCase();
@@ -665,18 +687,21 @@ function commonHeaders(credential) {
665
687
  /**
666
688
  * Chat request headers, including the X-No-* conventions the official CLI uses.
667
689
  *
668
- * `userAgent` carries the desktop identity for chat and probe requests; when
669
- * it is absent the shared CLI-form UA applies. Refresh shares `commonHeaders`
670
- * but never this override, so the two paths cannot drift into each other.
690
+ * `userAgent` and `clientVersion` carry the same resolved desktop identity for
691
+ * chat and probe requests. Refresh shares `commonHeaders` but never this
692
+ * override, so the two paths cannot drift into each other.
671
693
  */
672
- function chatHeaders(credential, userAgent) {
694
+ function chatHeaders(credential, userAgent, clientVersion) {
673
695
  return {
674
696
  ...commonHeaders(credential),
675
- ...userAgent === void 0 ? {} : { "User-Agent": userAgent },
697
+ "User-Agent": userAgent,
676
698
  "Content-Type": "application/json",
677
699
  ...credential.uid === "" ? { "X-No-User-Id": "1" } : { "X-User-Id": credential.uid },
678
700
  ...credential.enterpriseId === void 0 || credential.enterpriseId === "" ? { "X-No-Enterprise-Id": "1" } : { "X-Enterprise-Id": credential.enterpriseId },
679
701
  ...credential.domain === "" ? { "X-No-Department-Info": "1" } : { "X-Domain": credential.domain },
702
+ "X-IDE-Type": "WorkBuddy",
703
+ "X-IDE-Name": "WorkBuddy",
704
+ "X-IDE-Version": clientVersion,
680
705
  "X-Product": "SaaS"
681
706
  };
682
707
  }
@@ -821,18 +846,21 @@ var WorkBuddyUpstreamClient = class {
821
846
  /** POST the chat endpoint; a successful answer is the raw SSE response. */
822
847
  async chatStream(credential, bodyJson, signal) {
823
848
  const region = regionOf(credential.domain);
849
+ let identity;
824
850
  let userAgent;
825
851
  try {
826
- userAgent = chatUserAgent(await this.resolveChatIdentity(region), region);
852
+ identity = await this.resolveChatIdentity(region);
853
+ userAgent = chatUserAgent(identity, region);
827
854
  } catch {
828
- userAgent = chatUserAgent(fallbackChatIdentity(region), region);
855
+ identity = fallbackChatIdentity(region);
856
+ userAgent = chatUserAgent(identity, region);
829
857
  }
830
858
  let response;
831
859
  try {
832
860
  response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {
833
861
  method: "POST",
834
862
  headers: {
835
- ...chatHeaders(credential, userAgent),
863
+ ...chatHeaders(credential, userAgent, identity.clientVersion),
836
864
  "Authorization": `Bearer ${credential.accessToken}`
837
865
  },
838
866
  body: region === "global" ? prepareInternationalChatBody(bodyJson) : bodyJson,
@@ -1144,11 +1172,14 @@ var WorkBuddyUpstreamClient = class {
1144
1172
  */
1145
1173
  async probeEffort(credential, model, effort, signal) {
1146
1174
  const international = regionOf(credential.domain) === "global";
1175
+ let identity;
1147
1176
  let userAgent;
1148
1177
  try {
1149
- userAgent = chatUserAgent(await this.resolveChatIdentity(international ? "global" : "cn"), international ? "global" : "cn");
1178
+ identity = await this.resolveChatIdentity(international ? "global" : "cn");
1179
+ userAgent = chatUserAgent(identity, international ? "global" : "cn");
1150
1180
  } catch {
1151
- userAgent = chatUserAgent(fallbackChatIdentity(international ? "global" : "cn"), international ? "global" : "cn");
1181
+ identity = fallbackChatIdentity(international ? "global" : "cn");
1182
+ userAgent = chatUserAgent(identity, international ? "global" : "cn");
1152
1183
  }
1153
1184
  const payload = {
1154
1185
  model,
@@ -1168,7 +1199,7 @@ var WorkBuddyUpstreamClient = class {
1168
1199
  response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {
1169
1200
  method: "POST",
1170
1201
  headers: {
1171
- ...chatHeaders(credential, userAgent),
1202
+ ...chatHeaders(credential, userAgent, identity.clientVersion),
1172
1203
  "Authorization": `Bearer ${credential.accessToken}`
1173
1204
  },
1174
1205
  body: JSON.stringify(payload),
@@ -1443,15 +1474,143 @@ const INTERNATIONAL_SYSTEM_PROMPT = "You are a helpful assistant.";
1443
1474
  */
1444
1475
  const INTERNATIONAL_PROBE_MAX_TOKENS = 16;
1445
1476
  //#endregion
1477
+ //#region src/status-paths.ts
1478
+ /** Node-free constants and types shared by the Host and browser halves. */
1479
+ /** Plugin-owned status endpoint consumed by its browser half. */
1480
+ const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-connect/status";
1481
+ /**
1482
+ * Plugin-owned probe control endpoint.
1483
+ *
1484
+ * Separate from the status route because it accepts writes: the status route's
1485
+ * loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
1486
+ * not the same as authorizing a state-changing action. This route therefore
1487
+ * also requires the in-process key the browser half receives with the status
1488
+ * document.
1489
+ */
1490
+ const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-connect/probe";
1491
+ /**
1492
+ * The international (WorkBuddy AI) variant's own pair of routes.
1493
+ *
1494
+ * Kept as separate constants rather than a computed suffix so both halves
1495
+ * reference literal strings: the browser bundle and the host bundle are built
1496
+ * independently, and a shared expression is one build-config drift away from
1497
+ * the desk asking a route the host never mounted.
1498
+ */
1499
+ const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
1500
+ const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
1501
+ /**
1502
+ * Same-origin route backing the browser's update reminder.
1503
+ *
1504
+ * Read-only like the status route, so the same loopback Host/Origin gate
1505
+ * applies; it answers public npm/GitHub metadata only and never token
1506
+ * material.
1507
+ */
1508
+ const WORKBUDDY_UPDATE_PATH = "/plugins/dsh-workbuddy-connect/update";
1509
+ //#endregion
1510
+ //#region src/variants.ts
1511
+ /**
1512
+ * The two WorkBuddy desktop apps this one plugin serves.
1513
+ *
1514
+ * Both products are the same client framework in different regions, and both
1515
+ * write their sign-in into the *same* shared `CodeBuddyExtension` auth
1516
+ * directory — they differ by file basename, base URL, catalog endpoint, and
1517
+ * display identity. Everything that varies between them is collected here as
1518
+ * one descriptor, so no module has to carry its own `if (international)`
1519
+ * branch and a third variant would be a data change rather than a refactor.
1520
+ *
1521
+ * This module is host-side (it names files and env vars). The browser half
1522
+ * takes the same ids and routes from the Node-free `status-paths.ts`, which
1523
+ * stays the single source shared by both halves.
1524
+ *
1525
+ * @module dsh-workbuddy-connect/variants
1526
+ */
1527
+ /** CN WorkBuddy first: the existing provider keeps its id, paths, and copy. */
1528
+ const WORKBUDDY_VARIANTS = [{
1529
+ id: "workbuddy",
1530
+ displayName: "WorkBuddy",
1531
+ appName: "WorkBuddy",
1532
+ region: "cn",
1533
+ env: "WORKBUDDY_AUTH_FILE",
1534
+ electron: {
1535
+ productName: "WorkBuddy",
1536
+ envVar: "WORKBUDDY_ELECTRON_BIN",
1537
+ macOS: {
1538
+ bundleId: "com.tencent.workbuddy.mac",
1539
+ defaultPath: "/Applications/WorkBuddy.app/Contents/MacOS/Electron"
1540
+ },
1541
+ windows: {
1542
+ displayNamePattern: /^WorkBuddy(?:\s+\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.-]+)?)?$/u,
1543
+ exeBasename: "workbuddy.exe",
1544
+ defaultPathSegments: [
1545
+ "Programs",
1546
+ "WorkBuddy",
1547
+ "WorkBuddy.exe"
1548
+ ]
1549
+ }
1550
+ },
1551
+ desktopFilename: "workbuddy-desktop.info",
1552
+ ownFilename: ".workbuddy-auth.json",
1553
+ probeFilename: ".workbuddy-probe.json",
1554
+ catalogFilename: ".workbuddy-catalog.json",
1555
+ visibilityFilename: ".workbuddy-model-visibility.json",
1556
+ statusPath: WORKBUDDY_STATUS_PATH,
1557
+ probePath: WORKBUDDY_PROBE_PATH
1558
+ }, {
1559
+ id: "workbuddy-ai",
1560
+ displayName: "WorkBuddy AI",
1561
+ appName: "WorkBuddy AI",
1562
+ region: "global",
1563
+ env: "WORKBUDDY_AI_AUTH_FILE",
1564
+ electron: {
1565
+ productName: "WorkBuddy AI",
1566
+ envVar: "WORKBUDDY_AI_ELECTRON_BIN",
1567
+ macOS: {
1568
+ bundleId: "com.workbuddy.workbuddy-ai",
1569
+ defaultPath: "/Applications/WorkBuddy AI.app/Contents/MacOS/Electron"
1570
+ },
1571
+ windows: {
1572
+ displayNamePattern: /^WorkBuddy AI(?:\s+\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.-]+)?)?$/u,
1573
+ exeBasename: "workbuddyai.exe"
1574
+ }
1575
+ },
1576
+ desktopFilename: "workbuddy-desktop-ai.info",
1577
+ ownFilename: ".workbuddy-ai-auth.json",
1578
+ probeFilename: ".workbuddy-ai-probe.json",
1579
+ catalogFilename: ".workbuddy-ai-catalog.json",
1580
+ visibilityFilename: ".workbuddy-ai-model-visibility.json",
1581
+ statusPath: WORKBUDDY_AI_STATUS_PATH,
1582
+ probePath: WORKBUDDY_AI_PROBE_PATH
1583
+ }];
1584
+ /** The CN variant; the plugin's long-standing default and compatibility anchor. */
1585
+ const CN_VARIANT = WORKBUDDY_VARIANTS[0];
1586
+ /** The international variant. */
1587
+ const AI_VARIANT = WORKBUDDY_VARIANTS[1];
1588
+ /** Look up a variant by provider id. */
1589
+ function variantFor(id) {
1590
+ return WORKBUDDY_VARIANTS.find((variant) => variant.id === id);
1591
+ }
1592
+ /**
1593
+ * The Electron profile a variant resolves with.
1594
+ *
1595
+ * Callers inside the plugin pass their known-complete variants; descriptors
1596
+ * assembled outside (the type is public and predates the profile) fall back
1597
+ * by variant id, so an id-less or unknown custom variant stays on the CN
1598
+ * product — the same default the store's other legacy fields assume.
1599
+ */
1600
+ function electronProfileFor(variant) {
1601
+ return variant?.electron ?? (variant?.id === AI_VARIANT.id ? AI_VARIANT : CN_VARIANT).electron;
1602
+ }
1603
+ //#endregion
1446
1604
  //#region src/desktop-credential-protection.ts
1447
1605
  /**
1448
1606
  * WorkBuddy 5.6.x at-rest credential protection: classification, key
1449
- * resolution, and field decryption for the desktop app's encrypted auth file.
1607
+ * resolution, and field decryption for the desktop apps' encrypted auth files.
1450
1608
  *
1451
- * Since WorkBuddy 5.6 the desktop app encrypts `auth.accessToken` and
1452
- * `auth.refreshToken` at rest (`buildPolicy: "fields"`, on by default), so the
1453
- * plugin reads `{$wbEncrypted:1, envelope}` wrappers instead of token strings
1454
- * (issues #39/#40). Everything needed to open them lives on the same machine:
1609
+ * Since WorkBuddy 5.6 both desktop apps (CN and international, which followed
1610
+ * in 5.6.2) encrypt `auth.accessToken` and `auth.refreshToken` at rest
1611
+ * (`buildPolicy: "fields"`, on by default), so the plugin reads
1612
+ * `{$wbEncrypted:1, envelope}` wrappers instead of token strings (issues
1613
+ * #39/#40, #59/#60). Everything needed to open them lives on the same machine:
1455
1614
  *
1456
1615
  * - the sealed payload (`{version:1, atRestSecretKey}`) comes from the
1457
1616
  * WorkBuddy-modified Electron's private `workbuddyStorage` binding, reached
@@ -1469,18 +1628,17 @@ const INTERNATIONAL_PROBE_MAX_TOKENS = 16;
1469
1628
  *
1470
1629
  * @module dsh-workbuddy-connect/desktop-credential-protection
1471
1630
  */
1472
- /** Env variable that overrides the WorkBuddy Electron binary used as the key helper. */
1473
- const WORKBUDDY_ELECTRON_BIN_ENV = "WORKBUDDY_ELECTRON_BIN";
1474
- /** Platform-default Electron binary, confirmed only on macOS (5.6.2). */
1475
- const MACOS_ELECTRON_PATH = "/Applications/WorkBuddy.app/Contents/MacOS/Electron";
1476
1631
  /**
1477
- * The Electron binary the helper would spawn on this platform, or `undefined`
1478
- * where no default has been verified. Other platforms must set
1479
- * {@link WORKBUDDY_ELECTRON_BIN_ENV} explicitly — the layout is simply not
1480
- * known, and guessing would spawn the wrong app's binary.
1632
+ * The platform-default Electron binary for one product, or `undefined` where
1633
+ * none is verified. macOS defaults come from each product's measured layout;
1634
+ * the Windows default needs `LOCALAPPDATA`, and the international app has no
1635
+ * verified default location at all — registry discovery only, never a guess.
1481
1636
  */
1482
- function defaultWorkBuddyElectronPath() {
1483
- return process.platform === "darwin" ? MACOS_ELECTRON_PATH : void 0;
1637
+ function defaultWorkBuddyElectronPath(product, platform = process.platform) {
1638
+ if (platform === "darwin") return product.macOS.defaultPath;
1639
+ if (platform !== "win32" || product.windows.defaultPathSegments === void 0) return void 0;
1640
+ const localAppData = process.env.LOCALAPPDATA?.trim();
1641
+ return localAppData === void 0 || localAppData === "" ? void 0 : join(localAppData, ...product.windows.defaultPathSegments);
1484
1642
  }
1485
1643
  /** Distinct key ids across the wrapped fields, in field order. */
1486
1644
  function keyIdsOf(fields) {
@@ -1661,11 +1819,37 @@ function parseAtRestPayload(text) {
1661
1819
  function deriveProtectorKey(secret) {
1662
1820
  return createHash("sha256").update(secret, "utf8").digest();
1663
1821
  }
1664
- /** The CN app's bundle id; the only one this round resolves by discovery. */
1665
- const WORKBUDDY_CN_BUNDLE_ID = "com.tencent.workbuddy.mac";
1822
+ /**
1823
+ * Select discovery by platform. Both products have verified layouts on macOS
1824
+ * and Windows (the international app is registry-only there), so discovery no
1825
+ * longer gates on region; Linux and others stay `none`.
1826
+ */
1827
+ function electronDiscoveryFor(platform = process.platform) {
1828
+ if (platform === "darwin") return "macos-workbuddy";
1829
+ if (platform === "win32") return "windows-workbuddy";
1830
+ return "none";
1831
+ }
1832
+ /**
1833
+ * The at-rest key provider one variant's store should use. Shared by the
1834
+ * plugin host and the CLI entry so the browser card and `doctor`/`status` can
1835
+ * never disagree about which binary a variant resolves.
1836
+ */
1837
+ function atRestKeyProviderFor(variant) {
1838
+ return new WorkBuddyAtRestKeyProvider({
1839
+ product: electronProfileFor(variant),
1840
+ discovery: electronDiscoveryFor()
1841
+ });
1842
+ }
1666
1843
  /** Absolute tool paths: never resolved through PATH, which a user can change. */
1667
1844
  const MDFIND_BIN = "/usr/bin/mdfind";
1668
1845
  const PLUTIL_BIN = "/usr/bin/plutil";
1846
+ const WINDOWS_REGISTRY_ROOTS = [
1847
+ "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
1848
+ "HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
1849
+ "HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
1850
+ ];
1851
+ const WINDOWS_REGISTRY_OUTPUT_MAX_BYTES = 1048576;
1852
+ const WINDOWS_ELECTRON_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u;
1669
1853
  /** One discovery subprocess's own limits; see {@link WorkBuddyAtRestKeyProviderOptions}. */
1670
1854
  const WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS = 3e3;
1671
1855
  const MDFIND_MAX_OUTPUT_BYTES = 1048576;
@@ -1682,8 +1866,12 @@ var DiscoveryIncompleteError = class extends Error {};
1682
1866
  * identity. Every failure that means "we could not tell" — a missing tool, a
1683
1867
  * timeout, an oversized answer — is raised as {@link DiscoveryIncompleteError}
1684
1868
  * so it can never be silently read as "no such app".
1869
+ *
1870
+ * `bundleId` is the product being searched for; the Spotlight query and the
1871
+ * caller's identity comparison both use it, so the two can never disagree
1872
+ * about which app they are looking for.
1685
1873
  */
1686
- function workBuddyDiscoveryTools() {
1874
+ function workBuddyDiscoveryTools(bundleId) {
1687
1875
  const runTool = (bin, args, maxBytes, signal) => new Promise((resolve, reject) => {
1688
1876
  if (signal.aborted) {
1689
1877
  reject(new DiscoveryIncompleteError(`${bin} was not started: the discovery budget was already spent`));
@@ -1715,7 +1903,7 @@ function workBuddyDiscoveryTools() {
1715
1903
  });
1716
1904
  return {
1717
1905
  findApps: async (signal) => {
1718
- return (await runTool(MDFIND_BIN, [`kMDItemCFBundleIdentifier == '${WORKBUDDY_CN_BUNDLE_ID}'`], MDFIND_MAX_OUTPUT_BYTES, signal)).split("\n").map((line) => line.trim()).filter((line) => line.endsWith(".app"));
1906
+ return (await runTool(MDFIND_BIN, [`kMDItemCFBundleIdentifier == '${bundleId}'`], MDFIND_MAX_OUTPUT_BYTES, signal)).split("\n").map((line) => line.trim()).filter((line) => line.endsWith(".app"));
1719
1907
  },
1720
1908
  bundleIdentifier: async (bundlePath, signal) => {
1721
1909
  try {
@@ -1749,6 +1937,58 @@ function workBuddyDiscoveryTools() {
1749
1937
  };
1750
1938
  }
1751
1939
  /**
1940
+ * The Windows registry discovery tool. It is deliberately separate from the
1941
+ * macOS Spotlight/plutil seam: the two platforms have different identity and
1942
+ * candidate rules, and neither tool should accidentally become cross-platform.
1943
+ */
1944
+ function workBuddyWindowsDiscoveryTools() {
1945
+ const systemRoot = process.env.SystemRoot?.trim();
1946
+ const regPath = systemRoot === void 0 || systemRoot === "" ? void 0 : join(systemRoot, "System32", "reg.exe");
1947
+ const runTool = (root, signal) => new Promise((resolve, reject) => {
1948
+ if (regPath === void 0) {
1949
+ reject(new DiscoveryIncompleteError("SystemRoot is not configured"));
1950
+ return;
1951
+ }
1952
+ if (signal.aborted) {
1953
+ reject(new DiscoveryIncompleteError("reg.exe was not started: the discovery budget was already spent"));
1954
+ return;
1955
+ }
1956
+ let settled = false;
1957
+ const child = execFile(regPath, [
1958
+ "query",
1959
+ root,
1960
+ "/s"
1961
+ ], {
1962
+ maxBuffer: WINDOWS_REGISTRY_OUTPUT_MAX_BYTES,
1963
+ timeout: WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS,
1964
+ windowsHide: true
1965
+ }, (error, stdout, stderr) => {
1966
+ if (settled) return;
1967
+ settled = true;
1968
+ if (error !== null && error !== void 0) {
1969
+ if (error.killed !== true && (error.code === 1 || error.code === "1") && windowsRegistryKeyMissing(stderr)) {
1970
+ resolve("");
1971
+ return;
1972
+ }
1973
+ reject(new DiscoveryIncompleteError(`reg.exe could not complete (${error.killed === true ? "timed out" : String(error.code ?? "unavailable")})`));
1974
+ return;
1975
+ }
1976
+ resolve(stdout);
1977
+ });
1978
+ const abort = () => {
1979
+ if (settled) return;
1980
+ settled = true;
1981
+ child.kill();
1982
+ reject(new DiscoveryIncompleteError("reg.exe was abandoned: the discovery budget was spent"));
1983
+ };
1984
+ signal.addEventListener("abort", abort, { once: true });
1985
+ child.on("close", () => {
1986
+ signal.removeEventListener("abort", abort);
1987
+ });
1988
+ });
1989
+ return { queryUninstallRoot: runTool };
1990
+ }
1991
+ /**
1752
1992
  * In-memory protector-key resolver: one spawn per key id, single-flight, never
1753
1993
  * persisted. The cache is keyed by the id envelopes ask for, so an envelope
1754
1994
  * sealed under a rotated key triggers exactly one fresh resolution.
@@ -1760,9 +2000,12 @@ function workBuddyDiscoveryTools() {
1760
2000
  * a different app.
1761
2001
  */
1762
2002
  explicitPath;
2003
+ product;
1763
2004
  defaultPath;
1764
2005
  discovery;
1765
2006
  tools;
2007
+ windowsTools;
2008
+ platform;
1766
2009
  discoveryBudgetMs;
1767
2010
  timeoutMs;
1768
2011
  source;
@@ -1775,13 +2018,16 @@ function workBuddyDiscoveryTools() {
1775
2018
  discoveredPath;
1776
2019
  cache;
1777
2020
  inflight;
1778
- constructor(options = {}) {
1779
- const fromEnv = process.env[WORKBUDDY_ELECTRON_BIN_ENV]?.trim();
2021
+ constructor(options) {
2022
+ this.product = options.product;
2023
+ const fromEnv = process.env[options.product.envVar]?.trim();
1780
2024
  const envPath = fromEnv === void 0 || fromEnv === "" ? void 0 : fromEnv;
1781
2025
  this.explicitPath = options.electronPath ?? envPath;
1782
2026
  this.discovery = options.discovery ?? "none";
1783
- this.defaultPath = options.defaultElectronPath === void 0 ? defaultWorkBuddyElectronPath() : options.defaultElectronPath ?? void 0;
1784
- this.tools = options.tools ?? workBuddyDiscoveryTools();
2027
+ this.platform = options.platform ?? process.platform;
2028
+ this.defaultPath = this.discovery === "none" ? void 0 : options.defaultElectronPath === void 0 ? defaultWorkBuddyElectronPath(options.product, this.platform) : options.defaultElectronPath ?? void 0;
2029
+ this.tools = options.tools ?? workBuddyDiscoveryTools(options.product.macOS.bundleId);
2030
+ this.windowsTools = options.windowsTools ?? workBuddyWindowsDiscoveryTools();
1785
2031
  this.discoveryBudgetMs = options.discoveryBudgetMs ?? 1e4;
1786
2032
  this.timeoutMs = options.timeoutMs ?? 1e4;
1787
2033
  this.spawnHelper = options.spawnHelper ?? ((path) => this.spawnAt(path));
@@ -1839,29 +2085,29 @@ function workBuddyDiscoveryTools() {
1839
2085
  */
1840
2086
  async resolveElectronPath() {
1841
2087
  if (this.explicitPath !== void 0) {
1842
- if (!isExecutable(this.explicitPath)) throw new WorkBuddyElectronPathError("electron-path-invalid", `the configured WorkBuddy Electron binary is not available at ${this.explicitPath}; check ${WORKBUDDY_ELECTRON_BIN_ENV} or unset it to let the plugin look for the app itself`);
2088
+ if (!isExecutable(this.explicitPath)) throw new WorkBuddyElectronPathError("electron-path-invalid", `the configured ${this.product.productName} Electron binary is not available at ${this.explicitPath}; check ${this.product.envVar} or unset it to let the plugin look for the app itself`);
1843
2089
  return this.explicitPath;
1844
2090
  }
1845
- if (this.discovery === "none") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no WorkBuddy Electron binary is configured for this platform; set ${WORKBUDDY_ELECTRON_BIN_ENV} to the app's Electron binary`);
2091
+ if (this.discovery === "none") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no ${this.product.productName} Electron binary is configured for this platform; set ${this.product.envVar} to the app's Electron binary`);
1846
2092
  if (this.defaultPath !== void 0 && isExecutable(this.defaultPath)) return this.defaultPath;
1847
2093
  if (this.discoveredPath !== void 0) {
1848
2094
  if (isExecutable(this.discoveredPath)) return this.discoveredPath;
1849
2095
  this.discoveredPath = void 0;
1850
2096
  }
1851
- const found = await this.discoverMacosApp();
2097
+ const found = this.discovery === "macos-workbuddy" ? await this.discoverMacosApp() : await this.discoverWindowsApp();
1852
2098
  this.discoveredPath = found;
1853
2099
  return found;
1854
2100
  }
1855
2101
  /**
1856
- * Resolve the CN app through Spotlight, then prove each candidate's identity
1857
- * before it can be executed.
2102
+ * Resolve this product's app through Spotlight, then prove each candidate's
2103
+ * identity before it can be executed.
1858
2104
  *
1859
2105
  * The whole flow shares one budget: a hang in one candidate must not extend
1860
2106
  * the wait for the others, and running out of budget is reported as an
1861
2107
  * unfinished check rather than an absent app.
1862
2108
  */
1863
2109
  async discoverMacosApp() {
1864
- if (process.platform !== "darwin") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no WorkBuddy Electron binary is configured for this platform; set ${WORKBUDDY_ELECTRON_BIN_ENV} to the app's Electron binary`);
2110
+ if (this.platform !== "darwin") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no ${this.product.productName} Electron binary is configured for this platform; set ${this.product.envVar} to the app's Electron binary`);
1865
2111
  const controller = new AbortController();
1866
2112
  const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
1867
2113
  try {
@@ -1869,7 +2115,7 @@ function workBuddyDiscoveryTools() {
1869
2115
  try {
1870
2116
  candidates = await this.tools.findApps(controller.signal);
1871
2117
  } catch {
1872
- throw discoveryIncomplete("the app search did not complete");
2118
+ throw discoveryIncomplete(this.product.productName, "the app search did not complete");
1873
2119
  }
1874
2120
  const seen = /* @__PURE__ */ new Map();
1875
2121
  let unresolved = false;
@@ -1892,7 +2138,7 @@ function workBuddyDiscoveryTools() {
1892
2138
  unresolved = true;
1893
2139
  continue;
1894
2140
  }
1895
- if (bundleIdentifier !== "com.tencent.workbuddy.mac") continue;
2141
+ if (bundleIdentifier !== this.product.macOS.bundleId) continue;
1896
2142
  const electronPath = join(candidate, "Contents", "MacOS", "Electron");
1897
2143
  if (!isExecutable(electronPath)) continue;
1898
2144
  let identity;
@@ -1914,15 +2160,65 @@ function workBuddyDiscoveryTools() {
1914
2160
  ...version === void 0 ? {} : { version }
1915
2161
  });
1916
2162
  }
1917
- if (seen.size > 1) throw new WorkBuddyElectronPathError("electron-binary-ambiguous", `more than one WorkBuddy application was found, so none was chosen:\n${[...seen.values()].map((app) => ` - ${app.bundlePath}${app.version === void 0 ? "" : ` (${app.version})`}`).join("\n")}\n set ${WORKBUDDY_ELECTRON_BIN_ENV} to the one to use`);
1918
- if (unresolved) throw discoveryIncomplete("some candidates could not be checked");
1919
- if (seen.size === 0) throw new WorkBuddyElectronPathError("electron-binary-not-found", "no WorkBuddy application was found in the default location or the system index; if WorkBuddy is installed elsewhere, it may not be indexed yet");
2163
+ if (seen.size > 1) {
2164
+ const listed = [...seen.values()].map((app) => ` - ${app.bundlePath}${app.version === void 0 ? "" : ` (${app.version})`}`).join("\n");
2165
+ throw new WorkBuddyElectronPathError("electron-binary-ambiguous", `more than one ${this.product.productName} application was found, so none was chosen:\n${listed}\n set ${this.product.envVar} to the one to use`);
2166
+ }
2167
+ if (unresolved) throw discoveryIncomplete(this.product.productName, "some candidates could not be checked");
2168
+ if (seen.size === 0) throw new WorkBuddyElectronPathError("electron-binary-not-found", `no ${this.product.productName} application was found in the default location or the system index; if it is installed elsewhere, it may not be indexed yet; set ${this.product.envVar} to the app's Electron binary`);
1920
2169
  return [...seen.values()][0].electronPath;
1921
2170
  } finally {
1922
2171
  clearTimeout(timer);
1923
2172
  controller.abort();
1924
2173
  }
1925
2174
  }
2175
+ /**
2176
+ * Resolve this product's app through Windows uninstall records. Registry
2177
+ * entries provide hints, not trust: every DisplayIcon candidate must still
2178
+ * be the product's Electron binary with the known Electron layout before
2179
+ * execution.
2180
+ */
2181
+ async discoverWindowsApp() {
2182
+ if (this.platform !== "win32") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no ${this.product.productName} Electron binary is configured for this platform; set ${this.product.envVar} to the app's Electron binary`);
2183
+ const controller = new AbortController();
2184
+ const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
2185
+ try {
2186
+ const candidates = [];
2187
+ let unresolved = false;
2188
+ for (const root of WINDOWS_REGISTRY_ROOTS) {
2189
+ let output;
2190
+ try {
2191
+ output = await this.windowsTools.queryUninstallRoot(root, controller.signal);
2192
+ } catch {
2193
+ unresolved = true;
2194
+ continue;
2195
+ }
2196
+ const parsed = parseWindowsRegistryOutput(output, this.product.windows.displayNamePattern);
2197
+ candidates.push(...parsed.candidates);
2198
+ unresolved ||= parsed.incomplete;
2199
+ }
2200
+ const seen = /* @__PURE__ */ new Map();
2201
+ for (const candidate of new Set(candidates)) {
2202
+ const inspection = inspectWindowsElectronCandidate(candidate, this.platform, this.product.windows.exeBasename);
2203
+ if (inspection === "unresolved") {
2204
+ unresolved = true;
2205
+ continue;
2206
+ }
2207
+ if (inspection === void 0) continue;
2208
+ seen.set(inspection.identity, inspection.electronPath);
2209
+ }
2210
+ if (unresolved) throw discoveryIncomplete(this.product.productName, "some registry entries or candidates could not be checked");
2211
+ if (seen.size > 1) {
2212
+ const listed = [...seen.values()].map((path) => ` - ${path}`).join("\n");
2213
+ throw new WorkBuddyElectronPathError("electron-binary-ambiguous", `more than one ${this.product.productName} application was found, so none was chosen:\n${listed}\n set ${this.product.envVar} to the one to use`);
2214
+ }
2215
+ if (seen.size === 0) throw new WorkBuddyElectronPathError("electron-binary-not-found", `no usable ${this.product.productName} Electron binary was found in the default location or Windows uninstall records; set ${this.product.envVar} to the app's Electron binary`);
2216
+ return [...seen.values()][0];
2217
+ } finally {
2218
+ clearTimeout(timer);
2219
+ controller.abort();
2220
+ }
2221
+ }
1926
2222
  async spawnPayload() {
1927
2223
  return await this.spawnHelper(await this.resolveElectronPath());
1928
2224
  }
@@ -1951,6 +2247,115 @@ function workBuddyDiscoveryTools() {
1951
2247
  });
1952
2248
  }
1953
2249
  };
2250
+ /** Only known missing-key diagnostics can safely make a failed query empty. */
2251
+ function windowsRegistryKeyMissing(stderr) {
2252
+ const detail = stderr.trim();
2253
+ return /^ERROR:\s*The system was unable to find the specified registry key or value\.?$/iu.test(detail) || /^错误[::]\s*系统找不到指定的注册表项或值[。.]?$/u.test(detail);
2254
+ }
2255
+ /**
2256
+ * Parse the value columns emitted by `reg query ... /s`.
2257
+ *
2258
+ * Entries are filtered by the product's DisplayName pattern *before* the
2259
+ * DisplayIcon is judged, so another product's records — however broken — are
2260
+ * excluded as decisively not ours and can never mark this product's search
2261
+ * incomplete.
2262
+ */
2263
+ function parseWindowsRegistryOutput(output, displayNamePattern) {
2264
+ const entries = /* @__PURE__ */ new Map();
2265
+ let currentKey;
2266
+ for (const line of output.split(/\r?\n/u)) {
2267
+ const keyMatch = /^\s*(HKEY_[^\r\n]+?)\s*$/iu.exec(line);
2268
+ if (keyMatch !== null) {
2269
+ currentKey = keyMatch[1];
2270
+ entries.set(currentKey, {});
2271
+ continue;
2272
+ }
2273
+ if (currentKey === void 0) continue;
2274
+ const valueMatch = /^\s+(DisplayName|DisplayIcon)\s+REG_[A-Z0-9_]+\s*(.*?)\s*$/iu.exec(line);
2275
+ if (valueMatch === null) continue;
2276
+ const entry = entries.get(currentKey);
2277
+ if (entry === void 0) continue;
2278
+ const value = valueMatch[2] ?? "";
2279
+ if (valueMatch[1].toLowerCase() === "displayname") entry.displayName = value;
2280
+ else entry.displayIcon = value;
2281
+ }
2282
+ const candidates = [];
2283
+ let incomplete = false;
2284
+ for (const entry of entries.values()) {
2285
+ if (entry.displayName === void 0) continue;
2286
+ if (!displayNamePattern.test(entry.displayName.trim())) continue;
2287
+ const displayIcon = entry.displayIcon === void 0 ? void 0 : parseWindowsDisplayIcon(entry.displayIcon);
2288
+ if (displayIcon === void 0) incomplete = true;
2289
+ else candidates.push(displayIcon);
2290
+ }
2291
+ return {
2292
+ candidates,
2293
+ incomplete
2294
+ };
2295
+ }
2296
+ /** Read a quoted DisplayIcon path and remove the Windows icon-index suffix. */
2297
+ function parseWindowsDisplayIcon(value) {
2298
+ const raw = value.trim();
2299
+ let path;
2300
+ if (raw.startsWith("\"")) {
2301
+ const closingQuote = raw.indexOf("\"", 1);
2302
+ if (closingQuote < 0) return void 0;
2303
+ const suffix = raw.slice(closingQuote + 1).trim();
2304
+ if (suffix !== "" && !/^,\d+$/u.test(suffix)) return void 0;
2305
+ path = raw.slice(1, closingQuote).replace(/,\d+$/u, "");
2306
+ } else {
2307
+ const match = /^(.+?\.exe)(?:,\d+)?$/iu.exec(raw);
2308
+ if (match === null) return void 0;
2309
+ path = match[1];
2310
+ }
2311
+ path = path.trim();
2312
+ return /\.exe$/iu.test(path) ? path : void 0;
2313
+ }
2314
+ /**
2315
+ * Validate the known Windows layout for the product's exe. `undefined` is a
2316
+ * decidable exclusion; `unresolved` is reserved for errors that prevent
2317
+ * checking.
2318
+ */
2319
+ function inspectWindowsElectronCandidate(electronPath, platform, exeBasename) {
2320
+ if (platform !== "win32" || basename(electronPath).toLowerCase() !== exeBasename) return void 0;
2321
+ let binaryStat;
2322
+ try {
2323
+ binaryStat = statSync(electronPath);
2324
+ } catch (error) {
2325
+ return isENOENT$1(error) ? void 0 : "unresolved";
2326
+ }
2327
+ if (!binaryStat.isFile()) return void 0;
2328
+ try {
2329
+ accessSync(electronPath, constants.X_OK);
2330
+ } catch (error) {
2331
+ return isENOENT$1(error) ? void 0 : "unresolved";
2332
+ }
2333
+ const installRoot = dirname(electronPath);
2334
+ let version;
2335
+ try {
2336
+ version = readFileSync(join(installRoot, "version"), "utf8").trim();
2337
+ } catch (error) {
2338
+ return isENOENT$1(error) ? void 0 : "unresolved";
2339
+ }
2340
+ if (!WINDOWS_ELECTRON_VERSION_PATTERN.test(version)) return void 0;
2341
+ let resourcesStat;
2342
+ try {
2343
+ resourcesStat = statSync(join(installRoot, "resources", "app.asar"));
2344
+ } catch (error) {
2345
+ return isENOENT$1(error) ? void 0 : "unresolved";
2346
+ }
2347
+ if (!resourcesStat.isFile()) return void 0;
2348
+ let identity;
2349
+ try {
2350
+ identity = realpathSync(electronPath);
2351
+ } catch (error) {
2352
+ return isENOENT$1(error) ? void 0 : "unresolved";
2353
+ }
2354
+ return {
2355
+ electronPath,
2356
+ identity: platform === "win32" ? identity.toLowerCase() : identity
2357
+ };
2358
+ }
1954
2359
  /** Whether a path exists and is executable; never throws. */
1955
2360
  function isExecutable(path) {
1956
2361
  try {
@@ -1981,8 +2386,8 @@ function reasonCodeOf(error) {
1981
2386
  function isENOENT$1(error) {
1982
2387
  return error?.code === "ENOENT";
1983
2388
  }
1984
- function discoveryIncomplete(detail) {
1985
- return new WorkBuddyElectronPathError("electron-discovery-incomplete", `the WorkBuddy application search did not finish (${detail}); this is not proof that the app is missing`);
2389
+ function discoveryIncomplete(productName, detail) {
2390
+ return new WorkBuddyElectronPathError("electron-discovery-incomplete", `the ${productName} application search did not finish (${detail}); this is not proof that the app is missing`);
1986
2391
  }
1987
2392
  /**
1988
2393
  * The helper: run inside WorkBuddy's Electron as plain Node, where the
@@ -2199,7 +2604,7 @@ var WorkBuddyCredentialStore = class {
2199
2604
  this.refresh = options.refresh;
2200
2605
  this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
2201
2606
  this.ownPath = options.ownPath ?? (options.variant ? join(resolveDshHome(), options.variant.ownFilename) : workbuddyOwnAuthPath());
2202
- this.keyProvider = options.keyProvider ?? new WorkBuddyAtRestKeyProvider();
2607
+ this.keyProvider = options.keyProvider ?? new WorkBuddyAtRestKeyProvider({ product: electronProfileFor(options.variant) });
2203
2608
  this.desktopPathOverride = options.desktopPath;
2204
2609
  }
2205
2610
  /**
@@ -3203,33 +3608,8 @@ var WorkBuddyCatalog = class {
3203
3608
  }
3204
3609
  };
3205
3610
  //#endregion
3206
- //#region src/status-paths.ts
3207
- /** Node-free constants and types shared by the Host and browser halves. */
3208
- /** Plugin-owned status endpoint consumed by its browser half. */
3209
- const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-connect/status";
3210
- /**
3211
- * Plugin-owned probe control endpoint.
3212
- *
3213
- * Separate from the status route because it accepts writes: the status route's
3214
- * loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
3215
- * not the same as authorizing a state-changing action. This route therefore
3216
- * also requires the in-process key the browser half receives with the status
3217
- * document.
3218
- */
3219
- const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-connect/probe";
3220
- /**
3221
- * The international (WorkBuddy AI) variant's own pair of routes.
3222
- *
3223
- * Kept as separate constants rather than a computed suffix so both halves
3224
- * reference literal strings: the browser bundle and the host bundle are built
3225
- * independently, and a shared expression is one build-config drift away from
3226
- * the desk asking a route the host never mounted.
3227
- */
3228
- const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
3229
- const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
3230
- //#endregion
3231
3611
  //#region src/version.ts
3232
- const WORKBUDDY_CONNECT_VERSION = "0.6.2";
3612
+ const WORKBUDDY_CONNECT_VERSION = "0.6.4";
3233
3613
  //#endregion
3234
3614
  //#region src/host-heartbeat.ts
3235
3615
  /**
@@ -3400,58 +3780,4 @@ function isHeartbeatProcessAlive(heartbeat) {
3400
3780
  return startAtMs <= heartbeat.registeredAt;
3401
3781
  }
3402
3782
  //#endregion
3403
- //#region src/variants.ts
3404
- /**
3405
- * The two WorkBuddy desktop apps this one plugin serves.
3406
- *
3407
- * Both products are the same client framework in different regions, and both
3408
- * write their sign-in into the *same* shared `CodeBuddyExtension` auth
3409
- * directory — they differ by file basename, base URL, catalog endpoint, and
3410
- * display identity. Everything that varies between them is collected here as
3411
- * one descriptor, so no module has to carry its own `if (international)`
3412
- * branch and a third variant would be a data change rather than a refactor.
3413
- *
3414
- * This module is host-side (it names files and env vars). The browser half
3415
- * takes the same ids and routes from the Node-free `status-paths.ts`, which
3416
- * stays the single source shared by both halves.
3417
- *
3418
- * @module dsh-workbuddy-connect/variants
3419
- */
3420
- /** CN WorkBuddy first: the existing provider keeps its id, paths, and copy. */
3421
- const WORKBUDDY_VARIANTS = [{
3422
- id: "workbuddy",
3423
- displayName: "WorkBuddy",
3424
- appName: "WorkBuddy",
3425
- region: "cn",
3426
- env: "WORKBUDDY_AUTH_FILE",
3427
- desktopFilename: "workbuddy-desktop.info",
3428
- ownFilename: ".workbuddy-auth.json",
3429
- probeFilename: ".workbuddy-probe.json",
3430
- catalogFilename: ".workbuddy-catalog.json",
3431
- visibilityFilename: ".workbuddy-model-visibility.json",
3432
- statusPath: WORKBUDDY_STATUS_PATH,
3433
- probePath: WORKBUDDY_PROBE_PATH
3434
- }, {
3435
- id: "workbuddy-ai",
3436
- displayName: "WorkBuddy AI",
3437
- appName: "WorkBuddy AI",
3438
- region: "global",
3439
- env: "WORKBUDDY_AI_AUTH_FILE",
3440
- desktopFilename: "workbuddy-desktop-ai.info",
3441
- ownFilename: ".workbuddy-ai-auth.json",
3442
- probeFilename: ".workbuddy-ai-probe.json",
3443
- catalogFilename: ".workbuddy-ai-catalog.json",
3444
- visibilityFilename: ".workbuddy-ai-model-visibility.json",
3445
- statusPath: WORKBUDDY_AI_STATUS_PATH,
3446
- probePath: WORKBUDDY_AI_PROBE_PATH
3447
- }];
3448
- /** The CN variant; the plugin's long-standing default and compatibility anchor. */
3449
- const CN_VARIANT = WORKBUDDY_VARIANTS[0];
3450
- /** The international variant. */
3451
- const AI_VARIANT = WORKBUDDY_VARIANTS[1];
3452
- /** Look up a variant by provider id. */
3453
- function variantFor(id) {
3454
- return WORKBUDDY_VARIANTS.find((variant) => variant.id === id);
3455
- }
3456
- //#endregion
3457
- export { normalizeCredits as A, chatUserAgent as B, desktopAuthCandidatesFor as C, WorkBuddyUpstreamClient as D, WorkBuddyAtRestKeyProvider as E, PROBE_EFFORT_CANDIDATES as F, WORKBUDDY_APP_VERSION_FILENAME as G, readCliVersion as H, probeModel as I, readBundleVersion as J, appUserAgent as K, randomSentinel as L, prepareChatBody as M, prepareInternationalChatBody as N, classifyUpstreamError as O, regionOf as P, CN_APP_VERSION_FILENAME as R, defaultDesktopAuthPath as S, workbuddyOwnAuthPath as T, resolveChatIdentity as U, fallbackChatIdentity as V, validCliVersion as W, validAppVersion as X, resolveAppVersion as Y, WorkBuddyCatalog as _, WORKBUDDY_HOST_HEARTBEAT_FILENAME as a, WorkBuddyCredentialStore as b, processStartTimeMs as c, writeHostHeartbeat as d, WORKBUDDY_CONNECT_VERSION as f, FALLBACK_WORKBUDDY_MODELS as g, FALLBACK_WORKBUDDY_AI_MODELS as h, variantFor as i, parseModelCatalog as j, modelWithCurrentPromotion as k, readHostHeartbeat as l, WORKBUDDY_STATUS_PATH as m, CN_VARIANT as n, clearHostHeartbeat as o, WORKBUDDY_PROBE_PATH as p, installedAppVersion as q, WORKBUDDY_VARIANTS as r, isHeartbeatProcessAlive as s, AI_VARIANT as t, workbuddyHostHeartbeatPath as u, WORKBUDDY_AUTH_FILENAME as v, parseWorkBuddyAuth as w, defaultDesktopAuthCandidates as x, WORKBUDDY_AUTH_FILE_ENV as y, FALLBACK_CN_APP_VERSION as z };
3783
+ export { extractDisplayErrorMessage as A, CN_APP_VERSION_FILENAME as B, WORKBUDDY_VARIANTS as C, WORKBUDDY_UPDATE_PATH as D, WORKBUDDY_STATUS_PATH as E, prepareInternationalChatBody as F, resolveChatIdentity as G, chatUserAgent as H, regionOf as I, appUserAgent as J, validCliVersion as K, PROBE_EFFORT_CANDIDATES as L, normalizeCredits as M, parseModelCatalog as N, WorkBuddyUpstreamClient as O, prepareChatBody as P, validAppVersion as Q, probeModel as R, CN_VARIANT as S, WORKBUDDY_PROBE_PATH as T, fallbackChatIdentity as U, FALLBACK_CN_APP_VERSION as V, readCliVersion as W, readBundleVersion as X, installedAppVersion as Y, resolveAppVersion as Z, desktopAuthCandidatesFor as _, readHostHeartbeat as a, atRestKeyProviderFor as b, WORKBUDDY_CONNECT_VERSION as c, WorkBuddyCatalog as d, WORKBUDDY_AUTH_FILENAME as f, defaultDesktopAuthPath as g, defaultDesktopAuthCandidates as h, processStartTimeMs as i, modelWithCurrentPromotion as j, classifyUpstreamError as k, FALLBACK_WORKBUDDY_AI_MODELS as l, WorkBuddyCredentialStore as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, WORKBUDDY_AUTH_FILE_ENV as p, WORKBUDDY_APP_VERSION_FILENAME as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, FALLBACK_WORKBUDDY_MODELS as u, parseWorkBuddyAuth as v, variantFor as w, AI_VARIANT as x, workbuddyOwnAuthPath as y, randomSentinel as z };