dsh-workbuddy-connect 0.6.3 → 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.
@@ -629,6 +629,7 @@ const SESSION_DEAD_MARKERS = ["Offline user session not found", "12153"];
629
629
  /** Classify an upstream failure from its HTTP status and body excerpt. */
630
630
  function classifyUpstreamError(status, body) {
631
631
  if (status === 402) return "hard_credit";
632
+ if (status === 401) return "session_dead";
632
633
  const lower = body.toLowerCase();
633
634
  for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
634
635
  for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
@@ -638,6 +639,26 @@ function classifyUpstreamError(status, body) {
638
639
  if (status >= 400) return "client";
639
640
  return "client";
640
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
+ }
641
662
  /** Region for a login domain; an empty domain means CN (matching upstream tooling). */
642
663
  function regionOf(domain) {
643
664
  const lowered = domain.trim().toLowerCase();
@@ -1453,15 +1474,143 @@ const INTERNATIONAL_SYSTEM_PROMPT = "You are a helpful assistant.";
1453
1474
  */
1454
1475
  const INTERNATIONAL_PROBE_MAX_TOKENS = 16;
1455
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
1456
1604
  //#region src/desktop-credential-protection.ts
1457
1605
  /**
1458
1606
  * WorkBuddy 5.6.x at-rest credential protection: classification, key
1459
- * resolution, and field decryption for the desktop app's encrypted auth file.
1607
+ * resolution, and field decryption for the desktop apps' encrypted auth files.
1460
1608
  *
1461
- * Since WorkBuddy 5.6 the desktop app encrypts `auth.accessToken` and
1462
- * `auth.refreshToken` at rest (`buildPolicy: "fields"`, on by default), so the
1463
- * plugin reads `{$wbEncrypted:1, envelope}` wrappers instead of token strings
1464
- * (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:
1465
1614
  *
1466
1615
  * - the sealed payload (`{version:1, atRestSecretKey}`) comes from the
1467
1616
  * WorkBuddy-modified Electron's private `workbuddyStorage` binding, reached
@@ -1479,21 +1628,17 @@ const INTERNATIONAL_PROBE_MAX_TOKENS = 16;
1479
1628
  *
1480
1629
  * @module dsh-workbuddy-connect/desktop-credential-protection
1481
1630
  */
1482
- /** Env variable that overrides the WorkBuddy Electron binary used as the key helper. */
1483
- const WORKBUDDY_ELECTRON_BIN_ENV = "WORKBUDDY_ELECTRON_BIN";
1484
- /** Platform-default Electron binaries confirmed for the CN app (5.6.2). */
1485
- const MACOS_ELECTRON_PATH = "/Applications/WorkBuddy.app/Contents/MacOS/Electron";
1486
1631
  /**
1487
- * The Electron binary the helper would spawn on this platform, or `undefined`
1488
- * where no default has been verified. Windows requires `LOCALAPPDATA` to be
1489
- * present; platforms without a verified layout must set
1490
- * {@link WORKBUDDY_ELECTRON_BIN_ENV} explicitly.
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.
1491
1636
  */
1492
- function defaultWorkBuddyElectronPath(platform = process.platform) {
1493
- if (platform === "darwin") return MACOS_ELECTRON_PATH;
1494
- if (platform !== "win32") return 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;
1495
1640
  const localAppData = process.env.LOCALAPPDATA?.trim();
1496
- return localAppData === void 0 || localAppData === "" ? void 0 : join(localAppData, "Programs", "WorkBuddy", "WorkBuddy.exe");
1641
+ return localAppData === void 0 || localAppData === "" ? void 0 : join(localAppData, ...product.windows.defaultPathSegments);
1497
1642
  }
1498
1643
  /** Distinct key ids across the wrapped fields, in field order. */
1499
1644
  function keyIdsOf(fields) {
@@ -1674,15 +1819,27 @@ function parseAtRestPayload(text) {
1674
1819
  function deriveProtectorKey(secret) {
1675
1820
  return createHash("sha256").update(secret, "utf8").digest();
1676
1821
  }
1677
- /** Select discovery from product and platform at the composition root. */
1678
- function electronDiscoveryFor(variant, platform = process.platform) {
1679
- if (variant.region !== "cn") return "none";
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) {
1680
1828
  if (platform === "darwin") return "macos-workbuddy";
1681
1829
  if (platform === "win32") return "windows-workbuddy";
1682
1830
  return "none";
1683
1831
  }
1684
- /** The CN app's bundle id; the only one this round resolves by discovery. */
1685
- const WORKBUDDY_CN_BUNDLE_ID = "com.tencent.workbuddy.mac";
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
+ }
1686
1843
  /** Absolute tool paths: never resolved through PATH, which a user can change. */
1687
1844
  const MDFIND_BIN = "/usr/bin/mdfind";
1688
1845
  const PLUTIL_BIN = "/usr/bin/plutil";
@@ -1693,7 +1850,6 @@ const WINDOWS_REGISTRY_ROOTS = [
1693
1850
  ];
1694
1851
  const WINDOWS_REGISTRY_OUTPUT_MAX_BYTES = 1048576;
1695
1852
  const WINDOWS_ELECTRON_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u;
1696
- const WINDOWS_WORKBUDDY_DISPLAY_NAME_PATTERN = /^WorkBuddy(?:\s+\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.-]+)?)?$/u;
1697
1853
  /** One discovery subprocess's own limits; see {@link WorkBuddyAtRestKeyProviderOptions}. */
1698
1854
  const WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS = 3e3;
1699
1855
  const MDFIND_MAX_OUTPUT_BYTES = 1048576;
@@ -1710,8 +1866,12 @@ var DiscoveryIncompleteError = class extends Error {};
1710
1866
  * identity. Every failure that means "we could not tell" — a missing tool, a
1711
1867
  * timeout, an oversized answer — is raised as {@link DiscoveryIncompleteError}
1712
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.
1713
1873
  */
1714
- function workBuddyDiscoveryTools() {
1874
+ function workBuddyDiscoveryTools(bundleId) {
1715
1875
  const runTool = (bin, args, maxBytes, signal) => new Promise((resolve, reject) => {
1716
1876
  if (signal.aborted) {
1717
1877
  reject(new DiscoveryIncompleteError(`${bin} was not started: the discovery budget was already spent`));
@@ -1743,7 +1903,7 @@ function workBuddyDiscoveryTools() {
1743
1903
  });
1744
1904
  return {
1745
1905
  findApps: async (signal) => {
1746
- 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"));
1747
1907
  },
1748
1908
  bundleIdentifier: async (bundlePath, signal) => {
1749
1909
  try {
@@ -1840,6 +2000,7 @@ function workBuddyWindowsDiscoveryTools() {
1840
2000
  * a different app.
1841
2001
  */
1842
2002
  explicitPath;
2003
+ product;
1843
2004
  defaultPath;
1844
2005
  discovery;
1845
2006
  tools;
@@ -1857,14 +2018,15 @@ function workBuddyWindowsDiscoveryTools() {
1857
2018
  discoveredPath;
1858
2019
  cache;
1859
2020
  inflight;
1860
- constructor(options = {}) {
1861
- 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();
1862
2024
  const envPath = fromEnv === void 0 || fromEnv === "" ? void 0 : fromEnv;
1863
2025
  this.explicitPath = options.electronPath ?? envPath;
1864
2026
  this.discovery = options.discovery ?? "none";
1865
2027
  this.platform = options.platform ?? process.platform;
1866
- this.defaultPath = this.discovery === "none" ? void 0 : options.defaultElectronPath === void 0 ? defaultWorkBuddyElectronPath(this.platform) : options.defaultElectronPath ?? void 0;
1867
- this.tools = options.tools ?? workBuddyDiscoveryTools();
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);
1868
2030
  this.windowsTools = options.windowsTools ?? workBuddyWindowsDiscoveryTools();
1869
2031
  this.discoveryBudgetMs = options.discoveryBudgetMs ?? 1e4;
1870
2032
  this.timeoutMs = options.timeoutMs ?? 1e4;
@@ -1923,10 +2085,10 @@ function workBuddyWindowsDiscoveryTools() {
1923
2085
  */
1924
2086
  async resolveElectronPath() {
1925
2087
  if (this.explicitPath !== void 0) {
1926
- 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`);
1927
2089
  return this.explicitPath;
1928
2090
  }
1929
- 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`);
1930
2092
  if (this.defaultPath !== void 0 && isExecutable(this.defaultPath)) return this.defaultPath;
1931
2093
  if (this.discoveredPath !== void 0) {
1932
2094
  if (isExecutable(this.discoveredPath)) return this.discoveredPath;
@@ -1937,15 +2099,15 @@ function workBuddyWindowsDiscoveryTools() {
1937
2099
  return found;
1938
2100
  }
1939
2101
  /**
1940
- * Resolve the CN app through Spotlight, then prove each candidate's identity
1941
- * 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.
1942
2104
  *
1943
2105
  * The whole flow shares one budget: a hang in one candidate must not extend
1944
2106
  * the wait for the others, and running out of budget is reported as an
1945
2107
  * unfinished check rather than an absent app.
1946
2108
  */
1947
2109
  async discoverMacosApp() {
1948
- if (this.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`);
1949
2111
  const controller = new AbortController();
1950
2112
  const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
1951
2113
  try {
@@ -1953,7 +2115,7 @@ function workBuddyWindowsDiscoveryTools() {
1953
2115
  try {
1954
2116
  candidates = await this.tools.findApps(controller.signal);
1955
2117
  } catch {
1956
- throw discoveryIncomplete("the app search did not complete");
2118
+ throw discoveryIncomplete(this.product.productName, "the app search did not complete");
1957
2119
  }
1958
2120
  const seen = /* @__PURE__ */ new Map();
1959
2121
  let unresolved = false;
@@ -1976,7 +2138,7 @@ function workBuddyWindowsDiscoveryTools() {
1976
2138
  unresolved = true;
1977
2139
  continue;
1978
2140
  }
1979
- if (bundleIdentifier !== "com.tencent.workbuddy.mac") continue;
2141
+ if (bundleIdentifier !== this.product.macOS.bundleId) continue;
1980
2142
  const electronPath = join(candidate, "Contents", "MacOS", "Electron");
1981
2143
  if (!isExecutable(electronPath)) continue;
1982
2144
  let identity;
@@ -1998,9 +2160,12 @@ function workBuddyWindowsDiscoveryTools() {
1998
2160
  ...version === void 0 ? {} : { version }
1999
2161
  });
2000
2162
  }
2001
- 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`);
2002
- if (unresolved) throw discoveryIncomplete("some candidates could not be checked");
2003
- 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`);
2004
2169
  return [...seen.values()][0].electronPath;
2005
2170
  } finally {
2006
2171
  clearTimeout(timer);
@@ -2008,12 +2173,13 @@ function workBuddyWindowsDiscoveryTools() {
2008
2173
  }
2009
2174
  }
2010
2175
  /**
2011
- * Resolve the CN app through Windows uninstall records. Registry entries
2012
- * provide hints, not trust: every DisplayIcon candidate must still be a
2013
- * WorkBuddy Electron binary with the known Electron layout before execution.
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.
2014
2180
  */
2015
2181
  async discoverWindowsApp() {
2016
- if (this.platform !== "win32") 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`);
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`);
2017
2183
  const controller = new AbortController();
2018
2184
  const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
2019
2185
  try {
@@ -2027,13 +2193,13 @@ function workBuddyWindowsDiscoveryTools() {
2027
2193
  unresolved = true;
2028
2194
  continue;
2029
2195
  }
2030
- const parsed = parseWindowsRegistryOutput(output);
2196
+ const parsed = parseWindowsRegistryOutput(output, this.product.windows.displayNamePattern);
2031
2197
  candidates.push(...parsed.candidates);
2032
2198
  unresolved ||= parsed.incomplete;
2033
2199
  }
2034
2200
  const seen = /* @__PURE__ */ new Map();
2035
2201
  for (const candidate of new Set(candidates)) {
2036
- const inspection = inspectWindowsElectronCandidate(candidate, this.platform);
2202
+ const inspection = inspectWindowsElectronCandidate(candidate, this.platform, this.product.windows.exeBasename);
2037
2203
  if (inspection === "unresolved") {
2038
2204
  unresolved = true;
2039
2205
  continue;
@@ -2041,9 +2207,12 @@ function workBuddyWindowsDiscoveryTools() {
2041
2207
  if (inspection === void 0) continue;
2042
2208
  seen.set(inspection.identity, inspection.electronPath);
2043
2209
  }
2044
- if (unresolved) throw discoveryIncomplete("some registry entries or candidates could not be checked");
2045
- 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((path) => ` - ${path}`).join("\n")}\n set ${WORKBUDDY_ELECTRON_BIN_ENV} to the one to use`);
2046
- if (seen.size === 0) throw new WorkBuddyElectronPathError("electron-binary-not-found", "no usable WorkBuddy Electron binary was found in the default location or Windows uninstall records");
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`);
2047
2216
  return [...seen.values()][0];
2048
2217
  } finally {
2049
2218
  clearTimeout(timer);
@@ -2083,8 +2252,15 @@ function windowsRegistryKeyMissing(stderr) {
2083
2252
  const detail = stderr.trim();
2084
2253
  return /^ERROR:\s*The system was unable to find the specified registry key or value\.?$/iu.test(detail) || /^错误[::]\s*系统找不到指定的注册表项或值[。.]?$/u.test(detail);
2085
2254
  }
2086
- /** Parse the value columns emitted by `reg query ... /s`. */
2087
- function parseWindowsRegistryOutput(output) {
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) {
2088
2264
  const entries = /* @__PURE__ */ new Map();
2089
2265
  let currentKey;
2090
2266
  for (const line of output.split(/\r?\n/u)) {
@@ -2107,7 +2283,7 @@ function parseWindowsRegistryOutput(output) {
2107
2283
  let incomplete = false;
2108
2284
  for (const entry of entries.values()) {
2109
2285
  if (entry.displayName === void 0) continue;
2110
- if (!WINDOWS_WORKBUDDY_DISPLAY_NAME_PATTERN.test(entry.displayName.trim())) continue;
2286
+ if (!displayNamePattern.test(entry.displayName.trim())) continue;
2111
2287
  const displayIcon = entry.displayIcon === void 0 ? void 0 : parseWindowsDisplayIcon(entry.displayIcon);
2112
2288
  if (displayIcon === void 0) incomplete = true;
2113
2289
  else candidates.push(displayIcon);
@@ -2136,11 +2312,12 @@ function parseWindowsDisplayIcon(value) {
2136
2312
  return /\.exe$/iu.test(path) ? path : void 0;
2137
2313
  }
2138
2314
  /**
2139
- * Validate the known WorkBuddy Windows layout. `undefined` is a decidable
2140
- * exclusion; `unresolved` is reserved for errors that prevent checking.
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.
2141
2318
  */
2142
- function inspectWindowsElectronCandidate(electronPath, platform) {
2143
- if (platform !== "win32" || basename(electronPath).toLowerCase() !== "workbuddy.exe") return void 0;
2319
+ function inspectWindowsElectronCandidate(electronPath, platform, exeBasename) {
2320
+ if (platform !== "win32" || basename(electronPath).toLowerCase() !== exeBasename) return void 0;
2144
2321
  let binaryStat;
2145
2322
  try {
2146
2323
  binaryStat = statSync(electronPath);
@@ -2209,8 +2386,8 @@ function reasonCodeOf(error) {
2209
2386
  function isENOENT$1(error) {
2210
2387
  return error?.code === "ENOENT";
2211
2388
  }
2212
- function discoveryIncomplete(detail) {
2213
- 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`);
2214
2391
  }
2215
2392
  /**
2216
2393
  * The helper: run inside WorkBuddy's Electron as plain Node, where the
@@ -2427,7 +2604,7 @@ var WorkBuddyCredentialStore = class {
2427
2604
  this.refresh = options.refresh;
2428
2605
  this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
2429
2606
  this.ownPath = options.ownPath ?? (options.variant ? join(resolveDshHome(), options.variant.ownFilename) : workbuddyOwnAuthPath());
2430
- this.keyProvider = options.keyProvider ?? new WorkBuddyAtRestKeyProvider();
2607
+ this.keyProvider = options.keyProvider ?? new WorkBuddyAtRestKeyProvider({ product: electronProfileFor(options.variant) });
2431
2608
  this.desktopPathOverride = options.desktopPath;
2432
2609
  }
2433
2610
  /**
@@ -3431,33 +3608,8 @@ var WorkBuddyCatalog = class {
3431
3608
  }
3432
3609
  };
3433
3610
  //#endregion
3434
- //#region src/status-paths.ts
3435
- /** Node-free constants and types shared by the Host and browser halves. */
3436
- /** Plugin-owned status endpoint consumed by its browser half. */
3437
- const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-connect/status";
3438
- /**
3439
- * Plugin-owned probe control endpoint.
3440
- *
3441
- * Separate from the status route because it accepts writes: the status route's
3442
- * loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
3443
- * not the same as authorizing a state-changing action. This route therefore
3444
- * also requires the in-process key the browser half receives with the status
3445
- * document.
3446
- */
3447
- const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-connect/probe";
3448
- /**
3449
- * The international (WorkBuddy AI) variant's own pair of routes.
3450
- *
3451
- * Kept as separate constants rather than a computed suffix so both halves
3452
- * reference literal strings: the browser bundle and the host bundle are built
3453
- * independently, and a shared expression is one build-config drift away from
3454
- * the desk asking a route the host never mounted.
3455
- */
3456
- const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
3457
- const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
3458
- //#endregion
3459
3611
  //#region src/version.ts
3460
- const WORKBUDDY_CONNECT_VERSION = "0.6.3";
3612
+ const WORKBUDDY_CONNECT_VERSION = "0.6.4";
3461
3613
  //#endregion
3462
3614
  //#region src/host-heartbeat.ts
3463
3615
  /**
@@ -3628,58 +3780,4 @@ function isHeartbeatProcessAlive(heartbeat) {
3628
3780
  return startAtMs <= heartbeat.registeredAt;
3629
3781
  }
3630
3782
  //#endregion
3631
- //#region src/variants.ts
3632
- /**
3633
- * The two WorkBuddy desktop apps this one plugin serves.
3634
- *
3635
- * Both products are the same client framework in different regions, and both
3636
- * write their sign-in into the *same* shared `CodeBuddyExtension` auth
3637
- * directory — they differ by file basename, base URL, catalog endpoint, and
3638
- * display identity. Everything that varies between them is collected here as
3639
- * one descriptor, so no module has to carry its own `if (international)`
3640
- * branch and a third variant would be a data change rather than a refactor.
3641
- *
3642
- * This module is host-side (it names files and env vars). The browser half
3643
- * takes the same ids and routes from the Node-free `status-paths.ts`, which
3644
- * stays the single source shared by both halves.
3645
- *
3646
- * @module dsh-workbuddy-connect/variants
3647
- */
3648
- /** CN WorkBuddy first: the existing provider keeps its id, paths, and copy. */
3649
- const WORKBUDDY_VARIANTS = [{
3650
- id: "workbuddy",
3651
- displayName: "WorkBuddy",
3652
- appName: "WorkBuddy",
3653
- region: "cn",
3654
- env: "WORKBUDDY_AUTH_FILE",
3655
- desktopFilename: "workbuddy-desktop.info",
3656
- ownFilename: ".workbuddy-auth.json",
3657
- probeFilename: ".workbuddy-probe.json",
3658
- catalogFilename: ".workbuddy-catalog.json",
3659
- visibilityFilename: ".workbuddy-model-visibility.json",
3660
- statusPath: WORKBUDDY_STATUS_PATH,
3661
- probePath: WORKBUDDY_PROBE_PATH
3662
- }, {
3663
- id: "workbuddy-ai",
3664
- displayName: "WorkBuddy AI",
3665
- appName: "WorkBuddy AI",
3666
- region: "global",
3667
- env: "WORKBUDDY_AI_AUTH_FILE",
3668
- desktopFilename: "workbuddy-desktop-ai.info",
3669
- ownFilename: ".workbuddy-ai-auth.json",
3670
- probeFilename: ".workbuddy-ai-probe.json",
3671
- catalogFilename: ".workbuddy-ai-catalog.json",
3672
- visibilityFilename: ".workbuddy-ai-model-visibility.json",
3673
- statusPath: WORKBUDDY_AI_STATUS_PATH,
3674
- probePath: WORKBUDDY_AI_PROBE_PATH
3675
- }];
3676
- /** The CN variant; the plugin's long-standing default and compatibility anchor. */
3677
- const CN_VARIANT = WORKBUDDY_VARIANTS[0];
3678
- /** The international variant. */
3679
- const AI_VARIANT = WORKBUDDY_VARIANTS[1];
3680
- /** Look up a variant by provider id. */
3681
- function variantFor(id) {
3682
- return WORKBUDDY_VARIANTS.find((variant) => variant.id === id);
3683
- }
3684
- //#endregion
3685
- export { modelWithCurrentPromotion as A, FALLBACK_CN_APP_VERSION as B, desktopAuthCandidatesFor as C, electronDiscoveryFor as D, WorkBuddyAtRestKeyProvider as E, regionOf as F, validCliVersion as G, fallbackChatIdentity as H, PROBE_EFFORT_CANDIDATES as I, installedAppVersion as J, WORKBUDDY_APP_VERSION_FILENAME as K, probeModel as L, parseModelCatalog as M, prepareChatBody as N, WorkBuddyUpstreamClient as O, prepareInternationalChatBody as P, randomSentinel as R, defaultDesktopAuthPath as S, workbuddyOwnAuthPath as T, readCliVersion as U, chatUserAgent as V, resolveChatIdentity as W, resolveAppVersion as X, readBundleVersion as Y, validAppVersion as Z, 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, normalizeCredits as j, classifyUpstreamError as k, readHostHeartbeat as l, WORKBUDDY_STATUS_PATH as m, CN_VARIANT as n, clearHostHeartbeat as o, WORKBUDDY_PROBE_PATH as p, appUserAgent 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, CN_APP_VERSION_FILENAME 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 };