dsh-workbuddy-xdpool 1.7.2 → 1.7.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.
package/lib/bin.js CHANGED
@@ -1,15 +1,48 @@
1
1
  import { createDecipheriv, createHash, randomUUID } from "node:crypto";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
3
  import { copyFile, mkdir, readFile, readdir, rename, writeFile, writeFile as writeFile$1 } from "node:fs/promises";
3
4
  import { homedir, platform } from "node:os";
4
5
  import { basename, dirname, join, resolve } from "node:path";
5
6
  import { execFile, execFileSync } from "node:child_process";
6
- import { existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import z from "@deepseek-ai/schemastery";
8
8
  import "@earendil-works/pi-ai";
9
9
  import "@earendil-works/pi-ai/api/openai-completions.lazy";
10
10
  import "@deepseek-ai/dsh-llm";
11
11
  import "@deepseek-ai/dsh-llm-pi-ai";
12
12
  //#region src/upstream.ts
13
+ /**
14
+ * Backoff between catalog-fetch attempts.
15
+ *
16
+ * Three retries after the first try, so a startup network hiccup does not cost
17
+ * the user their model list for the whole session. The window covers the case
18
+ * that actually bit: several independent components reported `fetch failed`
19
+ * within the same second while the machine was still bringing its network up,
20
+ * and the very next request succeeded 0.7s later.
21
+ *
22
+ * Deliberately short and bounded. This runs during plugin startup, so a long
23
+ * ladder would delay the provider appearing at all; the total added latency
24
+ * here is ~12s in the worst case, and only when the network is genuinely down.
25
+ */
26
+ const CATALOG_RETRY_BACKOFF_MS = [
27
+ 1e3,
28
+ 3e3,
29
+ 8e3
30
+ ];
31
+ /**
32
+ * Sleep helper for the retry ladder.
33
+ *
34
+ * Deliberately NOT unref'd. An unref'd timer does not keep the event loop
35
+ * alive, so awaiting one can hang forever when nothing else is pending — the
36
+ * retry would simply never resume. That is exactly what the first version of
37
+ * this did, and it wedged the very first end-to-end run: `fetchModels` never
38
+ * settled. The wait is at most a few seconds and only happens on failure, so
39
+ * holding the loop open is the correct trade.
40
+ */
41
+ function delay(ms) {
42
+ return new Promise((resolve) => {
43
+ setTimeout(resolve, ms);
44
+ });
45
+ }
13
46
  /** CN chat base per dingminhua's on-machine probe (HTTP 200 for chat/models). */
14
47
  const CN_CHAT_BASE = "https://copilot.tencent.com";
15
48
  /** Billing/console base — `www.codebuddy.cn` is the billing origin. */
@@ -31,6 +64,13 @@ const MODELS_CATALOG_PATH = "/v2/enterprises/personal/models";
31
64
  const GLOBAL_CONFIG_PATH = "/v3/config";
32
65
  const JSON_TIMEOUT_MS = 3e4;
33
66
  const ERROR_BODY_LIMIT = 4096;
67
+ /** True for a request the CALLER aborted, which must never be retried. */
68
+ function isAbortError(error) {
69
+ if (typeof error !== "object" || error === null) return false;
70
+ if (error.name === "AbortError") return true;
71
+ const cause = error.cause;
72
+ return typeof cause === "object" && cause !== null && cause.name === "AbortError";
73
+ }
34
74
  /** Cap on the reassembled compaction reply, guarding against a runaway stream. */
35
75
  const COMPLETION_TEXT_LIMIT = 65536;
36
76
  /** Insufficient-credit markers, ASCII lowercase plus the original Chinese. */
@@ -638,9 +678,20 @@ function parseTask(value) {
638
678
  var WorkBuddyUpstreamClient = class {
639
679
  fetchImpl;
640
680
  clientVersion;
681
+ /** Backoff ladder for `fetchModels`; empty means a single attempt. */
682
+ catalogRetryBackoffMs;
683
+ /**
684
+ * Optional logger for retry notices.
685
+ *
686
+ * Set by the host so a retry is visible in the log with its attempt count —
687
+ * without it, a retry that eventually succeeds is invisible, and an operator
688
+ * debugging "why was the catalog slow" has nothing to look at.
689
+ */
690
+ logger;
641
691
  constructor(options = {}) {
642
692
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
643
693
  this.clientVersion = options.clientVersion ?? "2.0.4";
694
+ this.catalogRetryBackoffMs = options.catalogRetryBackoffMs ?? CATALOG_RETRY_BACKOFF_MS;
644
695
  }
645
696
  /**
646
697
  * Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
@@ -797,6 +848,30 @@ var WorkBuddyUpstreamClient = class {
797
848
  * below is common to the two branches.
798
849
  */
799
850
  async fetchModels(credential, signal) {
851
+ const attempts = this.catalogRetryBackoffMs.length + 1;
852
+ let lastError;
853
+ for (let attempt = 0; attempt < attempts; attempt += 1) try {
854
+ return await this.fetchModelsOnce(credential, signal);
855
+ } catch (error) {
856
+ lastError = error;
857
+ if (isAbortError(error)) throw error;
858
+ const wait = this.catalogRetryBackoffMs[attempt];
859
+ if (wait === void 0) break;
860
+ this.logger?.warn?.(`dsh-workbuddy-xdpool: model catalog fetch failed (attempt ${attempt + 1}/${attempts}), retrying in ${wait}ms`, error);
861
+ await delay(wait);
862
+ }
863
+ throw lastError;
864
+ }
865
+ /**
866
+ * One catalog attempt, without retries.
867
+ *
868
+ * Split out so {@link fetchModels} can retry it: the failure this guards
869
+ * against is a startup network hiccup, where several independent components
870
+ * see `fetch failed` inside the same second and the very next request
871
+ * succeeds — exactly the case a single attempt turns into "the user's model
872
+ * list is missing half its entries for the rest of the session".
873
+ */
874
+ async fetchModelsOnce(credential, signal) {
800
875
  const global = regionOf(credential.domain) === "global";
801
876
  const url = global ? `${globalBase(credential)}${GLOBAL_CONFIG_PATH}` : `${chatBase(credential)}${MODELS_CATALOG_PATH}`;
802
877
  const headers = global ? {
@@ -1815,42 +1890,155 @@ function macosBundleExecutable(bundle) {
1815
1890
  return join(bundle, "Contents", "MacOS", name);
1816
1891
  }
1817
1892
  /**
1893
+ * Encodings a Windows console child process may emit in, most likely first.
1894
+ *
1895
+ * `reg.exe` writes with the ACTIVE CODE PAGE, never UTF-8: a Chinese Windows
1896
+ * (code page 936) returns a path containing 「腾讯」 as GBK bytes. Decoding
1897
+ * those as UTF-8 mangles them into `��Ѷ`, every `existsSync` then fails, and the
1898
+ * plugin concludes no desktop app is installed while one is running fine.
1899
+ *
1900
+ * The list is ordered and each entry is TRIED rather than guessed: the first
1901
+ * decoding that yields usable text wins, and the decision is validated against
1902
+ * the filesystem rather than by sniffing bytes.
1903
+ */
1904
+ const WINDOWS_CONSOLE_ENCODINGS = [
1905
+ "utf-8",
1906
+ "gbk",
1907
+ "big5",
1908
+ "shift_jis",
1909
+ "euc-kr",
1910
+ "windows-1252"
1911
+ ];
1912
+ /**
1913
+ * Decode bytes from a Windows console child process.
1914
+ *
1915
+ * `accept` decides whether a decoding is usable — the callers pass "this text
1916
+ * contains a path that actually exists". Bytes that are already UTF-8 pass on
1917
+ * the first attempt, so the healthy case pays nothing.
1918
+ */
1919
+ function decodeWindowsConsole(buf, accept) {
1920
+ for (const label of WINDOWS_CONSOLE_ENCODINGS) {
1921
+ let text;
1922
+ try {
1923
+ text = new TextDecoder(label, { fatal: false }).decode(buf);
1924
+ } catch {
1925
+ continue;
1926
+ }
1927
+ if (accept(text)) return text;
1928
+ }
1929
+ return new TextDecoder("utf-8", { fatal: false }).decode(buf);
1930
+ }
1931
+ /** Strip the `"<path>",<index>` decoration DisplayIcon carries. */
1932
+ function cleanIconPath(raw) {
1933
+ return raw.replace(/^"/u, "").replace(/",-?\d+$/u, "").replace(/,-?\d+$/u, "").trim();
1934
+ }
1935
+ /** Registry locations the installer records its own product under. */
1936
+ const UNINSTALL_KEY_ROOTS = [
1937
+ "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
1938
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
1939
+ "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
1940
+ ];
1941
+ /**
1818
1942
  * Windows install locations recorded by the app's own uninstaller.
1819
1943
  *
1820
1944
  * The registry is the authoritative answer: it survives a non-default drive, a
1821
1945
  * renamed folder and a differently-named executable, none of which any fixed
1822
1946
  * path list can predict. Real machines put the app at `D:\workbuddy\WorkBuddy.exe`
1823
- * and `D:\workbuddyai\WorkBuddyAI.exe` — exactly the layouts a
1824
- * `%ProgramFiles%\WorkBuddy\WorkBuddy.exe` probe cannot see, which is why the
1825
- * plugin reported "the desktop app could not provide the key" for an app that was
1826
- * installed and running.
1947
+ * and `D:\ruanjian\anzhuang\腾讯\WorkBuddy\WorkBuddy.exe` — layouts a
1948
+ * `%ProgramFiles%\WorkBuddy\WorkBuddy.exe` probe cannot see.
1827
1949
  *
1828
1950
  * `DisplayIcon` is the field that actually carries the path (observed as
1829
1951
  * `D:\workbuddy\WorkBuddy.exe,0`); `InstallLocation` is usually empty for these
1830
1952
  * installers, so both are read and either may contribute.
1831
1953
  *
1954
+ * Two readers, because neither alone is enough:
1955
+ *
1956
+ * - **PowerShell first.** `Get-ItemProperty` talks to the registry API, so no
1957
+ * console code page is involved and non-ASCII paths come back intact. This
1958
+ * is the encoding-proof path.
1959
+ * - **`reg.exe` as a fallback**, for a machine where PowerShell is unavailable
1960
+ * (locked-down or stripped images). Its raw bytes are decoded through
1961
+ * {@link decodeWindowsConsole}, which is what the original code got wrong.
1962
+ *
1832
1963
  * Returns [] on any failure — a missing registry key is the normal case on
1833
1964
  * non-Windows, not an error.
1834
1965
  */
1835
1966
  function windowsRegistryAppPaths() {
1836
1967
  if (process.platform !== "win32") return [];
1837
- const roots = [
1838
- ["HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "/**"],
1839
- ["HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "/**"],
1840
- ["HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "/**"]
1841
- ];
1968
+ const viaPowerShell = windowsRegistryAppPathsViaPowerShell();
1969
+ if (viaPowerShell.length > 0) return viaPowerShell;
1970
+ return windowsRegistryAppPathsViaReg();
1971
+ }
1972
+ /**
1973
+ * Registry read through PowerShell, immune to the console code page.
1974
+ *
1975
+ * `[Console]::OutputEncoding` is set to UTF-8 so the bytes Node receives are
1976
+ * UTF-8 regardless of the machine's code page. Values are printed one per line
1977
+ * and matched by the caller, so a Chinese path survives the round trip.
1978
+ *
1979
+ * Best-effort: any failure (no PowerShell, no keys, a policy restriction)
1980
+ * returns [], and the `reg.exe` reader takes over.
1981
+ */
1982
+ function windowsRegistryAppPathsViaPowerShell() {
1983
+ const script = [
1984
+ "$ErrorActionPreference='SilentlyContinue'",
1985
+ "try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}",
1986
+ "$roots=@('HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*')",
1987
+ "foreach($r in $roots){",
1988
+ " Get-ItemProperty -Path $r | ForEach-Object {",
1989
+ " $dn=[string]$_.DisplayName; $di=[string]$_.DisplayIcon; $il=[string]$_.InstallLocation",
1990
+ " if($dn -match 'WorkBuddy|CodeBuddy' -or $di -match 'WorkBuddy|CodeBuddy'){",
1991
+ " if($di -ne ''){ Write-Output $di }",
1992
+ " if($il -ne ''){ Write-Output $il }",
1993
+ " }",
1994
+ " }",
1995
+ "}"
1996
+ ].join("\n");
1997
+ let stdout;
1998
+ try {
1999
+ stdout = execFileSync("powershell", [
2000
+ "-NoProfile",
2001
+ "-NonInteractive",
2002
+ "-ExecutionPolicy",
2003
+ "Bypass",
2004
+ "-Command",
2005
+ script
2006
+ ], {
2007
+ encoding: "utf8",
2008
+ timeout: 2e4,
2009
+ windowsHide: true,
2010
+ maxBuffer: 8388608
2011
+ });
2012
+ } catch {
2013
+ return [];
2014
+ }
1842
2015
  const out = [];
1843
- for (const [root] of roots) {
1844
- let listing;
2016
+ for (const line of stdout.split(/\r?\n/u)) {
2017
+ const cleaned = cleanIconPath(line.trim());
2018
+ if (cleaned !== "") out.push(cleaned);
2019
+ }
2020
+ return out;
2021
+ }
2022
+ /**
2023
+ * Registry read through `reg.exe`, with console-code-page detection.
2024
+ *
2025
+ * Kept as the fallback for machines without PowerShell. The decode step is the
2026
+ * fix: the output is captured as BYTES and decoded with whichever encoding
2027
+ * yields a path that exists, instead of assuming UTF-8 and quietly producing
2028
+ * mojibake that no existence check can ever match.
2029
+ */
2030
+ function windowsRegistryAppPathsViaReg() {
2031
+ const out = [];
2032
+ for (const root of UNINSTALL_KEY_ROOTS) {
2033
+ let listingBuf;
1845
2034
  try {
1846
- listing = execFileSync("reg", [
2035
+ listingBuf = execFileSync("reg", [
1847
2036
  "query",
1848
2037
  root,
1849
2038
  "/s",
1850
2039
  "/v",
1851
2040
  "DisplayName"
1852
2041
  ], {
1853
- encoding: "utf8",
1854
2042
  timeout: 1e4,
1855
2043
  windowsHide: true,
1856
2044
  maxBuffer: 8388608
@@ -1858,26 +2046,35 @@ function windowsRegistryAppPaths() {
1858
2046
  } catch {
1859
2047
  continue;
1860
2048
  }
1861
- const keys = listing.split(/\r?\n(?=HKEY_)/u).filter((block) => /WorkBuddy|CodeBuddy/iu.test(block));
2049
+ const keys = decodeWindowsConsole(listingBuf, (text) => /WorkBuddy|CodeBuddy/iu.test(text)).split(/\r?\n(?=HKEY_)/u).filter((block) => /WorkBuddy|CodeBuddy/iu.test(block));
1862
2050
  for (const key of keys) {
1863
2051
  const keyPath = /^(HKEY_[^\r\n]+)/u.exec(key)?.[1]?.trim();
1864
2052
  if (keyPath === void 0) continue;
1865
- for (const name of ["DisplayIcon", "InstallLocation"]) try {
1866
- const value = execFileSync("reg", [
1867
- "query",
1868
- keyPath,
1869
- "/v",
1870
- name
1871
- ], {
1872
- encoding: "utf8",
1873
- timeout: 5e3,
1874
- windowsHide: true
2053
+ for (const name of ["DisplayIcon", "InstallLocation"]) {
2054
+ let valueBuf;
2055
+ try {
2056
+ valueBuf = execFileSync("reg", [
2057
+ "query",
2058
+ keyPath,
2059
+ "/v",
2060
+ name
2061
+ ], {
2062
+ timeout: 5e3,
2063
+ windowsHide: true
2064
+ });
2065
+ } catch {
2066
+ continue;
2067
+ }
2068
+ const value = decodeWindowsConsole(valueBuf, (text) => {
2069
+ const candidate = /REG_(?:SZ|EXPAND_SZ)\s+(.+)$/mu.exec(text)?.[1]?.trim();
2070
+ if (candidate === void 0) return false;
2071
+ const path = cleanIconPath(candidate);
2072
+ return path !== "" && existsSync(path);
1875
2073
  });
1876
2074
  const raw = /REG_(?:SZ|EXPAND_SZ)\s+(.+)$/mu.exec(value)?.[1]?.trim();
1877
2075
  if (raw === void 0 || raw === "") continue;
1878
- const cleaned = raw.replace(/^"/u, "").replace(/",-?\d+$/u, "").replace(/,-?\d+$/u, "").trim();
1879
- out.push(cleaned);
1880
- } catch {}
2076
+ out.push(cleanIconPath(raw));
2077
+ }
1881
2078
  }
1882
2079
  }
1883
2080
  return out;
@@ -2146,7 +2343,7 @@ const ENCRYPTED_CREDENTIAL_CODE = "ENCRYPTED_CREDENTIAL";
2146
2343
  var WorkBuddyEncryptedCredentialError = class extends Error {
2147
2344
  code = ENCRYPTED_CREDENTIAL_CODE;
2148
2345
  constructor(sourcePath) {
2149
- super(`workbuddy: ${sourcePath} holds encrypted credentials, but no WorkBuddy desktop app could be located to provide the key. If the app IS installed, it is simply outside the paths this plugin probes — set WORKBUDDY_APP_EXECUTABLE to its full .exe path (then restart DSH) and the credential will open. Signing in again will not help: the credential itself is intact. Run \`dsh-workbuddy-xdpool doctor\` to see which paths were probed.`);
2346
+ super(`workbuddy: ${sourcePath} holds encrypted credentials, but no WorkBuddy desktop app could be located to provide the key. If the app IS installed, it is simply outside the paths this plugin probes — set WORKBUDDY_APP_EXECUTABLE to its full .exe path (then restart DSH) and the credential will open. Signing in again will not help: the credential itself is intact. Run \`dsh-workbuddy-xdpool doctor\` to see which paths were probed — if one of them looks like mojibake (e.g. "??" where a Chinese folder name should be), the registry value could not be decoded on this machine; report that line and use the override above in the meantime.`);
2150
2347
  this.name = "WorkBuddyEncryptedCredentialError";
2151
2348
  }
2152
2349
  };
@@ -2879,38 +3076,97 @@ ${new Date(until).toISOString()}
2879
3076
  * Static fallback used before the first live catalog fetch, and whenever the
2880
3077
  * upstream cannot be reached.
2881
3078
  *
2882
- * The multipliers are carried on purpose. Without them the provider's model
2883
- * picker silently loses every rate and every free badge the moment the live
2884
- * fetch fails — which reads to the user as "the plugin broke my model list"
2885
- * rather than "the upstream is unreachable". The values are the ones the two
2886
- * gateways actually advertise for these ids (`credits: "x0.79 credits"` and so
2887
- * on), so a fallback row looks the same as a live one.
3079
+ * This table is the DOMESTIC (CN) roster, and it is kept in step with what
3080
+ * `copilot.tencent.com` actually advertises. It matters more than a "just in
3081
+ * case" list usually would: a failed startup fetch falls back to it, and the
3082
+ * user sees that as "half my models were deleted" — `deepseek-v4.1-flash`
3083
+ * and friends simply vanishing from the picker, with no visible explanation.
3084
+ *
3085
+ * Every row below was captured from the live endpoint, INCLUDING the window
3086
+ * sizes: the previous version carried a 32K window for `hy3` when the gateway
3087
+ * says 192K, and listed `kimi-k3` under an id the gateway no longer uses
3088
+ * (`kimi-k3-1`). A stale fallback is worse than a short one — it looks
3089
+ * authoritative while being wrong.
2888
3090
  *
2889
3091
  * `multiplier: 0` is the gateways' own spelling of "free" (`credits: "x0.00"`),
2890
3092
  * which is what turns on the free badge.
2891
3093
  */
2892
3094
  const FALLBACK_WORKBUDDY_MODELS = [
3095
+ {
3096
+ id: "auto",
3097
+ name: "Auto",
3098
+ contextWindow: 256e3,
3099
+ maxOutputTokens: 32e3,
3100
+ supportsImages: true
3101
+ },
3102
+ {
3103
+ id: "hy4-preview",
3104
+ name: "Hy4 preview",
3105
+ contextWindow: 1e6,
3106
+ maxOutputTokens: 64e3,
3107
+ supportsImages: true,
3108
+ multiplier: .29
3109
+ },
3110
+ {
3111
+ id: "hy3",
3112
+ name: "Hy3",
3113
+ contextWindow: 192e3,
3114
+ maxOutputTokens: 64e3,
3115
+ supportsImages: true,
3116
+ multiplier: 0
3117
+ },
3118
+ {
3119
+ id: "hy3-x",
3120
+ name: "Hy3",
3121
+ contextWindow: 192e3,
3122
+ maxOutputTokens: 64e3,
3123
+ supportsImages: true,
3124
+ multiplier: .05
3125
+ },
3126
+ {
3127
+ id: "deepseek-v4.1-flash",
3128
+ name: "Deepseek-V4.1-Flash",
3129
+ contextWindow: 1e6,
3130
+ maxOutputTokens: 128e3,
3131
+ supportsImages: true,
3132
+ multiplier: .11
3133
+ },
3134
+ {
3135
+ id: "deepseek-v4-pro",
3136
+ name: "Deepseek-V4-Pro",
3137
+ contextWindow: 1e6,
3138
+ maxOutputTokens: 128e3,
3139
+ supportsImages: true,
3140
+ multiplier: .51
3141
+ },
3142
+ {
3143
+ id: "deepseek-v4-flash",
3144
+ name: "DeepSeek-V4-Flash",
3145
+ contextWindow: 1e6,
3146
+ maxOutputTokens: 128e3,
3147
+ supportsImages: true
3148
+ },
2893
3149
  {
2894
3150
  id: "glm-5.3",
2895
3151
  name: "GLM-5.3",
2896
- contextWindow: 2e5,
2897
- maxOutputTokens: 128e3,
3152
+ contextWindow: 1e6,
3153
+ maxOutputTokens: 64e3,
2898
3154
  supportsImages: true,
2899
3155
  multiplier: .79
2900
3156
  },
2901
3157
  {
2902
3158
  id: "glm-5.3-flash",
2903
3159
  name: "GLM-5.3-Flash",
2904
- contextWindow: 2e5,
2905
- maxOutputTokens: 128e3,
3160
+ contextWindow: 1e6,
3161
+ maxOutputTokens: 131072,
2906
3162
  supportsImages: true,
2907
3163
  multiplier: .06
2908
3164
  },
2909
3165
  {
2910
3166
  id: "glm-5.2",
2911
3167
  name: "GLM-5.2",
2912
- contextWindow: 2e5,
2913
- maxOutputTokens: 128e3,
3168
+ contextWindow: 1e6,
3169
+ maxOutputTokens: 64e3,
2914
3170
  supportsImages: true,
2915
3171
  multiplier: .79
2916
3172
  },
@@ -2918,53 +3174,57 @@ const FALLBACK_WORKBUDDY_MODELS = [
2918
3174
  id: "glm-5.1",
2919
3175
  name: "GLM-5.1",
2920
3176
  contextWindow: 2e5,
2921
- maxOutputTokens: 128e3,
2922
- supportsImages: false
2923
- },
2924
- {
2925
- id: "deepseek-v4-pro",
2926
- name: "DeepSeek-V4-Pro",
2927
- contextWindow: 2e5,
2928
- maxOutputTokens: 128e3,
2929
- supportsImages: true
3177
+ maxOutputTokens: 48e3,
3178
+ supportsImages: false,
3179
+ multiplier: .79
2930
3180
  },
2931
3181
  {
2932
- id: "deepseek-v4-flash",
2933
- name: "DeepSeek-V4-Flash",
3182
+ id: "glm-5v-turbo",
3183
+ name: "GLM-5v-Turbo",
2934
3184
  contextWindow: 2e5,
2935
- maxOutputTokens: 128e3,
2936
- supportsImages: true
3185
+ maxOutputTokens: 64e3,
3186
+ supportsImages: true,
3187
+ multiplier: .71
2937
3188
  },
2938
3189
  {
2939
- id: "kimi-k3",
3190
+ id: "kimi-k3-1",
2940
3191
  name: "Kimi-K3",
2941
- contextWindow: 2e5,
2942
- maxOutputTokens: 128e3,
3192
+ contextWindow: 1e6,
3193
+ maxOutputTokens: 32e3,
2943
3194
  supportsImages: true,
2944
3195
  multiplier: 1.62
2945
3196
  },
2946
3197
  {
2947
- id: "minimax-m3",
2948
- name: "MiniMax-M3",
2949
- contextWindow: 2e5,
2950
- maxOutputTokens: 128e3,
2951
- supportsImages: true
3198
+ id: "kimi-k2.8-preview",
3199
+ name: "Kimi-K2.8-Preview",
3200
+ contextWindow: 1e6,
3201
+ maxOutputTokens: 64e3,
3202
+ supportsImages: true,
3203
+ multiplier: .77
2952
3204
  },
2953
3205
  {
2954
- id: "hy3",
2955
- name: "Hy3",
2956
- contextWindow: 32e3,
2957
- maxOutputTokens: 8e3,
3206
+ id: "kimi-k2.7",
3207
+ name: "Kimi-K2.7-Code",
3208
+ contextWindow: 256e3,
3209
+ maxOutputTokens: 32e3,
2958
3210
  supportsImages: true,
2959
- multiplier: 0
3211
+ multiplier: .57
2960
3212
  },
2961
3213
  {
2962
- id: "hy4-preview",
2963
- name: "Hy4-Preview",
2964
- contextWindow: 1e6,
2965
- maxOutputTokens: 128e3,
3214
+ id: "kimi-k2.6",
3215
+ name: "Kimi-K2.6",
3216
+ contextWindow: 256e3,
3217
+ maxOutputTokens: 32e3,
2966
3218
  supportsImages: true,
2967
- multiplier: .29
3219
+ multiplier: .52
3220
+ },
3221
+ {
3222
+ id: "minimax-m3",
3223
+ name: "MiniMax-M3",
3224
+ contextWindow: 512e3,
3225
+ maxOutputTokens: 64e3,
3226
+ supportsImages: true,
3227
+ multiplier: .25
2968
3228
  }
2969
3229
  ];
2970
3230
  /** Live catalog with a static fallback behind it. */
@@ -2973,6 +3233,19 @@ var WorkBuddyCatalog = class {
2973
3233
  listeners = /* @__PURE__ */ new Set();
2974
3234
  /** User's model selection. Empty object = follow the catalog unfiltered. */
2975
3235
  selection = {};
3236
+ /**
3237
+ * Whether `models` came from the gateway or from the static table.
3238
+ *
3239
+ * Tracked so the card can SAY which it is showing. A failed fetch used to be
3240
+ * invisible on screen: the picker simply held fewer models than before, which
3241
+ * the user reasonably read as "the plugin deleted my models" rather than "the
3242
+ * network hiccuped at startup".
3243
+ */
3244
+ source = "fallback";
3245
+ /** When the live list last landed. */
3246
+ lastUpdatedAt;
3247
+ /** Why the last fetch failed, for display. */
3248
+ lastError;
2976
3249
  current() {
2977
3250
  return this.models;
2978
3251
  }
@@ -3007,6 +3280,7 @@ var WorkBuddyCatalog = class {
3007
3280
  /** Restore the static fallback, e.g. when the upstream stops answering. */
3008
3281
  reset() {
3009
3282
  this.models = FALLBACK_WORKBUDDY_MODELS;
3283
+ this.source = "fallback";
3010
3284
  this.notify();
3011
3285
  }
3012
3286
  /** Replace the user's selection; the adapter rebuilds from `visible()`. */
@@ -3018,6 +3292,31 @@ var WorkBuddyCatalog = class {
3018
3292
  currentSelection() {
3019
3293
  return this.selection;
3020
3294
  }
3295
+ /**
3296
+ * Whether this catalog is serving live data or the built-in table.
3297
+ *
3298
+ * `fallback` is not an error state, but it IS a degraded one: the user is
3299
+ * looking at a shorter roster than the gateway offers, so the card says so
3300
+ * and offers a retry instead of letting them wonder where the models went.
3301
+ */
3302
+ currentSource() {
3303
+ return this.source;
3304
+ }
3305
+ /**
3306
+ * When the live list last landed, if it ever did.
3307
+ *
3308
+ * Named `catalogUpdatedAt()` rather than `updatedAt()` because the class
3309
+ * already had an `updatedAt` member; two members of the same name is a
3310
+ * compile error, and the awkwardness is a useful signal that the concept is
3311
+ * "when THIS catalog was refreshed", not a generic timestamp.
3312
+ */
3313
+ catalogUpdatedAt() {
3314
+ return this.lastUpdatedAt;
3315
+ }
3316
+ /** Why the last fetch failed, if it did. */
3317
+ lastFetchError() {
3318
+ return this.lastError;
3319
+ }
3021
3320
  onChange(listener) {
3022
3321
  this.listeners.add(listener);
3023
3322
  return () => this.listeners.delete(listener);
@@ -3028,6 +3327,20 @@ var WorkBuddyCatalog = class {
3028
3327
  /** Replace the catalog from the live upstream list; keeps the fallback if empty. */
3029
3328
  updateFromUpstream(models) {
3030
3329
  this.update(catalogFromUpstream(models));
3330
+ this.source = "live";
3331
+ this.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
3332
+ this.lastError = void 0;
3333
+ }
3334
+ /**
3335
+ * Record that a fetch attempt failed, leaving the current list in place.
3336
+ *
3337
+ * The list is deliberately NOT reset here: a refresh that fails should keep
3338
+ * whatever working catalog is already loaded, rather than demoting a healthy
3339
+ * session to the static table because one retry ran out.
3340
+ */
3341
+ noteFetchFailure(message) {
3342
+ this.lastError = message;
3343
+ this.notify();
3031
3344
  }
3032
3345
  notify() {
3033
3346
  for (const listener of this.listeners) listener();
@@ -4920,6 +5233,31 @@ async function commandDoctor() {
4920
5233
  imported = (await readdir2(accountDir())).filter((name) => name.endsWith(".json"));
4921
5234
  } catch {}
4922
5235
  lines.push(imported.length === 0 ? " (none)" : imported.map((name) => ` ✓ ${name}`).join("\n"));
5236
+ lines.push("");
5237
+ lines.push("WorkBuddy desktop app:");
5238
+ const override = process.env[WORKBUDDY_APP_EXECUTABLE_ENV]?.trim();
5239
+ if (override !== void 0 && override !== "") lines.push(` env ${WORKBUDDY_APP_EXECUTABLE_ENV} = ${override}`);
5240
+ let candidates = [];
5241
+ try {
5242
+ candidates = workbuddyAppExecutableCandidates();
5243
+ } catch (error) {
5244
+ lines.push(` ✗ candidate probe threw: ${error instanceof Error ? error.message : String(error)}`);
5245
+ }
5246
+ let firstExisting;
5247
+ for (const candidate of candidates) {
5248
+ const exists = existsSync(candidate);
5249
+ if (exists && firstExisting === void 0) firstExisting = candidate;
5250
+ lines.push(` ${exists ? "✓" : "·"} ${candidate}`);
5251
+ }
5252
+ lines.push(` probed ${candidates.length} candidate path(s)`);
5253
+ if (firstExisting === void 0) {
5254
+ healthy = false;
5255
+ lines.push(" ✗ no WorkBuddy desktop executable found at any probed path.");
5256
+ lines.push(" If the app IS installed, set WORKBUDDY_APP_EXECUTABLE to its full .exe path");
5257
+ lines.push(" (or put that line in $DSH_HOME/.env) and restart DSH. A path shown above that");
5258
+ lines.push(" looks like mojibake (e.g. \"???\" for a Chinese folder) indicates the registry");
5259
+ lines.push(" value could not be decoded on this machine — please report it with the line.");
5260
+ } else lines.push(` ✓ using ${firstExisting}`);
4923
5261
  console.log(lines.join("\n"));
4924
5262
  return healthy ? 0 : 1;
4925
5263
  }